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": { "_get-748a816c.js": {
"file": "assets/get-10f2a251.js", "file": "assets/get-748a816c.js",
"imports": [ "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": [
"assets/layers-1dbbe9d0.png", "assets/layers-1dbbe9d0.png",
"assets/layers-2x-066daca8.png", "assets/layers-2x-066daca8.png",
@@ -15,8 +22,8 @@
"css": [ "css": [
"assets/vendor-49001d3f.css" "assets/vendor-49001d3f.css"
], ],
"file": "assets/vendor-5ec3da0f.js", "file": "assets/vendor-e194ad60.js",
"integrity": "sha384-ZVKGWd0fOujjIvJKxFA8SzgFIJQPEt1zpM+hgtL6o6S6gBRDTNEz2FlssKfAjVJ/" "integrity": "sha384-pBeK5qr0qG0MDsIfi2/X7NE5V+YUERUHOGDEL5JCGFtw8l+EiRe7D2uuMKV4/cxm"
}, },
"node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf": { "node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf": {
"file": "assets/fa-brands-400-5656d596.ttf", "file": "assets/fa-brands-400-5656d596.ttf",
@@ -64,24 +71,36 @@
"integrity": "sha384-wg83fCOXjBtqzFAWhTL9Sd9vmLUNhfEEzfmNUX9zwv2igKlz/YQbdapF4ObdxF+R" "integrity": "sha384-wg83fCOXjBtqzFAWhTL9Sd9vmLUNhfEEzfmNUX9zwv2igKlz/YQbdapF4ObdxF+R"
}, },
"resources/assets/v2/pages/dashboard/dashboard.js": { "resources/assets/v2/pages/dashboard/dashboard.js": {
"file": "assets/dashboard-f6763ad9.js", "file": "assets/dashboard-a55f7472.js",
"imports": [ "imports": [
"_get-10f2a251.js", "_load-translations-23553922.js",
"_vendor-5ec3da0f.js" "_get-748a816c.js",
"_vendor-e194ad60.js"
], ],
"isEntry": true, "isEntry": true,
"src": "resources/assets/v2/pages/dashboard/dashboard.js", "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": { "resources/assets/v2/pages/transactions/create.js": {
"file": "assets/create-5a2ad8a8.js", "file": "assets/create-77b1cf47.js",
"imports": [ "imports": [
"_get-10f2a251.js", "_load-translations-23553922.js",
"_vendor-5ec3da0f.js" "_vendor-e194ad60.js",
"_get-748a816c.js"
], ],
"isEntry": true, "isEntry": true,
"src": "resources/assets/v2/pages/transactions/create.js", "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": { "resources/assets/v2/sass/app.scss": {
"file": "assets/app-fb7b26ec.css", "file": "assets/app-fb7b26ec.css",

View File

@@ -20,16 +20,13 @@
import '../../boot/bootstrap.js'; import '../../boot/bootstrap.js';
import dates from '../../pages/shared/dates.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 {parseFromEntries} from "./shared/parse-from-entries.js";
import formatMoney from "../../util/format-money.js"; import formatMoney from "../../util/format-money.js";
import Autocomplete from "bootstrap5-autocomplete";
import Post from "../../api/v2/model/transaction/post.js"; 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 {getVariable} from "../../store/get-variable.js";
import {I18n} from "i18n-js"; import {I18n} from "i18n-js";
import {loadTranslations} from "../../support/load-translations.js"; import {loadTranslations} from "../../support/load-translations.js";
import Tags from "bootstrap5-tags";
import {loadCurrencies} from "./shared/load-currencies.js"; import {loadCurrencies} from "./shared/load-currencies.js";
import {loadBudgets} from "./shared/load-budgets.js"; import {loadBudgets} from "./shared/load-budgets.js";
import {loadPiggyBanks} from "./shared/load-piggy-banks.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 L from "leaflet";
import 'leaflet/dist/leaflet.css'; 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 upload attachments to other file
// TODO fix two maps, perhaps disconnect from entries entirely. // TODO fix two maps, perhaps disconnect from entries entirely.
@@ -54,121 +62,6 @@ const urls = {
tag: '/api/v2/autocomplete/tags', 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 () { let transactions = function () {
return { return {
// transactions are stored in "entries": // transactions are stored in "entries":
@@ -190,7 +83,8 @@ let transactions = function () {
// form behaviour during transaction // form behaviour during transaction
formBehaviour: { formBehaviour: {
formType: 'create', foreignCurrencyEnabled: true, formType: 'create',
foreignCurrencyEnabled: true,
}, },
// form data (except transactions) is stored in formData // form data (except transactions) is stored in formData
@@ -207,6 +101,8 @@ let transactions = function () {
// properties for the entire transaction group // properties for the entire transaction group
groupProperties: { groupProperties: {
transactionType: 'unknown', transactionType: 'unknown',
title: null,
id: null,
totalAmount: 0, totalAmount: 0,
}, },
@@ -223,8 +119,8 @@ let transactions = function () {
url: '', url: '',
}, },
wait: { wait: {
show: false,text: '', show: false,
url: '', text: '',
} }
}, },
@@ -259,13 +155,12 @@ let transactions = function () {
console.log('changedDescription'); console.log('changedDescription');
}, },
changedDestinationAccount(event) { changedDestinationAccount(event) {
console.log('changedDestinationAccount') this.detectTransactionType();
}, },
changedSourceAccount(event) { changedSourceAccount(event) {
console.log('changedSourceAccount') this.detectTransactionType();
}, },
detectTransactionType() { detectTransactionType() {
const sourceType = this.entries[0].source_account.type ?? 'unknown'; const sourceType = this.entries[0].source_account.type ?? 'unknown';
const destType = this.entries[0].destination_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.'); console.warn('Cannot infer transaction type from two unknown accounts.');
return; return;
} }
// transfer: both are the same and in strict set of account types // transfer: both are the same and in strict set of account types
if (sourceType === destType && ['Asset account', 'Loan', 'Debt', 'Mortgage'].includes(sourceType)) { if (sourceType === destType && ['Asset account', 'Loan', 'Debt', 'Mortgage'].includes(sourceType)) {
this.groupProperties.transactionType = 'transfer'; this.groupProperties.transactionType = 'transfer';
@@ -320,41 +216,34 @@ let transactions = function () {
} }
console.warn('Unknown account combination between "' + sourceType + '" and "' + destType + '".'); console.warn('Unknown account combination between "' + sourceType + '" and "' + destType + '".');
}, },
formattedTotalAmount() {
selectSourceAccount(item, ac) { if(this.entries.length === 0) {
const index = parseInt(ac._searchInput.attributes['data-index'].value); return formatMoney(this.groupProperties.totalAmount, 'EUR');
document.querySelector('#form')._x_dataStack[0].$data.entries[index].source_account = { }
id: item.id, return formatMoney(this.groupProperties.totalAmount, this.entries[0].currency_code ?? 'EUR');
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();
}, },
filterForeignCurrencies(code) { filterForeignCurrencies(code) {
console.log('filterForeignCurrencies("' + code + '")');
let list = []; let list = [];
let currency; let currency;
for (let i in this.enabledCurrencies) { for (let i in this.formData.enabledCurrencies) {
if (this.enabledCurrencies.hasOwnProperty(i)) { if (this.formData.enabledCurrencies.hasOwnProperty(i)) {
let current = this.enabledCurrencies[i]; let current = this.formData.enabledCurrencies[i];
if (current.code === code) { if (current.code === code) {
currency = current; currency = current;
} }
} }
} }
list.push(currency); list.push(currency);
this.foreignCurrencies = list; this.formData.foreignCurrencies = list;
// is he source account currency anyway: // is he source account currency anyway:
if (1 === list.length && list[0].code === this.entries[0].source_account.currency_code) { 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.'); 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) { 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.'); 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. // this also forces the currency_code on ALL entries.
@@ -365,19 +254,19 @@ let transactions = function () {
} }
}, },
filterNativeCurrencies(code) { filterNativeCurrencies(code) {
console.log('filterNativeCurrencies("' + code + '")');
let list = []; let list = [];
let currency; let currency;
for (let i in this.enabledCurrencies) { for (let i in this.formData.enabledCurrencies) {
if (this.enabledCurrencies.hasOwnProperty(i)) { if (this.formData.enabledCurrencies.hasOwnProperty(i)) {
let current = this.enabledCurrencies[i]; let current = this.formData.enabledCurrencies[i];
if (current.code === code) { if (current.code === code) {
currency = current; currency = current;
} }
} }
} }
list.push(currency); list.push(currency);
this.nativeCurrencies = list; this.formData.nativeCurrencies = list;
// this also forces the currency_code on ALL entries. // this also forces the currency_code on ALL entries.
for (let i in this.entries) { for (let i in this.entries) {
if (this.entries.hasOwnProperty(i)) { if (this.entries.hasOwnProperty(i)) {
@@ -388,164 +277,71 @@ let transactions = function () {
changedAmount(e) { changedAmount(e) {
const index = parseInt(e.target.dataset.index); const index = parseInt(e.target.dataset.index);
this.entries[index].amount = parseFloat(e.target.value); this.entries[index].amount = parseFloat(e.target.value);
this.totalAmount = 0; this.groupProperties.totalAmount = 0;
for (let i in this.entries) { for (let i in this.entries) {
if (this.entries.hasOwnProperty(i)) { 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() { addedSplit() {
console.log('addedSplit'); // addedSplit, is called from the HTML
// TODO improve code location // for source account
Autocomplete.init("input.ac-source", { const renderAccount = function (item, b, c) {
server: urls.account, return item.name_with_balance + '<br><small class="text-muted">' + i18n.t('firefly.account_type_' + item.type) + '</small>';
serverParams: { };
types: this.filters.source, console.log(this.filters);
}, addAutocomplete({
fetchOptions: { selector: 'input.ac-source',
headers: { serverUrl: urls.account,
'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content filters: this.filters.source,
} onRenderItem: renderAccount,
}, onChange: changeSourceAccount,
hiddenInput: true, onSelectItem: selectSourceAccount
preventBrowserAutocomplete: true,
highlightTyped: true,
liveServer: true,
onChange: this.changeSourceAccount,
onSelectItem: this.selectSourceAccount,
onRenderItem: function (item, b, c) {
return item.name_with_balance + '<br><small class="text-muted">' + i18n.t('firefly.account_type_' + item.type) + '</small>';
}
}); });
addAutocomplete({
Autocomplete.init("input.ac-category", { selector: 'input.ac-dest',
server: urls.category, serverUrl: urls.account,
fetchOptions: { filters: this.filters.destination,
headers: { onRenderItem: renderAccount,
'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content onChange: changeDestinationAccount,
} onSelectItem: selectDestinationAccount
},
valueField: "id",
labelField: "name",
highlightTyped: true,
onSelectItem: this.changeCategory,
onChange: this.changeCategory,
}); });
addAutocomplete({
Autocomplete.init("input.ac-dest", { selector: 'input.ac-category',
server: urls.account, serverUrl: urls.category,
serverParams: { valueField: 'id',
types: this.filters.destination, labelField: 'name',
}, onChange: changeCategory,
fetchOptions: { onSelectItem: changeCategory
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>';
}
}); });
this.filters.destination = []; addAutocomplete({
Autocomplete.init('input.ac-description', { selector: 'input.ac-description',
server: urls.description, serverUrl: urls.description,
fetchOptions: { valueField: 'id',
headers: { labelField: 'description',
'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content onChange: changeDescription,
} onSelectItem: changeDescription,
},
valueField: "id",
labelField: "description",
highlightTyped: true,
onSelectItem: this.changeDescription,
onChange: this.changeDescription,
}); });
}, },
processUpload(event) { processUpload(event) {
console.log('I am ALSO event listener for upload-success!'); this.showMessageOrRedirectUser();
console.log(event); },
this.showBarOrRedirect(); 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() { init() {
// get translations
// TODO loading translations could be better, but do this later.
Promise.all([getVariable('language', 'en_US')]).then((values) => { Promise.all([getVariable('language', 'en_US')]).then((values) => {
i18n = new I18n(); i18n = new I18n();
const locale = values[0].replace('-', '_'); const locale = values[0].replace('-', '_');
@@ -578,8 +374,12 @@ let transactions = function () {
document.addEventListener('upload-success', (event) => { document.addEventListener('upload-success', (event) => {
this.processUpload(event); this.processUpload(event);
document.querySelectorAll("input[type=file]").value = "";
}); });
document.addEventListener('upload-error', (event) => {
this.processUploadError(event);
});
// source can never be expense account // source can never be expense account
this.filters.source = ['Asset account', 'Loan', 'Debt', 'Mortgage', 'Revenue 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']; this.filters.destination = ['Expense account', 'Loan', 'Debt', 'Mortgage', 'Asset account'];
}, },
submitTransaction() { submitTransaction() {
// reset all views: // reset all messages:
this.submitting = true; this.notifications.error.show = false;
this.showSuccessMessage = false; this.notifications.success.show = false;
this.showErrorMessage = false; this.notifications.wait.show = false;
this.showWaitmessage = 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(); this.detectTransactionType();
// parse transaction: // parse transaction:
let transactions = parseFromEntries(this.entries, this.groupProperties.transactionType); let transactions = parseFromEntries(this.entries, this.groupProperties.transactionType);
let submission = { let submission = {
// todo process all options group_title: this.groupProperties.title,
group_title: null, fire_webhooks: false, apply_rules: false, transactions: transactions 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; submission.group_title = transactions[0].description;
} }
@@ -609,20 +423,25 @@ let transactions = function () {
let poster = new Post(); let poster = new Post();
console.log(submission); console.log(submission);
poster.post(submission).then((response) => { poster.post(submission).then((response) => {
// submission was a success. const group = response.data.data;
this.newGroupId = parseInt(response.data.data.id); // submission was a success!
this.newGroupTitle = submission.group_title ?? submission.transactions[0].description this.groupProperties.id = parseInt(group.id);
const attachmentCount = uploadAttachments(this.newGroupId, response.data.data.attributes.transactions); 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) { 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; return;
} }
// if not, respond to user options: // if not, respond to user options:
this.showBarOrRedirect(); this.showMessageOrRedirectUser();
}).catch((error) => { }).catch((error) => {
this.submitting = false; this.submitting = false;
console.log(error); console.log(error);
// todo put errors in form // todo put errors in form
@@ -633,153 +452,83 @@ let transactions = function () {
}); });
}, },
showBarOrRedirect() { showMessageOrRedirectUser() {
this.showWaitMessage = false; // disable all messages:
this.submitting = false; this.notifications.error.show = false;
if (this.returnHereButton) { this.notifications.success.show = false;
// todo create success banner this.notifications.wait.show = false;
this.showSuccessMessage = true;
this.successMessageLink = 'transactions/show/' + this.newGroupId; if (this.formStates.returnHereButton) {
this.successMessageText = i18n.t('firefly.stored_journal_js', {description: this.newGroupTitle});
// todo clear out form if necessary this.notifications.success.show = true;
if (this.resetButton) { 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.entries = [];
this.addSplit(); this.addSplit();
this.totalAmount = 0; this.groupProperties.totalAmount = 0;
} }
return;
} }
window.location = 'transactions/show/' + this.groupProperties.id + '?transaction_group_id=' + this.groupProperties.id + '&message=created';
if (!this.returnHereButton) {
window.location = 'transactions/show/' + this.newGroupId + '?transaction_group_id=' + this.newGroupId + '&message=created';
}
}, },
parseErrors(data) { parseErrors(data) {
this.setDefaultErrors(); // disable all messages:
this.showErrorMessage = true; this.notifications.error.show = true;
this.showSuccessMessage = false; this.notifications.success.show = false;
// todo create error banner. this.notifications.wait.show = false;
this.errorMessageText = i18n.t('firefly.errors_submission') + ' ' + data.message; this.formStates.isSubmitting = false;
let transactionIndex; this.notifications.error.text = i18n.t('firefly.errors_submission', {errorMessage: data.message});
let fieldName;
// todo add 'was-validated' to form. if(data.hasOwnProperty('errors')) {
console.log('Now processing errors.'); this.entries = spliceErrorsIntoTransactions(i18n, data.errors, this.entries);
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 (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() { addSplit() {
this.entries.push(createEmptySplit()); this.entries.push(createEmptySplit());
setTimeout(() => { // setTimeout(() => {
// render tags: // // render tags:
Tags.init('select.ac-tags', { // Tags.init('select.ac-tags', {
allowClear: true, // allowClear: true,
server: urls.tag, // server: urls.tag,
liveServer: true, // liveServer: true,
clearEnd: true, // clearEnd: true,
allowNew: true, // allowNew: true,
notFoundMessage: '(nothing found)', // notFoundMessage: '(nothing found)',
noCache: true, // noCache: true,
fetchOptions: { // fetchOptions: {
headers: { // headers: {
'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content // 'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content
} // }
} // }
}); // });
const count = this.entries.length - 1; // const count = this.entries.length - 1;
let map = L.map('location_map_' + count).setView([this.latitude, this.longitude], this.zoomLevel); // 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
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { // const id = 'location_map_' + count;
maxZoom: 19, // const map = () => {
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap ' + count + '</a>' // const el = document.getElementById(id),
}).addTo(map); // map = L.map(id).setView([this.latitude, this.longitude], this.zoomLevel)
map.on('click', this.addPointToMap); // L.tileLayer(
map.on('zoomend', this.saveZoomOfMap); // 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
this.entries[count].map // {attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap '+count+'</a>'}
// ).addTo(map)
// map.on('click', this.addPointToMap);
// map.on('zoomend', this.saveZoomOfMap);
// return map
// }
// this.entries[count].map = map();
// const id = 'location_map_' + count; // }, 250);
// const map = () => {
// const el = document.getElementById(id),
// map = L.map(id).setView([this.latitude, this.longitude], this.zoomLevel)
// L.tileLayer(
// 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
// {attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap '+count+'</a>'}
// ).addTo(map)
// map.on('click', this.addPointToMap);
// map.on('zoomend', this.saveZoomOfMap);
// return map
// }
// this.entries[count].map = map();
}, 250);
}, },
removeSplit(index) { removeSplit(index) {
@@ -788,9 +537,6 @@ let transactions = function () {
const triggerFirstTabEl = document.querySelector('#split-0-tab') const triggerFirstTabEl = document.querySelector('#split-0-tab')
triggerFirstTabEl.click(); triggerFirstTabEl.click();
}, },
formattedTotalAmount() {
return formatMoney(this.totalAmount, 'EUR');
},
clearLocation(e) { clearLocation(e) {
e.preventDefault(); e.preventDefault();
const target = e.currentTarget; const target = e.currentTarget;
@@ -844,15 +590,6 @@ function loadPage() {
Alpine.start(); 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. // wait for load until bootstrapped event is received.
document.addEventListener('firefly-iii-bootstrapped', () => { document.addEventListener('firefly-iii-bootstrapped', () => {
console.log('Loaded through event listener.'); 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() { export function createEmptySplit() {
let now = new Date(); let now = new Date();
let formatted = format(now, 'yyyy-MM-dd HH:mm'); let formatted = format(now, 'yyyy-MM-dd HH:mm');
@@ -75,44 +117,6 @@ export function createEmptySplit() {
payment_date: '', payment_date: '',
invoice_date: '', invoice_date: '',
errors: { errors: defaultErrorSet(),
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: [],
},
}; };
} }

View File

@@ -20,7 +20,6 @@
/** /**
* *
* @param entries
*/ */
export function parseFromEntries(entries, transactionType) { export function parseFromEntries(entries, transactionType) {
let returnArray = []; let returnArray = [];
@@ -71,8 +70,6 @@ export function parseFromEntries(entries, transactionType) {
current.zoom_level = entry.zoomLevel; current.zoom_level = entry.zoomLevel;
} }
// if foreign amount currency code is set: // if foreign amount currency code is set:
if (typeof entry.foreign_currency_code !== 'undefined' && '' !== entry.foreign_currency_code.toString()) { if (typeof entry.foreign_currency_code !== 'undefined' && '' !== entry.foreign_currency_code.toString()) {
current.foreign_currency_code = entry.foreign_currency_code; current.foreign_currency_code = entry.foreign_currency_code;
@@ -93,7 +90,6 @@ export function parseFromEntries(entries, transactionType) {
current.destination_id = entry.destination_account.id; current.destination_id = entry.destination_account.id;
} }
// TODO transaction type is hard coded:
current.type = transactionType; 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: // transactions:
'wait_attachments' => 'Please wait for the attachments to upload.', '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_foreign_if' => 'Amount in foreign currency, if any',
'amount_destination_account' => 'Amount in the currency of the destination account', 'amount_destination_account' => 'Amount in the currency of the destination account',
'edit_transaction_title' => 'Edit transaction ":description"', 'edit_transaction_title' => 'Edit transaction ":description"',
@@ -2469,7 +2470,7 @@ return [
'after_update_create_another' => 'After updating, return here to continue editing.', 'after_update_create_another' => 'After updating, return here to continue editing.',
'store_as_new' => 'Store as a new transaction instead of updating.', 'store_as_new' => 'Store as a new transaction instead of updating.',
'reset_after' => 'Reset form after submission', '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_expand_split' => 'Expand split',
'transaction_collapse_split' => 'Collapse split', 'transaction_collapse_split' => 'Collapse split',

View File

@@ -25,152 +25,154 @@
declare(strict_types=1); declare(strict_types=1);
return [ return [
'missing_where' => 'Array is missing "where"-clause', 'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.',
'missing_update' => 'Array is missing "update"-clause', 'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.',
'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause', 'missing_where' => 'Array is missing "where"-clause',
'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause', 'missing_update' => 'Array is missing "update"-clause',
'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.', 'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause',
'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.', 'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause',
'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.', 'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.',
'iban' => 'This is not a valid IBAN.', 'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.',
'zero_or_more' => 'The value cannot be negative.', 'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.',
'more_than_zero' => 'The value must be more than zero.', 'iban' => 'This is not a valid IBAN.',
'more_than_zero_correct' => 'The value must be zero or more.', 'zero_or_more' => 'The value cannot be negative.',
'no_asset_account' => 'This is not an asset account.', 'more_than_zero' => 'The value must be more than zero.',
'date_or_time' => 'The value must be a valid date or time value (ISO 8601).', 'more_than_zero_correct' => 'The value must be zero or more.',
'source_equals_destination' => 'The source account equals the destination account.', 'no_asset_account' => 'This is not an asset account.',
'unique_account_number_for_user' => 'It looks like this account number is already in use.', 'date_or_time' => 'The value must be a valid date or time value (ISO 8601).',
'unique_iban_for_user' => 'It looks like this IBAN is already in use.', 'source_equals_destination' => 'The source account equals the destination account.',
'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"', 'unique_account_number_for_user' => 'It looks like this account number is already in use.',
'deleted_user' => 'Due to security constraints, you cannot register using this email address.', 'unique_iban_for_user' => 'It looks like this IBAN is already in use.',
'rule_trigger_value' => 'This value is invalid for the selected trigger.', 'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"',
'rule_action_value' => 'This value is invalid for the selected action.', 'deleted_user' => 'Due to security constraints, you cannot register using this email address.',
'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', 'rule_trigger_value' => 'This value is invalid for the selected trigger.',
'file_attached' => 'Successfully uploaded file ":name".', 'rule_action_value' => 'This value is invalid for the selected action.',
'must_exist' => 'The ID in field :attribute does not exist in the database.', 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.',
'all_accounts_equal' => 'All accounts in this field must be equal.', 'file_attached' => 'Successfully uploaded file ":name".',
'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.', 'must_exist' => 'The ID in field :attribute does not exist in the database.',
'transaction_types_equal' => 'All splits must be of the same type.', 'all_accounts_equal' => 'All accounts in this field must be equal.',
'invalid_transaction_type' => 'Invalid transaction type.', 'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.',
'invalid_selection' => 'Your selection is invalid.', 'transaction_types_equal' => 'All splits must be of the same type.',
'belongs_user' => 'This value is linked to an object that does not seem to exist.', 'invalid_transaction_type' => 'Invalid transaction type.',
'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.', 'invalid_selection' => 'Your selection is invalid.',
'at_least_one_transaction' => 'Need at least one transaction.', 'belongs_user' => 'This value is linked to an object that does not seem to exist.',
'recurring_transaction_id' => 'Need at least one transaction.', 'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.',
'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.', 'at_least_one_transaction' => 'Need at least one transaction.',
'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.', 'recurring_transaction_id' => 'Need at least one transaction.',
'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.', 'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.',
'at_least_one_repetition' => 'Need at least one repetition.', 'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.',
'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.', 'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.',
'require_currency_info' => 'The content of this field is invalid without currency information.', 'at_least_one_repetition' => 'Need at least one repetition.',
'not_transfer_account' => 'This account is not an account that can be used for transfers.', 'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.',
'require_currency_amount' => 'The content of this field is invalid without foreign amount information.', 'require_currency_info' => 'The content of this field is invalid without currency information.',
'require_foreign_currency' => 'This field requires a number', 'not_transfer_account' => 'This account is not an account that can be used for transfers.',
'require_foreign_dest' => 'This field value must match the currency of the destination account.', 'require_currency_amount' => 'The content of this field is invalid without foreign amount information.',
'require_foreign_src' => 'This field value must match the currency of the source account.', 'require_foreign_currency' => 'This field requires a number',
'equal_description' => 'Transaction description should not equal global description.', 'require_foreign_dest' => 'This field value must match the currency of the destination account.',
'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', 'require_foreign_src' => 'This field value must match the currency of the source account.',
'file_too_large' => 'File ":name" is too large.', 'equal_description' => 'Transaction description should not equal global description.',
'belongs_to_user' => 'The value of :attribute is unknown.', 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.',
'accepted' => 'The :attribute must be accepted.', 'file_too_large' => 'File ":name" is too large.',
'bic' => 'This is not a valid BIC.', 'belongs_to_user' => 'The value of :attribute is unknown.',
'at_least_one_trigger' => 'Rule must have at least one trigger.', 'accepted' => 'The :attribute must be accepted.',
'at_least_one_active_trigger' => 'Rule must have at least one active trigger.', 'bic' => 'This is not a valid BIC.',
'at_least_one_action' => 'Rule must have at least one action.', 'at_least_one_trigger' => 'Rule must have at least one trigger.',
'at_least_one_active_action' => 'Rule must have at least one active action.', 'at_least_one_active_trigger' => 'Rule must have at least one active trigger.',
'base64' => 'This is not valid base64 encoded data.', 'at_least_one_action' => 'Rule must have at least one action.',
'model_id_invalid' => 'The given ID seems invalid for this model.', 'at_least_one_active_action' => 'Rule must have at least one active action.',
'less' => ':attribute must be less than 10,000,000', 'base64' => 'This is not valid base64 encoded data.',
'active_url' => 'The :attribute is not a valid URL.', 'model_id_invalid' => 'The given ID seems invalid for this model.',
'after' => 'The :attribute must be a date after :date.', 'less' => ':attribute must be less than 10,000,000',
'date_after' => 'The start date must be before the end date.', 'active_url' => 'The :attribute is not a valid URL.',
'alpha' => 'The :attribute may only contain letters.', 'after' => 'The :attribute must be a date after :date.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', 'date_after' => 'The start date must be before the end date.',
'alpha_num' => 'The :attribute may only contain letters and numbers.', 'alpha' => 'The :attribute may only contain letters.',
'array' => 'The :attribute must be an array.', 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
'unique_for_user' => 'There already is an entry with this :attribute.', 'alpha_num' => 'The :attribute may only contain letters and numbers.',
'before' => 'The :attribute must be a date before :date.', 'array' => 'The :attribute must be an array.',
'unique_object_for_user' => 'This name is already in use.', 'unique_for_user' => 'There already is an entry with this :attribute.',
'unique_account_for_user' => 'This account name is already in use.', 'before' => 'The :attribute must be a date before :date.',
'unique_object_for_user' => 'This name is already in use.',
'unique_account_for_user' => 'This account name is already in use.',
// Ignore this comment // Ignore this comment
'between.numeric' => 'The :attribute must be between :min and :max.', 'between.numeric' => 'The :attribute must be between :min and :max.',
'between.file' => 'The :attribute must be between :min and :max kilobytes.', 'between.file' => 'The :attribute must be between :min and :max kilobytes.',
'between.string' => 'The :attribute must be between :min and :max characters.', 'between.string' => 'The :attribute must be between :min and :max characters.',
'between.array' => 'The :attribute must have between :min and :max items.', 'between.array' => 'The :attribute must have between :min and :max items.',
'boolean' => 'The :attribute field must be true or false.', 'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.', 'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.', 'date' => 'The :attribute is not a valid date.',
'date_format' => 'The :attribute does not match the format :format.', 'date_format' => 'The :attribute does not match the format :format.',
'different' => 'The :attribute and :other must be different.', 'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.', 'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.', 'digits_between' => 'The :attribute must be between :min and :max digits.',
'email' => 'The :attribute must be a valid email address.', 'email' => 'The :attribute must be a valid email address.',
'filled' => 'The :attribute field is required.', 'filled' => 'The :attribute field is required.',
'exists' => 'The selected :attribute is invalid.', 'exists' => 'The selected :attribute is invalid.',
'image' => 'The :attribute must be an image.', 'image' => 'The :attribute must be an image.',
'in' => 'The selected :attribute is invalid.', 'in' => 'The selected :attribute is invalid.',
'integer' => 'The :attribute must be an integer.', 'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.', 'ip' => 'The :attribute must be a valid IP address.',
'json' => 'The :attribute must be a valid JSON string.', 'json' => 'The :attribute must be a valid JSON string.',
'max.numeric' => 'The :attribute may not be greater than :max.', 'max.numeric' => 'The :attribute may not be greater than :max.',
'max.file' => 'The :attribute may not be greater than :max kilobytes.', 'max.file' => 'The :attribute may not be greater than :max kilobytes.',
'max.string' => 'The :attribute may not be greater than :max characters.', 'max.string' => 'The :attribute may not be greater than :max characters.',
'max.array' => 'The :attribute may not have more than :max items.', 'max.array' => 'The :attribute may not have more than :max items.',
'mimes' => 'The :attribute must be a file of type: :values.', 'mimes' => 'The :attribute must be a file of type: :values.',
'min.numeric' => 'The :attribute must be at least :min.', 'min.numeric' => 'The :attribute must be at least :min.',
'lte.numeric' => 'The :attribute must be less than or equal :value.', 'lte.numeric' => 'The :attribute must be less than or equal :value.',
'min.file' => 'The :attribute must be at least :min kilobytes.', 'min.file' => 'The :attribute must be at least :min kilobytes.',
'min.string' => 'The :attribute must be at least :min characters.', 'min.string' => 'The :attribute must be at least :min characters.',
'min.array' => 'The :attribute must have at least :min items.', 'min.array' => 'The :attribute must have at least :min items.',
'not_in' => 'The selected :attribute is invalid.', 'not_in' => 'The selected :attribute is invalid.',
'numeric' => 'The :attribute must be a number.', 'numeric' => 'The :attribute must be a number.',
'scientific_notation' => 'The :attribute cannot use the scientific notation.', 'scientific_notation' => 'The :attribute cannot use the scientific notation.',
'numeric_native' => 'The native amount must be a number.', 'numeric_native' => 'The native amount must be a number.',
'numeric_destination' => 'The destination amount must be a number.', 'numeric_destination' => 'The destination amount must be a number.',
'numeric_source' => 'The source amount must be a number.', 'numeric_source' => 'The source amount must be a number.',
'regex' => 'The :attribute format is invalid.', 'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.', 'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.', 'required_if' => 'The :attribute field is required when :other is :value.',
'required_unless' => 'The :attribute field is required unless :other is in :values.', 'required_unless' => 'The :attribute field is required unless :other is in :values.',
'required_with' => 'The :attribute field is required when :values is present.', 'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values is present.', 'required_with_all' => 'The :attribute field is required when :values is present.',
'required_without' => 'The :attribute field is required when :values is not present.', 'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.', 'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute and :other must match.', 'same' => 'The :attribute and :other must match.',
'size.numeric' => 'The :attribute must be :size.', 'size.numeric' => 'The :attribute must be :size.',
'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.', 'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.',
'size.file' => 'The :attribute must be :size kilobytes.', 'size.file' => 'The :attribute must be :size kilobytes.',
'size.string' => 'The :attribute must be :size characters.', 'size.string' => 'The :attribute must be :size characters.',
'size.array' => 'The :attribute must contain :size items.', 'size.array' => 'The :attribute must contain :size items.',
'unique' => 'The :attribute has already been taken.', 'unique' => 'The :attribute has already been taken.',
'string' => 'The :attribute must be a string.', 'string' => 'The :attribute must be a string.',
'url' => 'The :attribute format is invalid.', 'url' => 'The :attribute format is invalid.',
'timezone' => 'The :attribute must be a valid zone.', 'timezone' => 'The :attribute must be a valid zone.',
'2fa_code' => 'The :attribute field is invalid.', '2fa_code' => 'The :attribute field is invalid.',
'dimensions' => 'The :attribute has invalid image dimensions.', 'dimensions' => 'The :attribute has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.', 'distinct' => 'The :attribute field has a duplicate value.',
'file' => 'The :attribute must be a file.', 'file' => 'The :attribute must be a file.',
'in_array' => 'The :attribute field does not exist in :other.', 'in_array' => 'The :attribute field does not exist in :other.',
'present' => 'The :attribute field must be present.', 'present' => 'The :attribute field must be present.',
'amount_zero' => 'The total amount cannot be zero.', 'amount_zero' => 'The total amount cannot be zero.',
'current_target_amount' => 'The current amount must be less than the target amount.', 'current_target_amount' => 'The current amount must be less than the target amount.',
'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.', 'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.',
'unique_object_group' => 'The group name must be unique', 'unique_object_group' => 'The group name must be unique',
'starts_with' => 'The value must start with :values.', 'starts_with' => 'The value must start with :values.',
'unique_webhook' => 'You already have a webhook with this combination of URL, trigger, response and delivery.', 'unique_webhook' => 'You already have a webhook with this combination of URL, trigger, response and delivery.',
'unique_existing_webhook' => 'You already have another webhook with this combination of URL, trigger, response and delivery.', 'unique_existing_webhook' => 'You already have another webhook with this combination of URL, trigger, response and delivery.',
'same_account_type' => 'Both accounts must be of the same account type', 'same_account_type' => 'Both accounts must be of the same account type',
'same_account_currency' => 'Both accounts must have the same currency setting', 'same_account_currency' => 'Both accounts must have the same currency setting',
// Ignore this comment // Ignore this comment
'secure_password' => 'This is not a secure password. Please try again. For more information, visit https://bit.ly/FF3-password-security', 'secure_password' => 'This is not a secure password. Please try again. For more information, visit https://bit.ly/FF3-password-security',
'valid_recurrence_rep_type' => 'Invalid repetition type for recurring transactions.', 'valid_recurrence_rep_type' => 'Invalid repetition type for recurring transactions.',
'valid_recurrence_rep_moment' => 'Invalid repetition moment for this type of repetition.', 'valid_recurrence_rep_moment' => 'Invalid repetition moment for this type of repetition.',
'invalid_account_info' => 'Invalid account information.', 'invalid_account_info' => 'Invalid account information.',
'attributes' => [ 'attributes' => [
'email' => 'email address', 'email' => 'email address',
'description' => 'description', 'description' => 'description',
'amount' => 'amount', 'amount' => 'amount',
@@ -209,57 +211,57 @@ return [
], ],
// validation of accounts: // validation of accounts:
'withdrawal_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', 'withdrawal_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'withdrawal_source_bad_data' => '[a] Could not find a valid source account when searching for ID ":id" or name ":name".', 'withdrawal_source_bad_data' => '[a] Could not find a valid source account when searching for ID ":id" or name ":name".',
'withdrawal_dest_need_data' => '[a] Need to get a valid destination account ID and/or valid destination account name to continue.', 'withdrawal_dest_need_data' => '[a] Need to get a valid destination account ID and/or valid destination account name to continue.',
'withdrawal_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', 'withdrawal_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'withdrawal_dest_iban_exists' => 'This destination account IBAN is already in use by an asset account or a liability and cannot be used as a withdrawal destination.', 'withdrawal_dest_iban_exists' => 'This destination account IBAN is already in use by an asset account or a liability and cannot be used as a withdrawal destination.',
'deposit_src_iban_exists' => 'This source account IBAN is already in use by an asset account or a liability and cannot be used as a deposit source.', 'deposit_src_iban_exists' => 'This source account IBAN is already in use by an asset account or a liability and cannot be used as a deposit source.',
'reconciliation_source_bad_data' => 'Could not find a valid reconciliation account when searching for ID ":id" or name ":name".', 'reconciliation_source_bad_data' => 'Could not find a valid reconciliation account when searching for ID ":id" or name ":name".',
'generic_source_bad_data' => '[e] Could not find a valid source account when searching for ID ":id" or name ":name".', 'generic_source_bad_data' => '[e] Could not find a valid source account when searching for ID ":id" or name ":name".',
'deposit_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', 'deposit_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'deposit_source_bad_data' => '[b] Could not find a valid source account when searching for ID ":id" or name ":name".', 'deposit_source_bad_data' => '[b] Could not find a valid source account when searching for ID ":id" or name ":name".',
'deposit_dest_need_data' => '[b] Need to get a valid destination account ID and/or valid destination account name to continue.', 'deposit_dest_need_data' => '[b] Need to get a valid destination account ID and/or valid destination account name to continue.',
'deposit_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', 'deposit_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'deposit_dest_wrong_type' => 'The submitted destination account is not of the right type.', 'deposit_dest_wrong_type' => 'The submitted destination account is not of the right type.',
// Ignore this comment // Ignore this comment
'transfer_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', 'transfer_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'transfer_source_bad_data' => '[c] Could not find a valid source account when searching for ID ":id" or name ":name".', 'transfer_source_bad_data' => '[c] Could not find a valid source account when searching for ID ":id" or name ":name".',
'transfer_dest_need_data' => '[c] Need to get a valid destination account ID and/or valid destination account name to continue.', 'transfer_dest_need_data' => '[c] Need to get a valid destination account ID and/or valid destination account name to continue.',
'transfer_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', 'transfer_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'need_id_in_edit' => 'Each split must have transaction_journal_id (either valid ID or 0).', 'need_id_in_edit' => 'Each split must have transaction_journal_id (either valid ID or 0).',
'ob_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.', 'ob_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'lc_source_need_data' => 'Need to get a valid source account ID to continue.', 'lc_source_need_data' => 'Need to get a valid source account ID to continue.',
'ob_dest_need_data' => '[d] Need to get a valid destination account ID and/or valid destination account name to continue.', 'ob_dest_need_data' => '[d] Need to get a valid destination account ID and/or valid destination account name to continue.',
'ob_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".', 'ob_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'reconciliation_either_account' => 'To submit a reconciliation, you must submit either a source or a destination account. Not both, not neither.', 'reconciliation_either_account' => 'To submit a reconciliation, you must submit either a source or a destination account. Not both, not neither.',
'generic_invalid_source' => 'You can\'t use this account as the source account.', 'generic_invalid_source' => 'You can\'t use this account as the source account.',
'generic_invalid_destination' => 'You can\'t use this account as the destination account.', 'generic_invalid_destination' => 'You can\'t use this account as the destination account.',
'generic_no_source' => 'You must submit source account information or submit a transaction journal ID.', 'generic_no_source' => 'You must submit source account information or submit a transaction journal ID.',
'generic_no_destination' => 'You must submit destination account information or submit a transaction journal ID.', 'generic_no_destination' => 'You must submit destination account information or submit a transaction journal ID.',
'gte.numeric' => 'The :attribute must be greater than or equal to :value.', 'gte.numeric' => 'The :attribute must be greater than or equal to :value.',
'gt.numeric' => 'The :attribute must be greater than :value.', 'gt.numeric' => 'The :attribute must be greater than :value.',
'gte.file' => 'The :attribute must be greater than or equal to :value kilobytes.', 'gte.file' => 'The :attribute must be greater than or equal to :value kilobytes.',
'gte.string' => 'The :attribute must be greater than or equal to :value characters.', 'gte.string' => 'The :attribute must be greater than or equal to :value characters.',
'gte.array' => 'The :attribute must have :value items or more.', 'gte.array' => 'The :attribute must have :value items or more.',
'amount_required_for_auto_budget' => 'The amount is required.', 'amount_required_for_auto_budget' => 'The amount is required.',
'auto_budget_amount_positive' => 'The amount must be more than zero.', 'auto_budget_amount_positive' => 'The amount must be more than zero.',
'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.', 'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.',
// no access to administration: // no access to administration:
'no_access_user_group' => 'You do not have the correct access rights for this administration.', 'no_access_user_group' => 'You do not have the correct access rights for this administration.',
]; ];
// Ignore this comment // Ignore this comment

View File

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

View File

@@ -109,7 +109,7 @@
<div class="card-footer"> <div class="card-footer">
<div class="row"> <div class="row">
<div class="col text-end"> <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> </div>
</div> </div>
@@ -117,11 +117,11 @@
</div> </div>
<div class="col-12"> <div class="col-12">
<template x-if="0 !== index"> <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 Remove this split
</button> </button>
</template> </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> </div>
</div> </div>

View File

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

View File

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

View File

@@ -5,11 +5,16 @@
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" <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" :id="'dest_' + index"
@changed="changedDestinationAccount"
x-model="transaction.destination_account.alpine_name" x-model="transaction.destination_account.alpine_name"
:data-index="index" :data-index="index"
@changed="changedDestinationAccount"
placeholder="{{ __('firefly.destination_account') }}"> 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>
</div> </div>

View File

@@ -5,11 +5,16 @@
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" <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" :id="'source_' + index"
x-model="transaction.source_account.alpine_name" x-model="transaction.source_account.alpine_name"
:data-index="index" :data-index="index"
@changed="changedSourceAccount" @changed="changedSourceAccount"
placeholder="{{ __('firefly.source_account') }}"> 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>
</div> </div>

View File

@@ -15,7 +15,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col text-end"> <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> </div>
</div> </div>