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) {
return item.name_with_balance + '<br><small class="text-muted">' + i18n.t('firefly.account_type_' + item.type) + '</small>';
}
// 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,153 +452,83 @@ 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 (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));
}
}
if(data.hasOwnProperty('errors')) {
this.entries = spliceErrorsIntoTransactions(i18n, data.errors, this.entries);
}
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);
// 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
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 = () => {
// 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();
// const id = 'location_map_' + count;
// 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);
// }, 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,152 +25,154 @@
declare(strict_types=1);
return [
'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',
'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause',
'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.',
'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.',
'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.',
'iban' => 'This is not a valid IBAN.',
'zero_or_more' => 'The value cannot be negative.',
'more_than_zero' => 'The value must be more than zero.',
'more_than_zero_correct' => 'The value must be zero or more.',
'no_asset_account' => 'This is not an asset account.',
'date_or_time' => 'The value must be a valid date or time value (ISO 8601).',
'source_equals_destination' => 'The source account equals the destination account.',
'unique_account_number_for_user' => 'It looks like this account number is already in use.',
'unique_iban_for_user' => 'It looks like this IBAN is already in use.',
'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"',
'deleted_user' => 'Due to security constraints, you cannot register using this email address.',
'rule_trigger_value' => 'This value is invalid for the selected trigger.',
'rule_action_value' => 'This value is invalid for the selected action.',
'file_already_attached' => 'Uploaded file ":name" is already attached to this object.',
'file_attached' => 'Successfully uploaded file ":name".',
'must_exist' => 'The ID in field :attribute does not exist in the database.',
'all_accounts_equal' => 'All accounts in this field must be equal.',
'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.',
'transaction_types_equal' => 'All splits must be of the same type.',
'invalid_transaction_type' => 'Invalid transaction type.',
'invalid_selection' => 'Your selection is invalid.',
'belongs_user' => 'This value is linked to an object that does not seem to exist.',
'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.',
'at_least_one_transaction' => 'Need at least one transaction.',
'recurring_transaction_id' => 'Need at least one transaction.',
'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.',
'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.',
'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.',
'at_least_one_repetition' => 'Need at least one repetition.',
'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.',
'require_currency_info' => 'The content of this field is invalid without currency information.',
'not_transfer_account' => 'This account is not an account that can be used for transfers.',
'require_currency_amount' => 'The content of this field is invalid without foreign amount information.',
'require_foreign_currency' => 'This field requires a number',
'require_foreign_dest' => 'This field value must match the currency of the destination account.',
'require_foreign_src' => 'This field value must match the currency of the source account.',
'equal_description' => 'Transaction description should not equal global description.',
'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.',
'file_too_large' => 'File ":name" is too large.',
'belongs_to_user' => 'The value of :attribute is unknown.',
'accepted' => 'The :attribute must be accepted.',
'bic' => 'This is not a valid BIC.',
'at_least_one_trigger' => 'Rule must have at least one trigger.',
'at_least_one_active_trigger' => 'Rule must have at least one active trigger.',
'at_least_one_action' => 'Rule must have at least one action.',
'at_least_one_active_action' => 'Rule must have at least one active action.',
'base64' => 'This is not valid base64 encoded data.',
'model_id_invalid' => 'The given ID seems invalid for this model.',
'less' => ':attribute must be less than 10,000,000',
'active_url' => 'The :attribute is not a valid URL.',
'after' => 'The :attribute must be a date after :date.',
'date_after' => 'The start date must be before the end date.',
'alpha' => 'The :attribute may only contain letters.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
'alpha_num' => 'The :attribute may only contain letters and numbers.',
'array' => 'The :attribute must be an array.',
'unique_for_user' => 'There already is an entry with this :attribute.',
'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.',
'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',
'invalid_update_key' => 'JSON contains an invalid key for the "update"-clause',
'invalid_query_data' => 'There is invalid data in the %s:%s field of your query.',
'invalid_query_account_type' => 'Your query contains accounts of different types, which is not allowed.',
'invalid_query_currency' => 'Your query contains accounts that have different currency settings, which is not allowed.',
'iban' => 'This is not a valid IBAN.',
'zero_or_more' => 'The value cannot be negative.',
'more_than_zero' => 'The value must be more than zero.',
'more_than_zero_correct' => 'The value must be zero or more.',
'no_asset_account' => 'This is not an asset account.',
'date_or_time' => 'The value must be a valid date or time value (ISO 8601).',
'source_equals_destination' => 'The source account equals the destination account.',
'unique_account_number_for_user' => 'It looks like this account number is already in use.',
'unique_iban_for_user' => 'It looks like this IBAN is already in use.',
'reconciled_forbidden_field' => 'This transaction is already reconciled, you cannot change the ":field"',
'deleted_user' => 'Due to security constraints, you cannot register using this email address.',
'rule_trigger_value' => 'This value is invalid for the selected trigger.',
'rule_action_value' => 'This value is invalid for the selected action.',
'file_already_attached' => 'Uploaded file ":name" is already attached to this object.',
'file_attached' => 'Successfully uploaded file ":name".',
'must_exist' => 'The ID in field :attribute does not exist in the database.',
'all_accounts_equal' => 'All accounts in this field must be equal.',
'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.',
'transaction_types_equal' => 'All splits must be of the same type.',
'invalid_transaction_type' => 'Invalid transaction type.',
'invalid_selection' => 'Your selection is invalid.',
'belongs_user' => 'This value is linked to an object that does not seem to exist.',
'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.',
'at_least_one_transaction' => 'Need at least one transaction.',
'recurring_transaction_id' => 'Need at least one transaction.',
'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.',
'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.',
'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.',
'at_least_one_repetition' => 'Need at least one repetition.',
'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.',
'require_currency_info' => 'The content of this field is invalid without currency information.',
'not_transfer_account' => 'This account is not an account that can be used for transfers.',
'require_currency_amount' => 'The content of this field is invalid without foreign amount information.',
'require_foreign_currency' => 'This field requires a number',
'require_foreign_dest' => 'This field value must match the currency of the destination account.',
'require_foreign_src' => 'This field value must match the currency of the source account.',
'equal_description' => 'Transaction description should not equal global description.',
'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.',
'file_too_large' => 'File ":name" is too large.',
'belongs_to_user' => 'The value of :attribute is unknown.',
'accepted' => 'The :attribute must be accepted.',
'bic' => 'This is not a valid BIC.',
'at_least_one_trigger' => 'Rule must have at least one trigger.',
'at_least_one_active_trigger' => 'Rule must have at least one active trigger.',
'at_least_one_action' => 'Rule must have at least one action.',
'at_least_one_active_action' => 'Rule must have at least one active action.',
'base64' => 'This is not valid base64 encoded data.',
'model_id_invalid' => 'The given ID seems invalid for this model.',
'less' => ':attribute must be less than 10,000,000',
'active_url' => 'The :attribute is not a valid URL.',
'after' => 'The :attribute must be a date after :date.',
'date_after' => 'The start date must be before the end date.',
'alpha' => 'The :attribute may only contain letters.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
'alpha_num' => 'The :attribute may only contain letters and numbers.',
'array' => 'The :attribute must be an array.',
'unique_for_user' => 'There already is an entry with this :attribute.',
'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
'between.numeric' => 'The :attribute must be between :min and :max.',
'between.file' => 'The :attribute must be between :min and :max kilobytes.',
'between.string' => 'The :attribute must be between :min and :max characters.',
'between.array' => 'The :attribute must have between :min and :max items.',
'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.',
'date_format' => 'The :attribute does not match the format :format.',
'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.',
'email' => 'The :attribute must be a valid email address.',
'filled' => 'The :attribute field is required.',
'exists' => 'The selected :attribute is invalid.',
'image' => 'The :attribute must be an image.',
'in' => 'The selected :attribute is invalid.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',
'json' => 'The :attribute must be a valid JSON string.',
'max.numeric' => 'The :attribute may not be greater than :max.',
'max.file' => 'The :attribute may not be greater than :max kilobytes.',
'max.string' => 'The :attribute may not be greater than :max characters.',
'max.array' => 'The :attribute may not have more than :max items.',
'mimes' => 'The :attribute must be a file of type: :values.',
'min.numeric' => 'The :attribute must be at least :min.',
'lte.numeric' => 'The :attribute must be less than or equal :value.',
'min.file' => 'The :attribute must be at least :min kilobytes.',
'min.string' => 'The :attribute must be at least :min characters.',
'min.array' => 'The :attribute must have at least :min items.',
'not_in' => 'The selected :attribute is invalid.',
'numeric' => 'The :attribute must be a number.',
'scientific_notation' => 'The :attribute cannot use the scientific notation.',
'numeric_native' => 'The native amount must be a number.',
'numeric_destination' => 'The destination amount must be a number.',
'numeric_source' => 'The source amount must be a number.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.',
'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_all' => 'The :attribute field is required when :values is 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.',
'same' => 'The :attribute and :other must match.',
'size.numeric' => 'The :attribute must be :size.',
'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.',
'size.file' => 'The :attribute must be :size kilobytes.',
'size.string' => 'The :attribute must be :size characters.',
'size.array' => 'The :attribute must contain :size items.',
'unique' => 'The :attribute has already been taken.',
'string' => 'The :attribute must be a string.',
'url' => 'The :attribute format is invalid.',
'timezone' => 'The :attribute must be a valid zone.',
'2fa_code' => 'The :attribute field is invalid.',
'dimensions' => 'The :attribute has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.',
'file' => 'The :attribute must be a file.',
'in_array' => 'The :attribute field does not exist in :other.',
'present' => 'The :attribute field must be present.',
'amount_zero' => 'The total amount cannot be zero.',
'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_object_group' => 'The group name must be unique',
'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_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_currency' => 'Both accounts must have the same currency setting',
'between.numeric' => 'The :attribute must be between :min and :max.',
'between.file' => 'The :attribute must be between :min and :max kilobytes.',
'between.string' => 'The :attribute must be between :min and :max characters.',
'between.array' => 'The :attribute must have between :min and :max items.',
'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.',
'date_format' => 'The :attribute does not match the format :format.',
'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.',
'email' => 'The :attribute must be a valid email address.',
'filled' => 'The :attribute field is required.',
'exists' => 'The selected :attribute is invalid.',
'image' => 'The :attribute must be an image.',
'in' => 'The selected :attribute is invalid.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',
'json' => 'The :attribute must be a valid JSON string.',
'max.numeric' => 'The :attribute may not be greater than :max.',
'max.file' => 'The :attribute may not be greater than :max kilobytes.',
'max.string' => 'The :attribute may not be greater than :max characters.',
'max.array' => 'The :attribute may not have more than :max items.',
'mimes' => 'The :attribute must be a file of type: :values.',
'min.numeric' => 'The :attribute must be at least :min.',
'lte.numeric' => 'The :attribute must be less than or equal :value.',
'min.file' => 'The :attribute must be at least :min kilobytes.',
'min.string' => 'The :attribute must be at least :min characters.',
'min.array' => 'The :attribute must have at least :min items.',
'not_in' => 'The selected :attribute is invalid.',
'numeric' => 'The :attribute must be a number.',
'scientific_notation' => 'The :attribute cannot use the scientific notation.',
'numeric_native' => 'The native amount must be a number.',
'numeric_destination' => 'The destination amount must be a number.',
'numeric_source' => 'The source amount must be a number.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.',
'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_all' => 'The :attribute field is required when :values is 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.',
'same' => 'The :attribute and :other must match.',
'size.numeric' => 'The :attribute must be :size.',
'amount_min_over_max' => 'The minimum amount cannot be larger than the maximum amount.',
'size.file' => 'The :attribute must be :size kilobytes.',
'size.string' => 'The :attribute must be :size characters.',
'size.array' => 'The :attribute must contain :size items.',
'unique' => 'The :attribute has already been taken.',
'string' => 'The :attribute must be a string.',
'url' => 'The :attribute format is invalid.',
'timezone' => 'The :attribute must be a valid zone.',
'2fa_code' => 'The :attribute field is invalid.',
'dimensions' => 'The :attribute has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.',
'file' => 'The :attribute must be a file.',
'in_array' => 'The :attribute field does not exist in :other.',
'present' => 'The :attribute field must be present.',
'amount_zero' => 'The total amount cannot be zero.',
'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_object_group' => 'The group name must be unique',
'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_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_currency' => 'Both accounts must have the same currency setting',
// Ignore this comment
'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_moment' => 'Invalid repetition moment for this type of repetition.',
'invalid_account_info' => 'Invalid account information.',
'attributes' => [
'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_moment' => 'Invalid repetition moment for this type of repetition.',
'invalid_account_info' => 'Invalid account information.',
'attributes' => [
'email' => 'email address',
'description' => 'description',
'amount' => 'amount',
@@ -209,57 +211,57 @@ return [
],
// 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_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_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'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_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_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.',
'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.',
'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_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_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_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_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_wrong_type' => 'The submitted destination account is not of the right type.',
// 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_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_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).',
'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_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".',
'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.',
'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_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.',
'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.',
'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".',
'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_destination' => 'You can\'t use this account as the destination 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_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_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.',
'gte.numeric' => 'The :attribute must be greater than or equal to :value.',
'gt.numeric' => 'The :attribute must be greater than :value.',
'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.array' => 'The :attribute must have :value items or more.',
'gte.numeric' => 'The :attribute must be greater than or equal to :value.',
'gt.numeric' => 'The :attribute must be greater than :value.',
'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.array' => 'The :attribute must have :value items or more.',
'amount_required_for_auto_budget' => 'The amount is required.',
'auto_budget_amount_positive' => 'The amount must be more than zero.',
'amount_required_for_auto_budget' => 'The amount is required.',
'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_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

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>