diff --git a/app/Api/V2/Controllers/Model/Transaction/StoreController.php b/app/Api/V2/Controllers/Model/Transaction/StoreController.php new file mode 100644 index 0000000000..24f5c815fb --- /dev/null +++ b/app/Api/V2/Controllers/Model/Transaction/StoreController.php @@ -0,0 +1,43 @@ +. + */ + +namespace FireflyIII\Api\V2\Controllers\Model\Transaction; + +use FireflyIII\Api\V2\Controllers\Controller; +use Illuminate\Http\JsonResponse; + +/** + * Class StoreController + */ +class StoreController extends Controller +{ + /** + * @return JsonResponse + */ + public function post(): JsonResponse + { + + return response()->json([]); + + } + + +} diff --git a/app/Helpers/Collector/GroupCollector.php b/app/Helpers/Collector/GroupCollector.php index 8ad641805b..eee489d612 100644 --- a/app/Helpers/Collector/GroupCollector.php +++ b/app/Helpers/Collector/GroupCollector.php @@ -871,7 +871,7 @@ class GroupCollector implements GroupCollectorInterface public function setLimit(int $limit): GroupCollectorInterface { $this->limit = $limit; - app('log')->debug(sprintf('GroupCollector: The limit is now %d', $limit)); + //app('log')->debug(sprintf('GroupCollector: The limit is now %d', $limit)); return $this; } @@ -976,7 +976,7 @@ class GroupCollector implements GroupCollectorInterface { $page = 0 === $page ? 1 : $page; $this->page = $page; - app('log')->debug(sprintf('GroupCollector: page is now %d', $page)); + //app('log')->debug(sprintf('GroupCollector: page is now %d', $page)); return $this; } diff --git a/resources/assets/v2/api/v2/model/transaction/post.js b/resources/assets/v2/api/v2/model/transaction/post.js new file mode 100644 index 0000000000..0b02577566 --- /dev/null +++ b/resources/assets/v2/api/v2/model/transaction/post.js @@ -0,0 +1,28 @@ +/* + * post.js + * Copyright (c) 2023 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 . + */ + +import {api} from "../../../../boot/axios"; + +export default class Post { + post(submission) { + let url = '/api/v2/transactions'; + return api.post(url, submission); + } +} diff --git a/resources/assets/v2/boot/bootstrap.js b/resources/assets/v2/boot/bootstrap.js index 984bb10e9e..a9da211fd2 100644 --- a/resources/assets/v2/boot/bootstrap.js +++ b/resources/assets/v2/boot/bootstrap.js @@ -29,39 +29,53 @@ import axios from 'axios'; import store from "store"; import observePlugin from 'store/plugins/observe'; import Alpine from "alpinejs"; -import * as bootstrap from 'bootstrap' +import * as bootstrap from 'bootstrap'; +import {getFreshVariable} from "../store/get-fresh-variable.js"; store.addPlugin(observePlugin); -window.store = store; + // import even more import {getVariable} from "../store/get-variable.js"; import {getViewRange} from "../support/get-viewrange.js"; -// wait for 3 promises, because we need those later on. window.bootstrapped = false; -Promise.all([ - getVariable('viewRange'), - getVariable('darkMode'), - getVariable('locale'), - getVariable('language'), -]).then((values) => { - if (!store.get('start') || !store.get('end')) { - // calculate new start and end, and store them. - const range = getViewRange(values[0], new Date); - store.set('start', range.start); - store.set('end', range.end); - } +window.store = store; - // save local in window.__ something - window.__localeId__ = values[2]; - store.set('language', values[3]); - store.set('locale', values[3]); - const event = new Event('firefly-iii-bootstrapped'); - document.dispatchEvent(event); - window.bootstrapped = true; +// always grab the preference "marker" from Firefly III. +getFreshVariable('lastActivity').then((serverValue) => { + const localValue = store.get('lastActivity'); + store.set('cacheValid', localValue === serverValue); + store.set('lastActivity', serverValue); + console.log('Server value: ' + serverValue); + console.log('Local value: ' + localValue); + console.log('Cache valid: ' + (localValue === serverValue)); +}).then(() => { + Promise.all([ + getVariable('viewRange'), + getVariable('darkMode'), + getVariable('locale'), + getVariable('language'), + ]).then((values) => { + if (!store.get('start') || !store.get('end')) { + // calculate new start and end, and store them. + const range = getViewRange(values[0], new Date); + store.set('start', range.start); + store.set('end', range.end); + } + + // save local in window.__ something + window.__localeId__ = values[2]; + store.set('language', values[3]); + store.set('locale', values[3]); + + const event = new Event('firefly-iii-bootstrapped'); + document.dispatchEvent(event); + window.bootstrapped = true; + }); }); +// wait for 3 promises, because we need those later on. window.axios = axios; window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/resources/assets/v2/pages/dashboard/accounts.js b/resources/assets/v2/pages/dashboard/accounts.js index 520da9d979..9f2f94db0a 100644 --- a/resources/assets/v2/pages/dashboard/accounts.js +++ b/resources/assets/v2/pages/dashboard/accounts.js @@ -33,6 +33,8 @@ let chart = null; let chartData = null; let afterPromises = false; +const CHART_CACHE_KEY = 'dashboard-accounts-chart'; +const ACCOUNTS_CACHE_KEY = 'dashboard-accounts-data'; export default () => ({ loading: false, loadingAccounts: false, @@ -44,12 +46,28 @@ export default () => ({ setVariable('autoConversion', this.autoConversion); }, getFreshData() { - const dashboard = new Dashboard(); - dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => { - this.chartData = response.data; - this.drawChart(this.generateOptions(this.chartData)); + const cacheValid = window.store.get('cacheValid'); + let cachedData = window.store.get(CHART_CACHE_KEY); + + if (cacheValid && typeof cachedData !== 'undefined') { + let options = window.store.get(CHART_CACHE_KEY); + this.drawChart(options); this.loading = false; - }); + console.log('Chart from cache'); + } + if (!cacheValid || typeof cachedData === 'undefined') { + const dashboard = new Dashboard(); + dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => { + this.chartData = response.data; + // cache generated options: + let options = this.generateOptions(this.chartData); + window.store.set(CHART_CACHE_KEY, options); + this.drawChart(options); + this.loading = false; + console.log('Chart FRESH'); + }); + } + }, generateOptions(data) { currencies = []; @@ -145,6 +163,15 @@ export default () => ({ this.loadingAccounts = false; return; } + const cacheValid = window.store.get('cacheValid'); + let cachedData = window.store.get(ACCOUNTS_CACHE_KEY); + + if (cacheValid && typeof cachedData !== 'undefined') { + this.accountList = cachedData; + this.loadingAccounts = false; + return; + } + // console.log('loadAccounts continue!'); const max = 10; let totalAccounts = 0; @@ -180,7 +207,10 @@ export default () => ({ group.transactions.push({ description: currentTransaction.description, id: current.id, + type: currentTransaction.type, + amount_raw: parseFloat(currentTransaction.amount), amount: formatMoney(currentTransaction.amount, currentTransaction.currency_code), + native_amount_raw: parseFloat(currentTransaction.native_amount), native_amount: formatMoney(currentTransaction.native_amount, currentTransaction.native_code), }); } @@ -190,7 +220,9 @@ export default () => ({ accounts.push({ name: parent.attributes.name, id: parent.id, + balance_raw: parseFloat(parent.attributes.current_balance), balance: formatMoney(parent.attributes.current_balance, parent.attributes.currency_code), + native_balance_raw: parseFloat(parent.attributes.native_current_balance), native_balance: formatMoney(parent.attributes.native_current_balance, parent.attributes.native_code), groups: groups, }); @@ -198,6 +230,7 @@ export default () => ({ if (count === totalAccounts) { this.accountList = accounts; this.loadingAccounts = false; + window.store.set(ACCOUNTS_CACHE_KEY, accounts); } }); }); diff --git a/resources/assets/v2/pages/transactions/create.js b/resources/assets/v2/pages/transactions/create.js index 622b041334..5dd61c9560 100644 --- a/resources/assets/v2/pages/transactions/create.js +++ b/resources/assets/v2/pages/transactions/create.js @@ -21,16 +21,66 @@ import '../../boot/bootstrap.js'; import dates from '../../pages/shared/dates.js'; import {createEmptySplit} 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"; let transactions = function () { return { count: 0, totalAmount: 0, entries: [], + + // error and success messages: + showError: false, + showSuccess: false, + init() { - this.entries.push(createEmptySplit()); + const opts = { + onSelectItem: console.log, + }; + let src = []; + for (let i = 0; i < 50; i++) { + src.push({ + title: "Option " + i, + id: "opt" + i, + data: { + key: i, + }, + }); + } + + // for each thing, make autocomplete? + + + this.addSplit(); console.log('Ik ben init hoera'); + + // let element = document.getElementById('source_0'); + // new Autocomplete(element, { + // items: src, + // valueField: "id", + // labelField: "title", + // highlightTyped: true, + // onSelectItem: console.log, + // }); + }, + submitTransaction() { + let transactions = parseFromEntries(this.entries); + let submission = { + group_title: null, + fire_webhooks: false, + apply_rules: false, + transactions: transactions + }; + let poster = new Post(); + console.log(submission); + poster.post(submission).then((response) => { + console.log(response); + }).catch((error) => { + console.error(error); + }); }, addSplit() { this.entries.push(createEmptySplit()); diff --git a/resources/assets/v2/pages/transactions/shared/create-empty-split.js b/resources/assets/v2/pages/transactions/shared/create-empty-split.js index 3947f396e5..809e35ef7c 100644 --- a/resources/assets/v2/pages/transactions/shared/create-empty-split.js +++ b/resources/assets/v2/pages/transactions/shared/create-empty-split.js @@ -19,6 +19,8 @@ */ +import format from "date-fns/format"; + function getAccount() { return { id: '', @@ -27,10 +29,13 @@ function getAccount() { } export function createEmptySplit() { + let now = new Date(); + let formatted = format(now, 'yyyy-MM-dd HH:mm'); return { - description: 'I am descr', + description: 'OK then', amount: '', source_account: getAccount(), destination_account: getAccount(), + date: formatted }; } diff --git a/resources/assets/v2/pages/transactions/shared/parse-from-entries.js b/resources/assets/v2/pages/transactions/shared/parse-from-entries.js new file mode 100644 index 0000000000..f561023bc4 --- /dev/null +++ b/resources/assets/v2/pages/transactions/shared/parse-from-entries.js @@ -0,0 +1,47 @@ +/* + * parse-from-entries.js + * Copyright (c) 2023 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 . + */ + +/** + * + * @param entries + */ +export function parseFromEntries(entries) { + let returnArray = []; + for (let i in entries) { + if (entries.hasOwnProperty(i)) { + const entry = entries[i]; + let current = {}; + + // fields for transaction + current.description = entry.description; + current.source_name = entry.source_account.name; + current.destination_name = entry.destination_account.name; + current.amount = entry.amount; + current.date = entry.date; + + // TODO transaction type is hard coded: + current.type = 'withdrawal'; + + + returnArray.push(current); + } + } + return returnArray; +} diff --git a/resources/assets/v2/store/get-fresh-variable.js b/resources/assets/v2/store/get-fresh-variable.js new file mode 100644 index 0000000000..9f425d5711 --- /dev/null +++ b/resources/assets/v2/store/get-fresh-variable.js @@ -0,0 +1,42 @@ +/* + * get-variable.js + * Copyright (c) 2023 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 . + */ + +import Get from "../api/v1/preferences/index.js"; +import Post from "../api/v1/preferences/post.js"; + +export function getFreshVariable(name, defaultValue = null) { + let getter = (new Get); + return getter.getByName(name).then((response) => { + // console.log('Get from API'); + return Promise.resolve(parseResponse(name, response)); + }).catch(() => { + // preference does not exist (yet). + // POST it and then return it anyway. + let poster = (new Post); + poster.post(name, defaultValue).then((response) => { + return Promise.resolve(parseResponse(name, response)); + }); + }); +} + +function parseResponse(name, response) { + return response.data.data.attributes.data; +} + diff --git a/resources/assets/v2/store/get-variable.js b/resources/assets/v2/store/get-variable.js index 2a59337e0b..e9ba25a721 100644 --- a/resources/assets/v2/store/get-variable.js +++ b/resources/assets/v2/store/get-variable.js @@ -23,27 +23,30 @@ import Post from "../api/v1/preferences/post.js"; export function getVariable(name, defaultValue = null) { + const validCache = window.store.get('cacheValid'); // currently unused, window.X can be used by the blade template // to make things available quicker than if the store has to grab it through the API. // then again, it's not that slow. - if (window.hasOwnProperty(name)) { + if (validCache && window.hasOwnProperty(name)) { // console.log('Get from window'); return Promise.resolve(window[name]); } // load from store2, if it's present. - if (window.store.get(name)) { - // console.log('Get from store'); - return Promise.resolve(window.store.get(name)); + const fromStore = window.store.get(name); + if (validCache && typeof fromStore !== 'undefined') { + // console.log('Get "' + name + '" from store'); + return Promise.resolve(fromStore); } let getter = (new Get); return getter.getByName(name).then((response) => { - // console.log('Get from API'); + // console.log('Get "' + name + '" from API'); return Promise.resolve(parseResponse(name, response)); }).catch(() => { // preference does not exist (yet). // POST it and then return it anyway. let poster = (new Post); poster.post(name, defaultValue).then((response) => { + return Promise.resolve(parseResponse(name, response)); }); }); @@ -52,7 +55,7 @@ export function getVariable(name, defaultValue = null) { function parseResponse(name, response) { let value = response.data.data.attributes.data; window.store.set(name, value); - // console.log('Store from API'); + // console.log('Store "' + name + '" in localStorage'); return value; } diff --git a/resources/views/v2/index.blade.php b/resources/views/v2/index.blade.php index 1b24580a9d..b958a9394e 100644 --- a/resources/views/v2/index.blade.php +++ b/resources/views/v2/index.blade.php @@ -88,12 +88,9 @@ - ( - ) + + @include('partials.elements.amount', ['autoConversion' => true,'amount' => 'account.balance','native' => 'account.native_balance']) +
@@ -106,7 +103,9 @@ @@ -119,7 +118,22 @@ diff --git a/resources/views/v2/partials/elements/amount.blade.php b/resources/views/v2/partials/elements/amount.blade.php new file mode 100644 index 0000000000..995f530a60 --- /dev/null +++ b/resources/views/v2/partials/elements/amount.blade.php @@ -0,0 +1,41 @@ +@if($autoConversion) + + +@else + + +@endif diff --git a/routes/api.php b/routes/api.php index c77a37d9af..ac0bdbe35a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -153,6 +153,20 @@ Route::group( } ); +/** + * V2 API route for transactions + */ +Route::group( + [ + 'namespace' => 'FireflyIII\Api\V2\Controllers\Model\Transaction', + 'prefix' => 'v2/transactions', + 'as' => 'api.v2.transactions.', + ], + static function () { + Route::post('', ['uses' => 'StoreController@post', 'as' => 'store']); + } +); + /** * V2 API route for budgets and budget limits: */