Other minification.

This commit is contained in:
James Cole 2024-01-02 15:41:14 +01:00
parent 97b65ac44c
commit 786b4c18a1
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
35 changed files with 1256 additions and 180 deletions

View File

@ -0,0 +1,83 @@
<?php
/*
* CategoryController.php
* 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/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V2\Controllers\Autocomplete;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Autocomplete\AutocompleteRequest;
use FireflyIII\Models\Category;
use FireflyIII\Repositories\UserGroups\Category\CategoryRepositoryInterface;
use Illuminate\Http\JsonResponse;
/**
* Class CategoryController
*/
class CategoryController extends Controller
{
private CategoryRepositoryInterface $repository;
/**
* AccountController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->repository = app(CategoryRepositoryInterface::class);
$userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) {
$this->repository->setUserGroup($userGroup);
}
return $next($request);
}
);
}
/**
* Documentation for this endpoint:
* TODO list of checks
* 1. use dates from ParameterBag
* 2. Request validates dates
* 3. Request includes user_group_id
* 4. Endpoint is documented.
* 5. Collector uses user_group_id
*/
public function categories(AutocompleteRequest $request): JsonResponse
{
$data = $request->getData();
$result = $this->repository->searchCategory($data['query'], $this->parameters->get('limit'));
$filtered = $result->map(
static function (Category $item) {
return [
'id' => (string) $item->id,
'name' => $item->name,
];
}
);
return response()->json($filtered);
}
}

View File

@ -0,0 +1,87 @@
<?php
/*
* CategoryController.php
* 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/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V2\Controllers\Autocomplete;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Autocomplete\AutocompleteRequest;
use FireflyIII\Models\Category;
use FireflyIII\Models\Tag;
use FireflyIII\Repositories\UserGroups\Tag\TagRepositoryInterface;
use FireflyIII\Repositories\UserGroups\Category\CategoryRepositoryInterface;
use Illuminate\Http\JsonResponse;
/**
* Class TagController
*/
class TagController extends Controller
{
private TagRepositoryInterface $repository;
/**
* AccountController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->repository = app(TagRepositoryInterface::class);
$userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) {
$this->repository->setUserGroup($userGroup);
}
return $next($request);
}
);
}
/**
* Documentation for this endpoint:
* TODO list of checks
* 1. use dates from ParameterBag
* 2. Request validates dates
* 3. Request includes user_group_id
* 4. Endpoint is documented.
* 5. Collector uses user_group_id
*/
public function tags(AutocompleteRequest $request): JsonResponse
{
$data = $request->getData();
$result = $this->repository->searchTag($data['query'], $this->parameters->get('limit'));
$filtered = $result->map(
static function (Tag $item) {
return [
'id' => (string) $item->id,
'name' => $item->tag,
'value' => (string) $item->id,
'label' => $item->tag,
];
}
);
return response()->json($filtered);
}
}

View File

@ -29,6 +29,8 @@ use FireflyIII\Repositories\Category\NoCategoryRepository;
use FireflyIII\Repositories\Category\NoCategoryRepositoryInterface;
use FireflyIII\Repositories\Category\OperationsRepository;
use FireflyIII\Repositories\Category\OperationsRepositoryInterface;
use FireflyIII\Repositories\UserGroups\Category\CategoryRepository as UserGroupCategoryRepository;
use FireflyIII\Repositories\UserGroups\Category\CategoryRepositoryInterface as UserGroupCategoryRepositoryInterface;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
@ -61,6 +63,20 @@ class CategoryServiceProvider extends ServiceProvider
}
);
// phpstan does not understand reference to 'auth'.
$this->app->bind(
UserGroupCategoryRepositoryInterface::class,
static function (Application $app) {
/** @var UserGroupCategoryRepository $repository */
$repository = app(UserGroupCategoryRepository::class);
if ($app->auth->check()) { // @phpstan-ignore-line
$repository->setUser(auth()->user());
}
return $repository;
}
);
$this->app->bind(
OperationsRepositoryInterface::class,
static function (Application $app) {

View File

@ -27,6 +27,8 @@ use FireflyIII\Repositories\Tag\OperationsRepository;
use FireflyIII\Repositories\Tag\OperationsRepositoryInterface;
use FireflyIII\Repositories\Tag\TagRepository;
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Repositories\UserGroups\Tag\TagRepository as UserGroupTagRepository;
use FireflyIII\Repositories\UserGroups\Tag\TagRepositoryInterface as UserGroupTagRepositoryInterface;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
@ -59,6 +61,20 @@ class TagServiceProvider extends ServiceProvider
}
);
$this->app->bind(
UserGroupTagRepositoryInterface::class,
static function (Application $app) {
/** @var UserGroupTagRepository $repository */
$repository = app(UserGroupTagRepository::class);
if ($app->auth->check()) { // @phpstan-ignore-line (phpstan does not understand the reference to auth)
$repository->setUser(auth()->user());
}
return $repository;
}
);
$this->app->bind(
OperationsRepositoryInterface::class,
static function (Application $app) {

View File

@ -0,0 +1,45 @@
<?php
/*
* CategoryRepository.php
* 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/>.
*/
declare(strict_types=1);
namespace FireflyIII\Repositories\UserGroups\Category;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
use Illuminate\Support\Collection;
class CategoryRepository implements CategoryRepositoryInterface
{
use UserGroupTrait;
/**
* @inheritDoc
*/
public function searchCategory(string $query, int $limit): Collection
{
$search = $this->userGroup->categories();
if ('' !== $query) {
$search->where('name', 'LIKE', sprintf('%%%s%%', $query));
}
return $search->take($limit)->get();
}
}

View File

@ -0,0 +1,39 @@
<?php
/*
* CategoryRepositoryInterface.php
* 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/>.
*/
declare(strict_types=1);
namespace FireflyIII\Repositories\UserGroups\Category;
use Illuminate\Support\Collection;
interface CategoryRepositoryInterface
{
/**
* Search for a category using wild cards. Uses the database, so case sensitive.
*
* @param string $query
* @param int $limit
*
* @return Collection
*/
public function searchCategory(string $query, int $limit): Collection;
}

View File

@ -0,0 +1,48 @@
<?php
/*
* TagRepository.php
* 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/>.
*/
declare(strict_types=1);
namespace FireflyIII\Repositories\UserGroups\Tag;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
use Illuminate\Support\Collection;
/**
* Class TagRepository
*/
class TagRepository implements TagRepositoryInterface
{
use UserGroupTrait;
/**
* @inheritDoc
*/
public function searchTag(string $query): Collection
{
$query = sprintf('%%%s%%', $query);
$search = $this->user->tags();
if ('' !== $query) {
$search->where('tag', 'LIKE', $query);
}
return $search->get(['tags.*']);
}
}

View File

@ -0,0 +1,35 @@
<?php
/*
* TagRepositoryInterface.php
* 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/>.
*/
declare(strict_types=1);
namespace FireflyIII\Repositories\UserGroups\Tag;
use Illuminate\Support\Collection;
interface TagRepositoryInterface
{
/**
* Find one or more tags based on the query.
*/
public function searchTag(string $query): Collection;
}

View File

@ -106,9 +106,9 @@ class PiggyBankTransformer extends AbstractTransformer
$id = (int)$entry->object_group_id;
$order = $entry->order;
$this->groups[$piggyBankId] = [
'object_group_id' => $id,
'object_group_id' => (string) $id,
'object_group_title' => $entry->title,
'object_group_order' => $order,
'object_group_order' => (int) $order,
];
}

12
package-lock.json generated
View File

@ -10,6 +10,7 @@
"alpinejs": "^3.13.3",
"bootstrap": "^5.3.0",
"bootstrap5-autocomplete": "^1.1.22",
"bootstrap5-tags": "^1.6.15",
"chart.js": "^4.4.0",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-chart-sankey": "^0.12.0",
@ -491,6 +492,11 @@
"resolved": "https://registry.npmjs.org/bootstrap5-autocomplete/-/bootstrap5-autocomplete-1.1.25.tgz",
"integrity": "sha512-6Z7vzlVBJduPUi7U1MPRBzoXmJf8ob9tGcUNxxos6qU1bbjPX7Li30r1Dhtk55hSBfPmVuN7p6zahF7G38xtWA=="
},
"node_modules/bootstrap5-tags": {
"version": "1.6.15",
"resolved": "https://registry.npmjs.org/bootstrap5-tags/-/bootstrap5-tags-1.6.15.tgz",
"integrity": "sha512-dQ1UpkP049OksrnwXr6g3JMz2S7+reAJnxxzCCf+f2F2/BtDtOHepQQjQ3QOPsFTYycS45p7wrWNM3bDnQMtYw=="
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
@ -638,9 +644,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"dev": true,
"funding": [
{

View File

@ -18,6 +18,7 @@
"alpinejs": "^3.13.3",
"bootstrap": "^5.3.0",
"bootstrap5-autocomplete": "^1.1.22",
"bootstrap5-tags": "^1.6.15",
"chart.js": "^4.4.0",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-chart-sankey": "^0.12.0",

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

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

@ -1,7 +1,14 @@
{
"_load-translations-36a0ce82.js": {
"file": "assets/load-translations-36a0ce82.js",
"integrity": "sha384-wTRO7jAxX9xxNgWUuXNrrSBjAyE0lWGOi3yVQ6S03bEYCk04Tc1C4BrVqHlnDSZL"
"_get-81296add.js": {
"file": "assets/get-81296add.js",
"imports": [
"_vendor-a1812e4c.js"
],
"integrity": "sha384-M+BAywPbHrOwlgtpdSO5UBNfYxe17K8369Ahh4qYQCxX5oZC2W1trl8zHRV9r2t4"
},
"_vendor-a1812e4c.js": {
"file": "assets/vendor-a1812e4c.js",
"integrity": "sha384-2dMX4JO7IblsxMPq8LWcAad57Ek9CjK147pQCo6WlNUHOu2+xfXTOhFKceSCNkDU"
},
"node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf": {
"file": "assets/fa-brands-400-5656d596.ttf",
@ -34,22 +41,24 @@
"integrity": "sha384-YxWlWCDksuL6Ljn1HkJNPH8l+jSRIWPpMpPw3pFa0QnmLXjwV/uPwpDm/b9vn/o1"
},
"resources/assets/v2/pages/dashboard/dashboard.js": {
"file": "assets/dashboard-9f02f998.js",
"file": "assets/dashboard-ada6c7cd.js",
"imports": [
"_load-translations-36a0ce82.js"
"_get-81296add.js",
"_vendor-a1812e4c.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/dashboard/dashboard.js",
"integrity": "sha384-mH/cboBLbKf0nKndnDaPzbF4ogflUacPt4Meytovf5d92Z6dytPdAXaUTPhWb89+"
"integrity": "sha384-OnpPqpRCVbvhxqcX6plPs/9kJW5KcvuxSwl/NlYRA4DvIyVdZmZIMEcv/sUlygZS"
},
"resources/assets/v2/pages/transactions/create.js": {
"file": "assets/create-8d2b7efc.js",
"file": "assets/create-f052c3bb.js",
"imports": [
"_load-translations-36a0ce82.js"
"_get-81296add.js",
"_vendor-a1812e4c.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/transactions/create.js",
"integrity": "sha384-AoSn0ASKb6wASHs6lp71iR/Siw9t2xU67Aos2Eq/osFPlEyJ58N7gHUoowy56/8z"
"integrity": "sha384-XdxhJ+VVga9b+RXvInTL51SXgzLh5aVvFOciuoP6M+LbyE7Ckhi7Vsc5hamU9yfS"
},
"resources/assets/v2/sass/app.scss": {
"file": "assets/app-fb7b26ec.css",

View File

@ -12500,6 +12500,12 @@ textarea.form-control-lg {
container: sidebar/inline-size;
}
.form-control {
-webkit-appearance: auto;
-moz-appearance: auto;
appearance: auto;
}
:root,
[data-bs-theme=light] {
--lte-sidebar-width: 250px;
@ -14291,7 +14297,7 @@ html.maximized-card {
}
.col-xl-3 .small-box h3, .col-xl-3 .small-box .h3, .col-lg-3 .small-box h3, .col-lg-3 .small-box .h3, .col-md-3 .small-box h3, .col-md-3 .small-box .h3 {
/* changed: font from 2.2rem to 1.5vw */
font-size: 1.5vw;
font-size: 1.5vwb;
}
}
.small-box p {

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

@ -1,6 +1,6 @@
/*!
* AdminLTE v4.0.0-alpha3 (https://adminlte.io)
* Copyright 2014-2023 Colorlib <https://colorlib.com>
* Copyright 2014-2024 Colorlib <https://colorlib.com>
* Licensed under MIT (https://github.com/ColorlibHQ/AdminLTE/blob/master/LICENSE)
*/
(function (global, factory) {

File diff suppressed because one or more lines are too long

View File

@ -288,7 +288,6 @@ export default {
if (e) {
e.preventDefault();
}
console.log('here we are');
let journalId = parseInt(prompt('Enter a transaction ID'));
if (journalId !== null && journalId > 0 && journalId <= 16777216) {
console.log('OK 1');

View File

@ -0,0 +1,33 @@
/*
* post.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 {api} from "../../../boot/axios";
export default class Post {
post(fileName, attachableType, attachableId) {
let url = '/api/v1/attachments';
return api.post(url, {filename: fileName, attachable_type: attachableType, attachable_id: attachableId});
}
upload(id, data) {
let url = './api/v1/attachments/' + id + '/upload';
return axios.post(url, data);
}
}

View File

@ -0,0 +1,35 @@
/*
* list.js
* Copyright (c) 2022 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 {api} from "../../../../boot/axios";
import format from "date-fns/format";
export default class Get {
/**
*
* @param params
* @returns {Promise<AxiosResponse<any>>}
*/
list(params) {
return api.get('/api/v2/budgets', {params: params});
}
}

View File

@ -188,7 +188,6 @@ export default () => ({
let start = new Date(window.store.get('start'));
let end = new Date(window.store.get('end'));
console.log('here we are');
const cacheValid = window.store.get('cacheValid');
let cachedData = window.store.get(getCacheKey('subscriptions-data-dashboard', start, end));

View File

@ -25,18 +25,141 @@ 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 Get from "../../api/v2/model/currency/get.js";
import BudgetGet from "../../api/v2/model/budget/get.js";
import PiggyBankGet from "../../api/v2/model/piggy-bank/get.js";
import SubscriptionGet from "../../api/v2/model/subscription/get.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";
let i18n;
const urls = {
description: '/api/v2/autocomplete/transaction-descriptions',
account: '/api/v2/autocomplete/accounts',
category: '/api/v2/autocomplete/categories',
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 {
count: 0,
@ -44,22 +167,34 @@ let transactions = function () {
transactionType: 'unknown',
showSuccessMessage: false,
showErrorMessage: false,
entries: [],
loadingCurrencies: true,
defaultCurrency: {},
entries: [], // loading things
loadingCurrencies: true,
loadingBudgets: true,
loadingPiggyBanks: true,
loadingSubscriptions: true,
// data sets
enabledCurrencies: [],
nativeCurrencies: [],
foreignCurrencies: [],
budgets: [],
piggyBanks: {},
subscriptions: [],
foreignAmountEnabled: true,
filters: {
source: [],
destination: [],
source: [], destination: [],
},
errorMessageText: '',
successMessageLink: '#',
successMessageText: '',
successMessageText: '', // error and success messages:
showError: false,
showSuccess: false,
showWaitMessage: false,
// four buttons
returnHereButton: false,
returnHereButton: true,
resetButton: false,
resetButtonEnabled: false,
rulesButton: true,
@ -68,6 +203,10 @@ let transactions = function () {
// state of the form
submitting: false,
// used to display the success message
newGroupTitle: '',
newGroupId: 0,
detectTransactionType() {
const sourceType = this.entries[0].source_account.type ?? 'unknown';
@ -85,7 +224,8 @@ let transactions = function () {
// this also locks the amount into the amount of the source account
// and the foreign amount (if different) in that of the destination account.
console.log('filter down currencies for transfer.');
this.filterNativeCurrencies(this.entries[0].source_account.currency_code);
this.filterForeignCurrencies(this.entries[0].destination_account.currency_code);
return;
}
// withdrawals:
@ -124,17 +264,47 @@ let transactions = 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].$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();
},
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];
if (current.code === code) {
currency = current;
}
}
}
list.push(currency);
this.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;
}
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 also forces the currency_code on ALL entries.
for (let i in this.entries) {
if (this.entries.hasOwnProperty(i)) {
this.entries[i].foreign_currency_code = code;
}
}
},
filterNativeCurrencies(code) {
console.log('filterNativeCurrencies("' + code + '")');
let list = [];
@ -169,22 +339,28 @@ let transactions = function () {
},
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,
};
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();
},
loadCurrencies() {
this.enabledCurrencies = [];
this.nativeCurrencies = [];
this.foreignCurrencies = [];
this.foreignCurrencies.push({
id: 0, name: '(no foreign currency)', code: '', default: false, symbol: '', decimal_places: 2,
});
console.log('Loading user currencies.');
let params = {
page: 1,
limit: 1337
page: 1, limit: 1337
};
let getter = new Get();
getter.list({}).then((response) => {
@ -208,11 +384,123 @@ let transactions = function () {
}
this.enabledCurrencies.push(obj);
this.nativeCurrencies.push(obj);
this.foreignCurrencies.push(obj);
}
}
}
this.loadingCurrencies = false;
console.log(this.enabledCurrencies);
});
},
loadBudgets() {
this.budgets = [];
this.budgets.push({
id: 0, name: '(no budget)',
});
console.log('Loading user budgets.');
let params = {
page: 1, limit: 1337
};
let getter = new BudgetGet();
getter.list({}).then((response) => {
for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) {
let current = response.data.data[i];
let obj = {
id: current.id, name: current.attributes.name,
};
this.budgets.push(obj);
}
}
this.loadingBudgets = false;
console.log(this.budgets);
});
},
loadPiggyBanks() {
this.piggyBanks = {};
let tempObject = {
'0': {
id: 0, name: '(no group)', order: 0, piggyBanks: [{
id: 0, name: '(no piggy bank)', order: 0,
}]
}
};
console.log('Loading user piggy banks.');
let params = {
page: 1, limit: 1337
};
let getter = new PiggyBankGet();
getter.list({}).then((response) => {
for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) {
let current = response.data.data[i];
let objectGroupId = current.attributes.object_group_id ?? '0';
let objectGroupTitle = current.attributes.object_group_title ?? '(no group)';
let piggyBank = {
id: current.id, name: current.attributes.name, order: current.attributes.order,
};
if (!tempObject.hasOwnProperty(objectGroupId)) {
tempObject[objectGroupId] = {
id: objectGroupId,
name: objectGroupTitle,
order: current.attributes.object_group_order ?? 0,
piggyBanks: []
};
}
tempObject[objectGroupId].piggyBanks.push(piggyBank);
tempObject[objectGroupId].piggyBanks.sort((a, b) => a.order - b.order);
}
}
//tempObject.sort((a,b) => a.order - b.order);
this.loadingPiggyBanks = false;
this.piggyBanks = Object.keys(tempObject).sort().reduce((obj, key) => {
obj[key] = tempObject[key];
return obj;
}, {});
});
},
loadSubscriptions() {
this.subscriptions = {};
let tempObject = {
'0': {
id: 0, name: '(no group)', order: 0, subscriptions: [{
id: 0, name: '(no subscription)', order: 0,
}]
}
};
console.log('Loading user suscriptions.');
let params = {
page: 1, limit: 1337
};
let getter = new SubscriptionGet();
getter.list({}).then((response) => {
for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) {
let current = response.data.data[i];
let objectGroupId = current.attributes.object_group_id ?? '0';
let objectGroupTitle = current.attributes.object_group_title ?? '(no group)';
let piggyBank = {
id: current.id, name: current.attributes.name, order: current.attributes.order,
};
if (!tempObject.hasOwnProperty(objectGroupId)) {
tempObject[objectGroupId] = {
id: objectGroupId,
name: objectGroupTitle,
order: current.attributes.object_group_order ?? 0,
subscriptions: []
};
}
tempObject[objectGroupId].subscriptions.push(piggyBank);
tempObject[objectGroupId].subscriptions.sort((a, b) => a.order - b.order);
}
}
//tempObject.sort((a,b) => a.order - b.order);
this.loadingSubscriptions = false;
this.subscriptions = Object.keys(tempObject).sort().reduce((obj, key) => {
obj[key] = tempObject[key];
return obj;
}, {});
});
},
changeSourceAccount(item, ac) {
@ -225,11 +513,9 @@ let transactions = function () {
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,
};
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();
@ -246,20 +532,32 @@ let transactions = function () {
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,
};
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;
},
// error and success messages:
showError: false,
showSuccess: false,
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');
@ -285,6 +583,20 @@ let transactions = function () {
}
});
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,
});
Autocomplete.init("input.ac-dest", {
server: urls.account,
serverParams: {
@ -316,11 +628,17 @@ let transactions = function () {
valueField: "id",
labelField: "description",
highlightTyped: true,
onSelectItem: console.log,
onSelectItem: this.changeDescription,
onChange: this.changeDescription,
});
},
processUpload(event) {
console.log('I am ALSO event listener for upload-success!');
console.log(event);
this.showBarOrRedirect();
},
init() {
Promise.all([getVariable('language', 'en_US')]).then((values) => {
@ -333,6 +651,14 @@ let transactions = function () {
});
this.loadCurrencies();
this.loadBudgets();
this.loadPiggyBanks();
this.loadSubscriptions();
document.addEventListener('upload-success', (event) => {
this.processUpload(event);
});
// source can never be expense account
this.filters.source = ['Asset account', 'Loan', 'Debt', 'Mortgage', 'Revenue account'];
@ -340,54 +666,72 @@ 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;
this.detectTransactionType();
// parse transaction:
let transactions = parseFromEntries(this.entries, this.transactionType);
let submission = {
// todo process all options
group_title: null,
fire_webhooks: false,
apply_rules: false,
transactions: transactions
group_title: null, fire_webhooks: false, apply_rules: false, transactions: transactions
};
if (transactions.length > 1) {
// todo improve me
submission.group_title = transactions[0].description;
}
// submit the transaction. Multi-stage process thing going on here!
let poster = new Post();
console.log(submission);
poster.post(submission).then((response) => {
this.submitting = false;
console.log(response);
const id = parseInt(response.data.data.id);
if (this.returnHereButton) {
// todo create success banner
this.showSuccessMessage = true;
this.successMessageLink = 'transactions/show/' + id;
this.successMessageText = i18n.t('firefly.stored_journal_js', {description: submission.group_title ?? submission.transactions[0].description});
// todo clear out form if necessary
if(this.resetButton) {
this.entries = [];
this.addSplit();
this.totalAmount = 0;
}
}
if (!this.returnHereButton) {
window.location = 'transactions/show/' + id + '?transaction_group_id=' + id + '&message=created';
// 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);
// upload transactions? then just show the wait message and do nothing else.
if (attachmentCount > 0) {
this.showWaitMessage = true;
return;
}
// if not, respond to user options:
this.showBarOrRedirect();
}).catch((error) => {
this.submitting = false;
console.log(error);
// todo put errors in form
this.parseErrors(error.response.data);
if (typeof error.response !== 'undefined') {
this.parseErrors(error.response.data);
}
});
},
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) {
this.entries = [];
this.addSplit();
this.totalAmount = 0;
}
}
if (!this.returnHereButton) {
window.location = 'transactions/show/' + this.newGroupId + '?transaction_group_id=' + this.newGroupId + '&message=created';
}
},
parseErrors(data) {
this.setDefaultErrors();
this.showErrorMessage = true;
@ -421,27 +765,22 @@ let transactions = function () {
break;
case 'source_name':
case 'source_id':
this.entries[transactionIndex].errors.source_account =
this.entries[transactionIndex].errors.source_account.concat(data.errors[key]);
this.entries[transactionIndex].errors.source_account = 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]);
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]);
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));
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));
}
}
}
@ -452,6 +791,22 @@ let transactions = function () {
},
addSplit() {
this.entries.push(createEmptySplit());
setTimeout(() => {
Tags.init('select.ac-tags', {
allowClear: true,
server: urls.tag,
liveServer: true,
clearEnd: true,
notFoundMessage: '(nothing found)',
noCache: true,
fetchOptions: {
headers: {
'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content
}
}
});
}, 250);
},
removeSplit(index) {
this.entries.splice(index, 1);
@ -476,6 +831,15 @@ 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

@ -38,16 +38,27 @@ export function createEmptySplit() {
// amount information:
amount: '',
currency_code: 'EUR',
foreign_amount: '',
foreign_currency_code: '',
// source and destination
source_account: getAccount(),
destination_account: getAccount(),
// meta data information:
budget_id: null,
category_name: '',
piggy_bank_id: null,
// date and time
date: formatted,
errors: {
'amount': [],
'foreign_amount': [],
'budget_id': [],
'category_name': [],
'piggy_bank_id': [],
},
};
}

View File

@ -31,12 +31,36 @@ export function parseFromEntries(entries, transactionType) {
// fields for transaction
current.description = entry.description;
// source and destination
current.source_name = entry.source_account.name;
current.destination_name = entry.destination_account.name;
// amount information:
current.amount = entry.amount;
current.date = entry.date;
current.currency_code = entry.currency_code;
// dates
current.date = entry.date;
// meta
current.budget_id = entry.budget_id;
current.category_name = entry.category_name;
current.piggy_bank_id = entry.piggy_bank_id;
// 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;
if(typeof entry.foreign_amount !== 'undefined' && '' !== entry.foreign_amount.toString()) {
current.foreign_amount = entry.foreign_amount;
}
if(typeof entry.foreign_amount === 'undefined' || '' === entry.foreign_amount.toString()) {
delete current.foreign_amount;
delete current.foreign_currency_code;
}
}
// if ID is set:
if (typeof entry.source_account.id !== 'undefined' && '' !== entry.source_account.id.toString()) {
current.source_id = entry.source_account.id;

View File

@ -15,7 +15,14 @@
</div>
</template>
<template x-if="showErrorMessage">
<div class="alert alert-danger alert-dismissible fade show" role="alert" x-text="errorMessageText">
<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>
</div>
</template>
<template x-if="showWaitMessage">
<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>
</div>
</template>
@ -56,12 +63,12 @@
<div class="tab-content" id="splitTabsContent">
<template x-for="transaction,index in entries">
<div
:class="{'tab-pane fade pt-2':true, 'show active': index ===0}"
:id="'split-'+index+'-pane'"
role="tabpanel"
:aria-labelledby="'split-'+index+'-tab'"
tabindex="0"
x-init="addedSplit()"
:class="{'tab-pane fade pt-2':true, 'show active': index ===0}"
:id="'split-'+index+'-pane'"
role="tabpanel"
:aria-labelledby="'split-'+index+'-tab'"
tabindex="0"
x-init="addedSplit()"
>
<div class="row mb-2">
<div class="col-xl-6 col-lg-6 col-md-12 col-xs-12 mb-2">
@ -77,19 +84,21 @@
class="col-sm-1 col-form-label d-none d-sm-block">
<em
title="TODO explain me"
class="fa-solid fa-font"></em>
class="fa-solid fa-font"></em>
</label>
<div class="col-sm-10">
<input type="text" class="form-control ac-description"
:id="'description_' + index"
@change="detectTransactionType"
x-model="transaction.description"
:data-index="index"
placeholder="{{ __('firefly.description') }}">
</div>
</div>
<div class="row mb-3">
<label :for="'source_' + index" class="col-sm-1 col-form-label d-none d-sm-block">
<label :for="'source_' + index"
class="col-sm-1 col-form-label d-none d-sm-block">
<i class="fa-solid fa-arrow-right"></i>
</label>
<div class="col-sm-10">
@ -103,7 +112,8 @@
</div>
<div class="row mb-3">
<label :for ="'dest_' + index" class="col-sm-1 col-form-label d-none d-sm-block">
<label :for="'dest_' + index"
class="col-sm-1 col-form-label d-none d-sm-block">
<i class="fa-solid fa-arrow-left"></i>
</label>
<div class="col-sm-10">
@ -140,16 +150,19 @@
<div class="row mb-3">
<div class="col-sm-3">
<template x-if="loadingCurrencies">
<span class="form-control-plaintext"><em class="fa-solid fa-spinner fa-spin"></em></span>
<span class="form-control-plaintext"><em
class="fa-solid fa-spinner fa-spin"></em></span>
</template>
<template x-if="!loadingCurrencies">
<select class="form-control" :id="'currency_code_' + index"
x-model="transaction.currency_code"
>
<template x-for="currency in nativeCurrencies">
<option :selected="currency.id == defaultCurrency.id" :label="currency.name" :value="currency.code" x-text="currency.name"></option>
</template>
</select>
<select class="form-control" :id="'currency_code_' + index"
x-model="transaction.currency_code"
>
<template x-for="currency in nativeCurrencies">
<option :selected="currency.id == defaultCurrency.id"
:label="currency.name" :value="currency.code"
x-text="currency.name"></option>
</template>
</select>
</template>
</div>
<div class="col-sm-9">
@ -161,13 +174,54 @@
@change="changedAmount"
placeholder="0.00">
<template x-if="transaction.errors.amount.length > 0">
<div class="invalid-feedback" x-text="transaction.errors.amount[0]"></div>
<div class="invalid-feedback"
x-text="transaction.errors.amount[0]"></div>
</template>
</div>
</div>
</div>
<div class="card-footer">
amount card
<template x-if="foreignAmountEnabled">
<div class="row mb-3">
<div class="col-sm-3">
<label class="form-label">&nbsp;</label>
<template x-if="loadingCurrencies">
<span class="form-control-plaintext"><em
class="fa-solid fa-spinner fa-spin"></em></span>
</template>
<template x-if="!loadingCurrencies">
<select class="form-control"
:id="'foreign_currency_code_' + index"
x-model="transaction.foreign_currency_code"
>
<template x-for="currency in foreignCurrencies">
<option :label="currency.name" :value="currency.code"
x-text="currency.name"></option>
</template>
</select>
</template>
</div>
<div class="col-sm-9">
<template x-if="transactionType != 'transfer'">
<label class="small form-label">Amount in foreign amount, if
any</label>
</template>
<template x-if="transactionType == 'transfer'">
<label class="small form-label">Amount in currency of destination
account</label>
</template>
<input type="number" step="any" min="0"
:id="'amount_' + index"
:data-index="index"
:class="{'is-invalid': transaction.errors.foreign_amount.length > 0, 'input-mask' : true, 'form-control': true}"
x-model="transaction.foreign_amount" data-inputmask="currency"
@change="changedAmount"
placeholder="0.00">
<template x-if="transaction.errors.foreign_amount.length > 0">
<div class="invalid-feedback"
x-text="transaction.errors.foreign_amount[0]"></div>
</template>
</div>
</div>
</template>
</div>
</div>
</div>
@ -179,10 +233,129 @@
</h3>
</div>
<div class="card-body">
important meta info card
</div>
<div class="card-footer">
important meta info card
<template x-if="transactionType != 'deposit' && transactionType != 'transfer'">
<div class="row mb-3">
<label :for="'budget_id_' + index"
class="col-sm-1 col-form-label d-none d-sm-block">
<i class="fa-solid fa-chart-pie"></i>
</label>
<div class="col-sm-10">
<template x-if="loadingBudgets">
<span class="form-control-plaintext"><em
class="fa-solid fa-spinner fa-spin"></em></span>
</template>
<template x-if="!loadingBudgets">
<select class="form-control"
:id="'budget_id_' + index"
x-model="transaction.budget_id"
>
<template x-for="budget in budgets">
<option :label="budget.name" :value="budget.id"
x-text="budget.name"></option>
</template>
</select>
</template>
</div>
</div>
</template>
<div class="row mb-3">
<label :for="'category_name_' + index"
class="col-sm-1 col-form-label d-none d-sm-block">
<i class="fa-solid fa-bookmark"></i>
</label>
<div class="col-sm-10">
<input type="search"
class="form-control ac-category"
:id="'category_name_' + index"
x-model="transaction.category_name"
:data-index="index"
placeholder="{{ __('firefly.category') }}">
</div>
</div>
<template x-if="transactionType != 'deposit' && transactionType != 'withdrawal'">
<div class="row mb-3">
<label :for="'piggy_bank_id_' + index"
class="col-sm-1 col-form-label d-none d-sm-block">
<i class="fa-solid fa-piggy-bank"></i>
</label>
<div class="col-sm-10">
<template x-if="loadingPiggyBanks">
<span class="form-control-plaintext"><em
class="fa-solid fa-spinner fa-spin"></em></span>
</template>
<template x-if="!loadingPiggyBanks">
<select class="form-control"
:id="'piggy_bank_id_' + index"
x-model="transaction.piggy_bank_id">
<template x-for="group in piggyBanks">
<optgroup :label="group.name">
<template x-for="piggyBank in group.piggyBanks">
<option :label="piggyBank.name"
:value="piggyBank.id"
x-text="piggyBank.name"></option>
</template>
</optgroup>
</template>
</select>
</template>
</div>
</div>
</template>
<template x-if="transactionType != 'transfer' && transactionType != 'deposit'">
<div class="row mb-3">
<label :for="'bill_id_' + index"
class="col-sm-1 col-form-label d-none d-sm-block">
<i class="fa-solid fa-calendar"></i>
</label>
<div class="col-sm-10">
<template x-if="loadingSubscriptions">
<span class="form-control-plaintext"><em
class="fa-solid fa-spinner fa-spin"></em></span>
</template>
<template x-if="!loadingSubscriptions">
<select class="form-control"
:id="'bill_id_' + index"
x-model="transaction.bill_id">
<template x-for="group in subscriptions">
<optgroup :label="group.name">
<template x-for="subscription in group.subscriptions">
<option :label="subscription.name"
:value="subscription.id"
x-text="subscription.name"></option>
</template>
</optgroup>
</template>
</select>
</template>
</div>
</div>
</template>
<div class="row mb-3">
<label :for="'tags_' + index"
class="col-sm-1 col-form-label d-none d-sm-block">
<i class="fa-solid fa-tag"></i>
</label>
<div class="col-sm-10">
<select
class="form-select ac-tags"
:id="'tags_' + index"
:name="'tags['+index+'][]'"
multiple>
<option value="">Type a tag...</option>
</select>
</div>
</div>
<div class="row mb-3">
<label :for="'notes_' + index"
class="col-sm-1 col-form-label d-none d-sm-block">
<i class="fa-solid fa-font"></i>
</label>
<div class="col-sm-10">
<textarea class="form-control" :id="'notes_' + index"
x-model="transaction.notes"
placeholder="{{ __('firefly.notes') }}"></textarea>
</div>
</div>
</div>
</div>
@ -195,7 +368,32 @@
</h3>
</div>
<div class="card-body">
Less important meta
<div class="row mb-3">
<label :for="'attachments_' + index"
class="col-sm-1 col-form-label d-none d-sm-block">
<i class="fa-solid fa-file-import"></i>
</label>
<div class="col-sm-10">
<input type="file" multiple
class="form-control attachments"
:id="'attachments_' + index"
:data-index="index"
name="attachments[]"
placeholder="{{ __('firefly.category') }}">
</div>
</div>
Attachments<br>
Internal ref<br>
External URL<br>
Location<br>
Links?<br>
Date 1<br>
Date 2<br>
Date 3<br>
Date 4<br>
Date 5<br>
Date 6<br>
</div>
<div class="card-footer">
Less important meta
@ -214,28 +412,32 @@
</div>
<div class="card-body">
<div class="form-check">
<input class="form-check-input" x-model="returnHereButton" type="checkbox" id="returnButton">
<input class="form-check-input" x-model="returnHereButton" type="checkbox"
id="returnButton">
<label class="form-check-label" for="returnButton">
Return here to create a new transaction
</label>
</div>
<div class="form-check">
<input class="form-check-input" x-model="resetButton" type="checkbox" id="resetButton" :disabled="!returnHereButton">
<input class="form-check-input" x-model="resetButton" type="checkbox"
id="resetButton" :disabled="!returnHereButton">
<label class="form-check-label" for="resetButton">
Reset the form after returning
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="rulesButton" :checked="rulesButton">
<input class="form-check-input" type="checkbox" id="rulesButton"
:checked="rulesButton">
<label class="form-check-label" for="rulesButton">
Run rules on this transaction
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="webhookButton" :checked="webhookButton">
<input class="form-check-input" type="checkbox" id="webhookButton"
:checked="webhookButton">
<label class="form-check-label" for="webhookButton">
Run webhooks on this transaction
</label>
@ -248,7 +450,8 @@
</div>
<div class="col-12">
<template x-if="0 !== index">
<button :disabled="submitting" class="btn btn-danger" @click="removeSplit(index)">Remove this split
<button :disabled="submitting" class="btn btn-danger" @click="removeSplit(index)">
Remove this split
</button>
</template>
<button class="btn btn-info" :disabled="submitting">Add another split</button>

View File

@ -64,6 +64,8 @@ Route::group(
// Auto complete routes
Route::get('accounts', ['uses' => 'AccountController@accounts', 'as' => 'accounts']);
Route::get('transaction-descriptions', ['uses' => 'TransactionController@transactionDescriptions', 'as' => 'transaction-descriptions']);
Route::get('categories', ['uses' => 'CategoryController@categories', 'as' => 'categories']);
Route::get('tags', ['uses' => 'TagController@tags', 'as' => 'tags']);
}
);

View File

@ -24,7 +24,20 @@ import manifestSRI from 'vite-plugin-manifest-sri';
const host = '127.0.0.1';
function manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor';
}
};
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks,
},
}
},
plugins: [
laravel({
input: [
@ -35,6 +48,7 @@ export default defineConfig({
refresh: true,
}),
manifestSRI(),
],