Merge remote-tracking branch 'upstream/develop' into feat/expression-engine

This commit is contained in:
Michael Thomas 2024-03-07 13:09:43 -05:00
commit 41fc1e8f82
79 changed files with 1463 additions and 1777 deletions

2
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,2 @@
# code owners for this Firefly III related repository
* @JC5 @SDx3

View File

@ -0,0 +1,101 @@
<?php
/*
* IndexController.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\Model\Account;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Model\Account\IndexRequest;
use FireflyIII\Api\V2\Request\Model\Transaction\InfiniteListRequest;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
use FireflyIII\Transformers\V2\AccountTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
class IndexController extends Controller
{
public const string RESOURCE_KEY = 'accounts';
private AccountRepositoryInterface $repository;
/**
* AccountController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class);
// new way of user group validation
$userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) {
$this->repository->setUserGroup($userGroup);
}
return $next($request);
}
);
}
/**
* TODO see autocomplete/accountcontroller for list.
*/
public function index(IndexRequest $request): JsonResponse
{
$this->repository->resetAccountOrder();
$types = $request->getAccountTypes();
$accounts = $this->repository->getAccountsByType($types);
$pageSize = $this->parameters->get('limit');
$count = $accounts->count();
$accounts = $accounts->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
$paginator = new LengthAwarePaginator($accounts, $count, $pageSize, $this->parameters->get('page'));
$transformer = new AccountTransformer();
$transformer->setParameters($this->parameters); // give params to transformer
return response()
->json($this->jsonApiList('accounts', $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE)
;
}
public function infiniteList(InfiniteListRequest $request): JsonResponse
{
$this->repository->resetAccountOrder();
// get accounts of the specified type, and return.
$types = $request->getAccountTypes();
// get from repository
$accounts = $this->repository->getAccountsInOrder($types, $request->getSortInstructions('accounts'), $request->getStartRow(), $request->getEndRow());
$total = $this->repository->countAccounts($types);
$count = $request->getEndRow() - $request->getStartRow();
$paginator = new LengthAwarePaginator($accounts, $total, $count, $this->parameters->get('page'));
$transformer = new AccountTransformer();
$transformer->setParameters($this->parameters); // give params to transformer
return response()
->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE)
;
}
}

View File

@ -35,6 +35,47 @@ use Illuminate\Http\JsonResponse;
*/
class TransactionController extends Controller
{
public function infiniteList(InfiniteListRequest $request): JsonResponse
{
// get sort instructions
$instructions = $request->getSortInstructions('transactions');
// collect transactions:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setUserGroup(auth()->user()->userGroup)
->withAPIInformation()
->setStartRow($request->getStartRow())
->setEndRow($request->getEndRow())
->setTypes($request->getTransactionTypes())
->setSorting($instructions)
;
$start = $this->parameters->get('start');
$end = $this->parameters->get('end');
if (null !== $start) {
$collector->setStart($start);
}
if (null !== $end) {
$collector->setEnd($end);
}
$paginator = $collector->getPaginatedGroups();
$params = $request->buildParams();
$paginator->setPath(
sprintf(
'%s?%s',
route('api.v2.infinite.transactions.list'),
$params
)
);
return response()
->json($this->jsonApiList('transactions', $paginator, new TransactionGroupTransformer()))
->header('Content-Type', self::CONTENT_TYPE)
;
}
public function list(ListRequest $request): JsonResponse
{
// collect transactions:
@ -75,45 +116,4 @@ class TransactionController extends Controller
->header('Content-Type', self::CONTENT_TYPE)
;
}
public function infiniteList(InfiniteListRequest $request): JsonResponse
{
// get sort instructions
$instructions = $request->getSortInstructions();
// collect transactions:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setUserGroup(auth()->user()->userGroup)
->withAPIInformation()
->setStartRow($request->getStartRow())
->setEndRow($request->getEndRow())
->setTypes($request->getTransactionTypes())
->setSorting($instructions)
;
$start = $this->parameters->get('start');
$end = $this->parameters->get('end');
if (null !== $start) {
$collector->setStart($start);
}
if (null !== $end) {
$collector->setEnd($end);
}
$paginator = $collector->getPaginatedGroups();
$params = $request->buildParams();
$paginator->setPath(
sprintf(
'%s?%s',
route('api.v2.infinite.transactions.list'),
$params
)
);
return response()
->json($this->jsonApiList('transactions', $paginator, new TransactionGroupTransformer()))
->header('Content-Type', self::CONTENT_TYPE)
;
}
}

View File

@ -0,0 +1,67 @@
<?php
/*
* IndexRequest.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\Request\Model\Account;
use Carbon\Carbon;
use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest;
/**
* Class IndexRequest
*
* Lots of code stolen from the SingleDateRequest.
*/
class IndexRequest extends FormRequest
{
use AccountFilter;
use ChecksLogin;
use ConvertsDataTypes;
/**
* Get all data from the request.
*/
public function getDate(): Carbon
{
return $this->getCarbonDate('date');
}
public function getAccountTypes(): array
{
$type = (string)$this->get('type', 'default');
return $this->mapAccountTypes($type);
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
return [
'date' => 'date|after:1900-01-01|before:2099-12-31',
];
}
}

View File

@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V2\Request\Model\Transaction;
use Carbon\Carbon;
use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\Support\Http\Api\TransactionFilter;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
@ -36,6 +37,7 @@ use Illuminate\Foundation\Http\FormRequest;
*/
class InfiniteListRequest extends FormRequest
{
use AccountFilter;
use ChecksLogin;
use ConvertsDataTypes;
use TransactionFilter;
@ -81,6 +83,13 @@ class InfiniteListRequest extends FormRequest
return $this->getCarbonDate('end');
}
public function getAccountTypes(): array
{
$type = (string)$this->get('type', 'default');
return $this->mapAccountTypes($type);
}
public function getPage(): int
{
$page = $this->convertInteger('page');
@ -88,9 +97,9 @@ class InfiniteListRequest extends FormRequest
return 0 === $page || $page > 65536 ? 1 : $page;
}
public function getSortInstructions(): array
public function getSortInstructions(string $key): array
{
$allowed = config('firefly.sorting.allowed.transactions');
$allowed = config(sprintf('firefly.sorting.allowed.%s', $key));
$set = $this->get('sorting', []);
$result = [];
if (0 === count($set)) {

View File

@ -117,6 +117,7 @@ class GracefulNotFoundHandler extends ExceptionHandler
return redirect(route('tags.index'));
case 'categories.show':
case 'categories.edit':
case 'categories.show.all':
$request->session()->reflash();

View File

@ -72,31 +72,6 @@ class StandardMessageGenerator implements MessageGeneratorInterface
$this->run();
}
public function getVersion(): int
{
return $this->version;
}
public function setObjects(Collection $objects): void
{
$this->objects = $objects;
}
public function setTrigger(int $trigger): void
{
$this->trigger = $trigger;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setWebhooks(Collection $webhooks): void
{
$this->webhooks = $webhooks;
}
private function getWebhooks(): Collection
{
return $this->user->webhooks()->where('active', true)->where('trigger', $this->trigger)->get(['webhooks.*']);
@ -206,6 +181,11 @@ class StandardMessageGenerator implements MessageGeneratorInterface
$this->storeMessage($webhook, $basicMessage);
}
public function getVersion(): int
{
return $this->version;
}
private function collectAccounts(TransactionGroup $transactionGroup): Collection
{
$accounts = new Collection();
@ -232,4 +212,24 @@ class StandardMessageGenerator implements MessageGeneratorInterface
$webhookMessage->save();
app('log')->debug(sprintf('Stored new webhook message #%d', $webhookMessage->id));
}
public function setObjects(Collection $objects): void
{
$this->objects = $objects;
}
public function setTrigger(int $trigger): void
{
$this->trigger = $trigger;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setWebhooks(Collection $webhooks): void
{
$this->webhooks = $webhooks;
}
}

View File

@ -33,10 +33,11 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
*/
trait CollectorProperties
{
public const string TEST = 'Test';
/** @var array<int, string> */
public array $sorting;
public const string TEST = 'Test';
private ?int $endRow;
private ?int $endRow;
private bool $expandGroupSearch;
private array $fields;
private bool $hasAccountInfo;
@ -52,7 +53,7 @@ trait CollectorProperties
private ?int $page;
private array $postFilters;
private HasMany $query;
private ?int $startRow;
private ?int $startRow;
private array $stringFields;
/*
* This array is used to collect ALL tags the user may search for (using 'setTags').

View File

@ -25,6 +25,7 @@ namespace FireflyIII\Helpers\Collector;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\Extensions\AccountCollection;
use FireflyIII\Helpers\Collector\Extensions\AmountCollection;
@ -782,6 +783,35 @@ class GroupCollector implements GroupCollectorInterface
return $currentCollection;
}
#[\Override]
public function sortCollection(Collection $collection): Collection
{
/**
* @var string $field
* @var string $direction
*/
foreach ($this->sorting as $field => $direction) {
$func = 'ASC' === $direction ? 'sortBy' : 'sortByDesc';
$collection = $collection->{$func}(function (array $product, int $key) use ($field) { // @phpstan-ignore-line
// depends on $field:
if ('description' === $field) {
if (1 === count($product['transactions'])) {
return array_values($product['transactions'])[0][$field];
}
if (count($product['transactions']) > 1) {
return $product['title'];
}
return 'zzz';
}
exit('here we are');
});
}
return $collection;
}
/**
* Same as getGroups but everything is in a paginator.
*/
@ -792,6 +822,7 @@ class GroupCollector implements GroupCollectorInterface
$this->setLimit(50);
}
if (null !== $this->startRow && null !== $this->endRow) {
/** @var int $total */
$total = $this->endRow - $this->startRow;
return new LengthAwarePaginator($set, $this->total, $total, 1);
@ -931,6 +962,14 @@ class GroupCollector implements GroupCollectorInterface
return $this;
}
#[\Override]
public function setSorting(array $instructions): GroupCollectorInterface
{
$this->sorting = $instructions;
return $this;
}
public function setStartRow(int $startRow): self
{
$this->startRow = $startRow;
@ -1091,41 +1130,4 @@ class GroupCollector implements GroupCollectorInterface
return $this;
}
#[\Override]
public function sortCollection(Collection $collection): Collection
{
/**
* @var string $field
* @var string $direction
*/
foreach ($this->sorting as $field => $direction) {
$func = 'ASC' === $direction ? 'sortBy' : 'sortByDesc';
$collection = $collection->{$func}(function (array $product, int $key) use ($field) { // @phpstan-ignore-line
// depends on $field:
if ('description' === $field) {
if (1 === count($product['transactions'])) {
return array_values($product['transactions'])[0][$field];
}
if (count($product['transactions']) > 1) {
return $product['title'];
}
return 'zzz';
}
exit('here we are');
});
}
return $collection;
}
#[\Override]
public function setSorting(array $instructions): GroupCollectorInterface
{
$this->sorting = $instructions;
return $this;
}
}

View File

@ -285,13 +285,6 @@ interface GroupCollectorInterface
*/
public function getPaginatedGroups(): LengthAwarePaginator;
public function setSorting(array $instructions): self;
/**
* Sort the collection on a column.
*/
public function sortCollection(Collection $collection): Collection;
public function hasAnyTag(): self;
/**
@ -560,6 +553,8 @@ interface GroupCollectorInterface
public function setSepaCT(string $sepaCT): self;
public function setSorting(array $instructions): self;
/**
* Set source accounts.
*/
@ -620,6 +615,11 @@ interface GroupCollectorInterface
*/
public function setXorAccounts(Collection $accounts): self;
/**
* Sort the collection on a column.
*/
public function sortCollection(Collection $collection): Collection;
/**
* Automatically include all stuff required to make API calls work.
*/

View File

@ -51,7 +51,7 @@ class NetWorth implements NetWorthInterface
private CurrencyRepositoryInterface $currencyRepos;
private User $user;
private ?UserGroup $userGroup;
private ?UserGroup $userGroup;
/**
* This method collects the user's net worth in ALL the user's currencies

View File

@ -69,6 +69,11 @@ abstract class Controller extends BaseController
$authGuard = config('firefly.authentication_guard');
$logoutUrl = config('firefly.custom_logout_url');
// overrule v2 layout back to v1.
if ('true' === request()->get('force_default_layout') && 'v2' === config('firefly.layout')) {
app('view')->getFinder()->setPaths([realpath(base_path('resources/views'))]); // @phpstan-ignore-line
}
app('view')->share('authGuard', $authGuard);
app('view')->share('logoutUrl', $logoutUrl);

View File

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use Carbon\Carbon;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Middleware\IsDemoUser;
use FireflyIII\Models\AccountType;

View File

@ -34,6 +34,7 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class ReconcileController
@ -73,7 +74,6 @@ class ReconcileController extends Controller
$accountCurrency = $this->accountRepos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency();
$amount = '0';
$clearedAmount = '0';
$route = '';
if (null === $start && null === $end) {
throw new FireflyException('Invalid dates submitted.');
@ -103,14 +103,11 @@ class ReconcileController extends Controller
$clearedJournals = $collector->getExtractedJournals();
}
app('log')->debug('Start transaction loop');
/** @var array $journal */
foreach ($journals as $journal) {
$amount = $this->processJournal($account, $accountCurrency, $journal, $amount);
}
app('log')->debug(sprintf('Final amount is %s', $amount));
app('log')->debug('End transaction loop');
/** @var array $journal */
foreach ($clearedJournals as $journal) {
@ -118,31 +115,17 @@ class ReconcileController extends Controller
$clearedAmount = $this->processJournal($account, $accountCurrency, $journal, $clearedAmount);
}
}
Log::debug(sprintf('Start balance: "%s"', $startBalance));
Log::debug(sprintf('End balance: "%s"', $endBalance));
Log::debug(sprintf('Cleared amount: "%s"', $clearedAmount));
Log::debug(sprintf('Amount: "%s"', $amount));
$difference = bcadd(bcadd(bcsub($startBalance, $endBalance), $clearedAmount), $amount);
$diffCompare = bccomp($difference, '0');
$countCleared = count($clearedJournals);
$reconSum = bcadd(bcadd($startBalance, $amount), $clearedAmount);
try {
$view = view(
'accounts.reconcile.overview',
compact(
'account',
'start',
'diffCompare',
'difference',
'end',
'clearedAmount',
'startBalance',
'endBalance',
'amount',
'route',
'countCleared',
'reconSum',
'selectedIds'
)
)->render();
$view = view('accounts.reconcile.overview', compact('account', 'start', 'diffCompare', 'difference', 'end', 'clearedAmount', 'startBalance', 'endBalance', 'amount', 'route', 'countCleared', 'reconSum', 'selectedIds'))->render();
} catch (\Throwable $e) {
app('log')->debug(sprintf('View error: %s', $e->getMessage()));
app('log')->error($e->getTraceAsString());
@ -151,14 +134,42 @@ class ReconcileController extends Controller
throw new FireflyException($view, 0, $e);
}
$return = [
'post_url' => $route,
'html' => $view,
];
$return = ['post_url' => $route, 'html' => $view];
return response()->json($return);
}
private function processJournal(Account $account, TransactionCurrency $currency, array $journal, string $amount): string
{
$toAdd = '0';
app('log')->debug(sprintf('User submitted %s #%d: "%s"', $journal['transaction_type_type'], $journal['transaction_journal_id'], $journal['description']));
// not much magic below we need to cover using tests.
if ($account->id === $journal['source_account_id']) {
if ($currency->id === $journal['currency_id']) {
$toAdd = $journal['amount'];
}
if (null !== $journal['foreign_currency_id'] && $journal['foreign_currency_id'] === $currency->id) {
$toAdd = $journal['foreign_amount'];
}
}
if ($account->id === $journal['destination_account_id']) {
if ($currency->id === $journal['currency_id']) {
$toAdd = bcmul($journal['amount'], '-1');
}
if (null !== $journal['foreign_currency_id'] && $journal['foreign_currency_id'] === $currency->id) {
$toAdd = bcmul($journal['foreign_amount'], '-1');
}
}
app('log')->debug(sprintf('Going to add %s to %s', $toAdd, $amount));
$amount = bcadd($amount, $toAdd);
app('log')->debug(sprintf('Result is %s', $amount));
return $amount;
}
/**
* Returns a list of transactions in a modal.
*
@ -176,6 +187,7 @@ class ReconcileController extends Controller
}
$startDate = clone $start;
$startDate->subDay();
$end->endOfDay();
$currency = $this->accountRepos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrency();
$startBalance = app('steam')->bcround(app('steam')->balance($account, $startDate), $currency->decimal_places);
@ -214,37 +226,6 @@ class ReconcileController extends Controller
return response()->json(['html' => $html, 'startBalance' => $startBalance, 'endBalance' => $endBalance]);
}
private function processJournal(Account $account, TransactionCurrency $currency, array $journal, string $amount): string
{
$toAdd = '0';
app('log')->debug(sprintf('User submitted %s #%d: "%s"', $journal['transaction_type_type'], $journal['transaction_journal_id'], $journal['description']));
// not much magic below we need to cover using tests.
if ($account->id === $journal['source_account_id']) {
if ($currency->id === $journal['currency_id']) {
$toAdd = $journal['amount'];
}
if (null !== $journal['foreign_currency_id'] && $journal['foreign_currency_id'] === $currency->id) {
$toAdd = $journal['foreign_amount'];
}
}
if ($account->id === $journal['destination_account_id']) {
if ($currency->id === $journal['currency_id']) {
$toAdd = bcmul($journal['amount'], '-1');
}
if (null !== $journal['foreign_currency_id'] && $journal['foreign_currency_id'] === $currency->id) {
$toAdd = bcmul($journal['foreign_amount'], '-1');
}
}
app('log')->debug(sprintf('Going to add %s to %s', $toAdd, $amount));
$amount = bcadd($amount, $toAdd);
app('log')->debug(sprintf('Result is %s', $amount));
return $amount;
}
/**
* "fix" amounts to make it easier on the reconciliation overview:
*/

View File

@ -75,52 +75,45 @@ class IndexController extends Controller
$objectType = 'transfer';
}
// add a split for the (future) v2 release.
$periods = [];
$groups = [];
$subTitle = 'TODO page subtitle in v2';
$subTitleIcon = config('firefly.transactionIconsByType.'.$objectType);
$types = config('firefly.transactionTypesByType.'.$objectType);
$page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
$subTitleIcon = config('firefly.transactionIconsByType.'.$objectType);
$types = config('firefly.transactionTypesByType.'.$objectType);
$page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
if ('v2' !== (string)config('firefly.layout')) {
if (null === $start) {
$start = session('start');
$end = session('end');
}
if (null === $end) {
// get last transaction ever?
$last = $this->repository->getLast();
$end = null !== $last ? $last->date : session('end');
}
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
$startStr = $start->isoFormat($this->monthAndDayFormat);
$endStr = $end->isoFormat($this->monthAndDayFormat);
$subTitle = (string)trans(sprintf('firefly.title_%s_between', $objectType), ['start' => $startStr, 'end' => $endStr]);
$path = route('transactions.index', [$objectType, $start->format('Y-m-d'), $end->format('Y-m-d')]);
$firstJournal = $this->repository->firstNull();
$startPeriod = null === $firstJournal ? new Carbon() : $firstJournal->date;
$endPeriod = clone $end;
$periods = $this->getTransactionPeriodOverview($objectType, $startPeriod, $endPeriod);
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)
->setTypes($types)
->setLimit($pageSize)
->setPage($page)
->withBudgetInformation()
->withCategoryInformation()
->withAccountInformation()
->withAttachmentInformation()
;
$groups = $collector->getPaginatedGroups();
$groups->setPath($path);
if (null === $start) {
$start = session('start');
$end = session('end');
}
if (null === $end) {
// get last transaction ever?
$last = $this->repository->getLast();
$end = null !== $last ? $last->date : session('end');
}
[$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
$startStr = $start->isoFormat($this->monthAndDayFormat);
$endStr = $end->isoFormat($this->monthAndDayFormat);
$subTitle = (string)trans(sprintf('firefly.title_%s_between', $objectType), ['start' => $startStr, 'end' => $endStr]);
$path = route('transactions.index', [$objectType, $start->format('Y-m-d'), $end->format('Y-m-d')]);
$firstJournal = $this->repository->firstNull();
$startPeriod = null === $firstJournal ? new Carbon() : $firstJournal->date;
$endPeriod = clone $end;
$periods = $this->getTransactionPeriodOverview($objectType, $startPeriod, $endPeriod);
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)
->setTypes($types)
->setLimit($pageSize)
->setPage($page)
->withBudgetInformation()
->withCategoryInformation()
->withAccountInformation()
->withAttachmentInformation()
;
$groups = $collector->getPaginatedGroups();
$groups->setPath($path);
return view('transactions.index', compact('subTitle', 'objectType', 'subTitleIcon', 'groups', 'periods', 'start', 'end'));
}

View File

@ -202,7 +202,7 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface
$availableBudget->user()->associate($this->user);
$availableBudget->transactionCurrency()->associate($currency);
$availableBudget->start_date = $start->startOfDay()->format('Y-m-d'); // @phpstan-ignore-line
$availableBudget->end_date = $end->endOfDay()->format('Y-m-d'); // @phpstan-ignore-line
$availableBudget->end_date = $end->endOfDay()->format('Y-m-d'); // @phpstan-ignore-line
}
$availableBudget->amount = $amount;
$availableBudget->save();

View File

@ -375,6 +375,12 @@ class RecurringRepository implements RecurringRepositoryInterface
app('log')->debug('Now in getXOccurrencesSince()');
$skipMod = $repetition->repetition_skip + 1;
$occurrences = [];
// to fix #8616, take a few days from both dates, then filter the list to make sure no entries
// from today or before are saved.
$date->subDays(4);
$afterDate->subDays(4);
if ('daily' === $repetition->repetition_type) {
$occurrences = $this->getXDailyOccurrencesSince($date, $afterDate, $count, $skipMod);
}
@ -407,7 +413,7 @@ class RecurringRepository implements RecurringRepositoryInterface
}
$filtered = [];
foreach ($occurrences as $date) {
if ($date->lte($max)) {
if ($date->lte($max) && $date->gt(today())) {
$filtered[] = $date;
}
}

View File

@ -39,6 +39,17 @@ class AccountRepository implements AccountRepositoryInterface
{
use UserGroupTrait;
#[\Override]
public function countAccounts(array $types): int
{
$query = $this->userGroup->accounts();
if (0 !== count($types)) {
$query->accountTypeIn($types);
}
return $query->count();
}
public function findByAccountNumber(string $number, array $types): ?Account
{
$dbQuery = $this->userGroup
@ -161,6 +172,71 @@ class AccountRepository implements AccountRepositoryInterface
return $query->get(['accounts.*']);
}
#[\Override]
public function getAccountsInOrder(array $types, array $sort, int $startRow, int $endRow): Collection
{
$query = $this->userGroup->accounts();
if (0 !== count($types)) {
$query->accountTypeIn($types);
}
$query->skip($startRow);
$query->take($endRow - $startRow);
// add sort parameters. At this point they're filtered to allowed fields to sort by:
if (0 !== count($sort)) {
foreach ($sort as $label => $direction) {
$query->orderBy(sprintf('accounts.%s', $label), $direction);
}
}
if (0 === count($sort)) {
$query->orderBy('accounts.order', 'ASC');
$query->orderBy('accounts.active', 'DESC');
$query->orderBy('accounts.name', 'ASC');
}
return $query->get(['accounts.*']);
}
public function getActiveAccountsByType(array $types): Collection
{
$query = $this->userGroup->accounts();
if (0 !== count($types)) {
$query->accountTypeIn($types);
}
$query->where('active', true);
$query->orderBy('accounts.account_type_id', 'ASC');
$query->orderBy('accounts.order', 'ASC');
$query->orderBy('accounts.name', 'ASC');
return $query->get(['accounts.*']);
}
public function resetAccountOrder(): void
{
$sets = [
[AccountType::DEFAULT, AccountType::ASSET],
[AccountType::LOAN, AccountType::DEBT, AccountType::CREDITCARD, AccountType::MORTGAGE],
];
foreach ($sets as $set) {
$list = $this->getAccountsByType($set);
$index = 1;
foreach ($list as $account) {
if (false === $account->active) {
$account->order = 0;
continue;
}
if ($index !== (int)$account->order) {
app('log')->debug(sprintf('Account #%d ("%s"): order should %d be but is %d.', $account->id, $account->name, $index, $account->order));
$account->order = $index;
$account->save();
}
++$index;
}
}
}
public function getAccountsByType(array $types, ?array $sort = []): Collection
{
$res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types);
@ -187,20 +263,6 @@ class AccountRepository implements AccountRepositoryInterface
return $query->get(['accounts.*']);
}
public function getActiveAccountsByType(array $types): Collection
{
$query = $this->userGroup->accounts();
if (0 !== count($types)) {
$query->accountTypeIn($types);
}
$query->where('active', true);
$query->orderBy('accounts.account_type_id', 'ASC');
$query->orderBy('accounts.order', 'ASC');
$query->orderBy('accounts.name', 'ASC');
return $query->get(['accounts.*']);
}
public function searchAccount(string $query, array $types, int $limit): Collection
{
// search by group, not by user

View File

@ -35,6 +35,8 @@ use Illuminate\Support\Collection;
*/
interface AccountRepositoryInterface
{
public function countAccounts(array $types): int;
public function find(int $accountId): ?Account;
public function findByAccountNumber(string $number, array $types): ?Account;
@ -49,6 +51,11 @@ interface AccountRepositoryInterface
public function getAccountsByType(array $types, ?array $sort = []): Collection;
/**
* Used in the infinite accounts list.
*/
public function getAccountsInOrder(array $types, array $sort, int $startRow, int $endRow): Collection;
public function getActiveAccountsByType(array $types): Collection;
/**
@ -56,6 +63,11 @@ interface AccountRepositoryInterface
*/
public function getMetaValue(Account $account, string $field): ?string;
/**
* Reset order types of the mentioned accounts.
*/
public function resetAccountOrder(): void;
public function searchAccount(string $query, array $types, int $limit): Collection;
public function setUser(User $user): void;

View File

@ -39,7 +39,7 @@ class RemoteUserGuard implements Guard
{
protected Application $application;
protected UserProvider $provider;
protected ?User $user;
protected ?User $user;
/**
* Create a new authentication guard.

View File

@ -79,13 +79,14 @@ trait CalculateXOccurrencesSince
while ($total < $count) {
$domCorrected = min($dayOfMonth, $mutator->daysInMonth);
$mutator->day = $domCorrected;
app('log')->debug(sprintf('Mutator is now %s', $mutator->format('Y-m-d')));
if (0 === $attempts % $skipMod && $mutator->gte($afterDate)) {
app('log')->debug('Is added to the list.');
$return[] = clone $mutator;
++$total;
}
++$attempts;
$mutator = $mutator->endOfMonth()->addDay();
app('log')->debug(sprintf('Mutator is now %s', $mutator->format('Y-m-d')));
}
return $return;

View File

@ -72,16 +72,18 @@ class OperatorQuerySearch implements SearchInterface
private GroupCollectorInterface $collector;
private CurrencyRepositoryInterface $currencyRepository;
private array $excludeTags;
private array $includeAnyTags;
// added to fix #8632
private array $includeTags;
private array $invalidOperators;
private int $limit;
private Collection $operators;
private int $page;
private array $prohibitedWords;
private float $startTime;
private TagRepositoryInterface $tagRepository;
private array $validOperators;
private array $words;
private array $invalidOperators;
private int $limit;
private Collection $operators;
private int $page;
private array $prohibitedWords;
private float $startTime;
private TagRepositoryInterface $tagRepository;
private array $validOperators;
private array $words;
/**
* OperatorQuerySearch constructor.
@ -93,6 +95,7 @@ class OperatorQuerySearch implements SearchInterface
$this->page = 1;
$this->words = [];
$this->excludeTags = [];
$this->includeAnyTags = [];
$this->includeTags = [];
$this->prohibitedWords = [];
$this->invalidOperators = [];
@ -1112,8 +1115,9 @@ class OperatorQuerySearch implements SearchInterface
$this->collector->findNothing();
}
if ($tags->count() > 0) {
$ids = array_values($tags->pluck('id')->toArray());
$this->includeTags = array_unique(array_merge($this->includeTags, $ids));
// changed from includeTags to includeAnyTags for #8632
$ids = array_values($tags->pluck('id')->toArray());
$this->includeAnyTags = array_unique(array_merge($this->includeAnyTags, $ids));
}
break;
@ -1125,8 +1129,9 @@ class OperatorQuerySearch implements SearchInterface
$this->collector->findNothing();
}
if ($tags->count() > 0) {
$ids = array_values($tags->pluck('id')->toArray());
$this->includeTags = array_unique(array_merge($this->includeTags, $ids));
// changed from includeTags to includeAnyTags for #8632
$ids = array_values($tags->pluck('id')->toArray());
$this->includeAnyTags = array_unique(array_merge($this->includeAnyTags, $ids));
}
break;
@ -2725,6 +2730,19 @@ class OperatorQuerySearch implements SearchInterface
}
$this->collector->setAllTags($collection);
}
// if include ANY tags, include them: (see #8632)
if (count($this->includeAnyTags) > 0) {
app('log')->debug(sprintf('%d include ANY tag(s)', count($this->includeAnyTags)));
$collection = new Collection();
foreach ($this->includeAnyTags as $tagId) {
$tag = $this->tagRepository->find($tagId);
if (null !== $tag) {
app('log')->debug(sprintf('Include ANY tag "%s"', $tag->tag));
$collection->push($tag);
}
}
$this->collector->setTags($collection);
}
}
public function getWords(): array

View File

@ -114,7 +114,7 @@ class AccountTransformer extends AbstractTransformer
// no currency? use default
$currency = $this->default;
if (0 !== (int)$this->accountMeta[$id]['currency_id']) {
if (array_key_exists($id, $this->accountMeta) && 0 !== (int)$this->accountMeta[$id]['currency_id']) {
$currency = $this->currencies[(int)$this->accountMeta[$id]['currency_id']];
}
// amounts and calculation.

View File

@ -47,7 +47,7 @@ use Illuminate\Support\Facades\DB;
class TransactionGroupTransformer extends AbstractTransformer
{
private array $accountTypes = []; // account types collection.
private ExchangeRateConverter $converter; // collection of all journals and some important meta-data.
private ExchangeRateConverter $converter; // collection of all journals and some important meta-data.
private array $currencies = [];
private TransactionCurrency $default; // collection of all currencies for this transformer.
private array $journals = [];

View File

@ -112,7 +112,7 @@
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.9",
"barryvdh/laravel-ide-helper": "2.*",
"barryvdh/laravel-ide-helper": "3.*",
"ergebnis/phpstan-rules": "^2.1",
"fakerphp/faker": "1.*",
"filp/whoops": "2.*",

1797
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -115,9 +115,9 @@ return [
'handle_debts' => true,
// see cer.php for exchange rates feature flag.
],
'version' => '6.1.10',
'version' => 'develop/2024-03-07',
'api_version' => '2.0.12',
'db_version' => 22,
'db_version' => 23,
// generic settings
'maxUploadSize' => 1073741824, // 1 GB
@ -917,6 +917,7 @@ return [
'sorting' => [
'allowed' => [
'transactions' => ['description', 'amount'],
'accounts' => ['name'],
],
],
];

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\QueryException;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
private const string QUERY_ERROR = 'Could not execute query (table "%s", field "%s"): %s';
private const string EXPL = 'If the index already exists (see error), this is not an problem. Otherwise, please open a GitHub discussion.';
/**
* Run the migrations.
*/
public function up(): void
{
// add missing indices
$set = [
'account_meta' => ['account_id'],
'accounts' => ['user_id', 'user_group_id', 'account_type_id'],
'budgets' => ['user_id', 'user_group_id'],
'journal_meta' => ['transaction_journal_id', 'data', 'name'],
'category_transaction_journal' => ['transaction_journal_id'],
'categories' => ['user_id', 'user_group_id'],
'transaction_currencies' => ['code'],
'transaction_groups' => ['user_id', 'user_group_id'],
'transaction_journals' => ['user_id', 'user_group_id', 'date', 'transaction_group_id', 'transaction_type_id', 'transaction_currency_id', 'bill_id'],
'transactions' => ['account_id', 'transaction_journal_id', 'transaction_currency_id', 'foreign_currency_id'],
];
foreach ($set as $table => $fields) {
foreach ($fields as $field) {
try {
Schema::table(
$table,
static function (Blueprint $blueprint) use ($field): void {
$blueprint->index($field);
}
);
} catch (QueryException $e) {
app('log')->error(sprintf(self::QUERY_ERROR, $table, $field, $e->getMessage()));
app('log')->error(self::EXPL);
}
}
}
}
/**
* Reverse the migrations.
*/
public function down(): void {}
};

6
package-lock.json generated
View File

@ -460,9 +460,9 @@
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
},
"node_modules/alpinejs": {
"version": "3.13.5",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.5.tgz",
"integrity": "sha512-1d2XeNGN+Zn7j4mUAKXtAgdc4/rLeadyTMWeJGXF5DzwawPBxwTiBhFFm6w/Ei8eJxUZeyNWWSD9zknfdz1kEw==",
"version": "3.13.6",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.6.tgz",
"integrity": "sha512-/F7pVR+11r1A0KVw+eY1DcjTFlRQn9arD3p5/2Q4vq0N2WDC/dHpg+Pz7ZMiVQlHE7ZmZmcqTRm1wYTdDLMiEg==",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{f as n}from"./vendor-50e42c6c.js";function e(){return{id:"",name:"",alpine_name:""}}function o(){return{description:[],amount:[],currency_code:[],foreign_amount:[],foreign_currency_code:[],source_account:[],destination_account:[],budget_id:[],category_name:[],piggy_bank_id:[],bill_id:[],tags:[],notes:[],internal_reference:[],external_url:[],latitude:[],longitude:[],zoom_level:[],date:[],interest_date:[],book_date:[],process_date:[],due_date:[],payment_date:[],invoice_date:[]}}function d(){let t=n(new Date,"yyyy-MM-dd HH:mm");return{description:"",amount:"",currency_code:"EUR",foreign_amount:"",foreign_currency_code:"",source_account:e(),destination_account:e(),budget_id:null,category_name:"",piggy_bank_id:null,bill_id:null,tags:[],notes:"",internal_reference:"",external_url:"",hasLocation:!1,latitude:null,longitude:null,zoomLevel:null,date:t,interest_date:"",book_date:"",process_date:"",due_date:"",payment_date:"",invoice_date:"",errors:o()}}export{d as c,o as d};
import{f as n}from"./vendor-f5e18451.js";function e(){return{id:"",name:"",alpine_name:""}}function o(){return{description:[],amount:[],currency_code:[],foreign_amount:[],foreign_currency_code:[],source_account:[],destination_account:[],budget_id:[],category_name:[],piggy_bank_id:[],bill_id:[],tags:[],notes:[],internal_reference:[],external_url:[],latitude:[],longitude:[],zoom_level:[],date:[],interest_date:[],book_date:[],process_date:[],due_date:[],payment_date:[],invoice_date:[]}}function d(){let t=n(new Date,"yyyy-MM-dd HH:mm");return{description:"",amount:"",currency_code:"EUR",foreign_amount:"",foreign_currency_code:"",source_account:e(),destination_account:e(),budget_id:null,category_name:"",piggy_bank_id:null,bill_id:null,tags:[],notes:"",internal_reference:"",external_url:"",hasLocation:!1,latitude:null,longitude:null,zoomLevel:null,date:t,interest_date:"",book_date:"",process_date:"",due_date:"",payment_date:"",invoice_date:"",errors:o()}}export{d as c,o as d};

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 +1 @@
import{a as s}from"./format-money-2e5851ad.js";let t=class{list(a){return s.get("/api/v2/subscriptions",{params:a})}paid(a){return s.get("/api/v2/subscriptions/sum/paid",{params:a})}unpaid(a){return s.get("/api/v2/subscriptions/sum/unpaid",{params:a})}};class e{list(a){return s.get("/api/v2/piggy-banks",{params:a})}}export{t as G,e as a};
import{a as s}from"./format-money-00d5f4b9.js";let t=class{list(a){return s.get("/api/v2/subscriptions",{params:a})}paid(a){return s.get("/api/v2/subscriptions/sum/paid",{params:a})}unpaid(a){return s.get("/api/v2/subscriptions/sum/unpaid",{params:a})}};class e{list(a){return s.get("/api/v2/piggy-banks",{params:a})}}export{t as G,e as a};

View File

@ -1 +1 @@
import{a as t}from"./format-money-2e5851ad.js";class n{list(a){return t.get("/api/v2/transactions",{params:a})}infiniteList(a){return t.get("/api/v2/infinite/transactions",{params:a})}show(a,i){return t.get("/api/v2/transactions/"+a,{params:i})}}export{n as G};
import{a as t}from"./format-money-00d5f4b9.js";class n{list(a){return t.get("/api/v2/transactions",{params:a})}infiniteList(a){return t.get("/api/v2/infinite/transactions",{params:a})}show(a,i){return t.get("/api/v2/transactions/"+a,{params:i})}}export{n as G};

View File

@ -0,0 +1 @@
import{a as s}from"./format-money-00d5f4b9.js";import{f as n}from"./vendor-f5e18451.js";class c{show(a,t){return s.get("/api/v2/accounts/"+a,{params:t})}index(a){return s.get("/api/v2/accounts",{params:a})}transactions(a,t){const r={page:t.page??1};return t.hasOwnProperty("start")&&(r.start=n(t.start,"y-MM-dd")),t.hasOwnProperty("end")&&(r.end=n(t.end,"y-MM-dd")),s.get("/api/v2/accounts/"+a+"/transactions",{params:r})}}export{c as G};

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,f as d}from"./format-money-00d5f4b9.js";import{f as c,i as o}from"./vendor-f5e18451.js";/* empty css */import{G as f}from"./get-5b2cde80.js";const i=window.location.href.split("/"),u=i[i.length-1];let h=function(){return{notifications:{error:{show:!1,text:"",url:""},success:{show:!1,text:"",url:""},wait:{show:!1,text:""}},totalPages:1,page:1,tableColumns:{name:{enabled:!0}},accounts:[],formatMoney(t,a){return d(t,a)},format(t){return c(t,o.t("config.date_time_fns"))},init(){this.notifications.wait.show=!0,this.notifications.wait.text=o.t("firefly.wait_loading_data"),this.loadAccounts()},loadAccounts(){new f().index({type:u,page:this.page}).then(t=>{for(let a=0;a<t.data.data.length;a++)if(t.data.data.hasOwnProperty(a)){let e=t.data.data[a],r={id:parseInt(e.id),name:e.attributes.name};this.accounts.push(r)}this.notifications.wait.show=!1})}}},n={index:h,dates:l};function s(){Object.keys(n).forEach(t=>{console.log(`Loading page component "${t}"`);let a=n[t]();Alpine.data(t,()=>a)}),Alpine.start()}document.addEventListener("firefly-iii-bootstrapped",()=>{console.log("Loaded through event listener."),s()});window.bootstrapped&&(console.log("Loaded through window variable."),s());

View File

@ -1 +1 @@
import{c as o}from"./create-empty-split-81f71b2e.js";import{f as _}from"./vendor-50e42c6c.js";function l(a,r){let n=[];for(let i in a)if(a.hasOwnProperty(i)){let e=a[i],t=o();t.transaction_journal_id=e.transaction_journal_id,t.transaction_group_id=r,t.bill_id=e.bill_id,t.bill_name=e.bill_name,t.budget_id=e.budget_id,t.budget_name=e.budget_name,t.category_name=e.category_name,t.category_id=e.category_id,t.piggy_bank_id=e.piggy_bank_id,t.piggy_bank_name=e.piggy_bank_name,t.book_date=e.book_date,t.due_date=e.due_date,t.interest_date=e.interest_date,t.invoice_date=e.invoice_date,t.payment_date=e.payment_date,t.process_date=e.process_date,t.external_url=e.external_url,t.internal_reference=e.internal_reference,t.notes=e.notes,t.tags=e.tags,t.amount=parseFloat(e.amount).toFixed(e.currency_decimal_places),t.currency_code=e.currency_code,e.foreign_amount!==null&&(t.forein_currency_code=e.foreign_currency_code,t.foreign_amount=parseFloat(e.foreign_amount).toFixed(e.foreign_currency_decimal_places)),t.date=_(new Date(e.date),"yyyy-MM-dd HH:mm"),t.description=e.description,t.destination_account={id:e.destination_id,name:e.destination_name,type:e.destination_type,alpine_name:e.destination_name},t.source_account={id:e.source_id,name:e.source_name,type:e.source_type,alpine_name:e.source_name},e.latitude!==null&&(t.hasLocation=!0,t.latitude=e.latitude,t.longitude=e.longitude,t.zoomLevel=e.zoom_level),n.push(t)}return n}export{l as p};
import{c as o}from"./create-empty-split-c97f45c5.js";import{f as _}from"./vendor-f5e18451.js";function l(a,r){let n=[];for(let i in a)if(a.hasOwnProperty(i)){let e=a[i],t=o();t.transaction_journal_id=e.transaction_journal_id,t.transaction_group_id=r,t.bill_id=e.bill_id,t.bill_name=e.bill_name,t.budget_id=e.budget_id,t.budget_name=e.budget_name,t.category_name=e.category_name,t.category_id=e.category_id,t.piggy_bank_id=e.piggy_bank_id,t.piggy_bank_name=e.piggy_bank_name,t.book_date=e.book_date,t.due_date=e.due_date,t.interest_date=e.interest_date,t.invoice_date=e.invoice_date,t.payment_date=e.payment_date,t.process_date=e.process_date,t.external_url=e.external_url,t.internal_reference=e.internal_reference,t.notes=e.notes,t.tags=e.tags,t.amount=parseFloat(e.amount).toFixed(e.currency_decimal_places),t.currency_code=e.currency_code,e.foreign_amount!==null&&(t.forein_currency_code=e.foreign_currency_code,t.foreign_amount=parseFloat(e.foreign_amount).toFixed(e.foreign_currency_decimal_places)),t.date=_(new Date(e.date),"yyyy-MM-dd HH:mm"),t.description=e.description,t.destination_account={id:e.destination_id,name:e.destination_name,type:e.destination_type,alpine_name:e.destination_name},t.source_account={id:e.source_id,name:e.source_name,type:e.source_type,alpine_name:e.source_name},e.latitude!==null&&(t.hasLocation=!0,t.latitude=e.latitude,t.longitude=e.longitude,t.zoomLevel=e.zoom_level),n.push(t)}return n}export{l as p};

View File

@ -1 +1 @@
import{a as p}from"./format-money-2e5851ad.js";class u{put(t,a){let r="/api/v2/transactions/"+parseInt(a.id);return p.put(r,t)}}export{u as P};
import{a as p}from"./format-money-00d5f4b9.js";class u{put(t,a){let r="/api/v2/transactions/"+parseInt(a.id);return p.put(r,t)}}export{u as 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

File diff suppressed because one or more lines are too long

View File

@ -1,67 +1,80 @@
{
"_create-empty-split-81f71b2e.js": {
"file": "assets/create-empty-split-81f71b2e.js",
"_create-empty-split-c97f45c5.js": {
"file": "assets/create-empty-split-c97f45c5.js",
"imports": [
"_vendor-50e42c6c.js"
"_vendor-f5e18451.js"
],
"integrity": "sha384-iCRJ+fmYZVSXY4L5+Q8vhQKqcjSi6lnglG7yW8deRAjJwiVaANm0EzriQPOAgQMS"
"integrity": "sha384-wKgs+6aM6DzW9ZXDeQVeN/TZtIlkASgtnnFzSjaSdQ1oFOyhKBuZBr3j2nGoXWRI"
},
"_format-money-2e5851ad.js": {
"file": "assets/format-money-2e5851ad.js",
"_format-money-00d5f4b9.js": {
"file": "assets/format-money-00d5f4b9.js",
"imports": [
"_vendor-50e42c6c.js"
"_vendor-f5e18451.js"
],
"integrity": "sha384-kzF0fVkQWinHDsR1R190XjenILJ0VYvD2H1rQJW23dJvcA18qRrGRyb9q+3RADZU"
"integrity": "sha384-NwibVlqlZ8kjqF9U76HNU1p/xODJOtH4zTBi6i4OGG4x9PcqhTEbQtoXC4WnYE2u"
},
"_get-28b9aa25.js": {
"file": "assets/get-28b9aa25.js",
"_get-0097c30a.js": {
"file": "assets/get-0097c30a.js",
"imports": [
"_format-money-2e5851ad.js"
"_format-money-00d5f4b9.js"
],
"integrity": "sha384-pKOfaG0JCi2+ROk2Tt8poQvr+uMK7u4BUCUKRcvEU/bREr4YReZnqSNBWtvdP3dW"
"integrity": "sha384-FLOxTBwEkGf7PbJFJGi1kmcf110PY99rnGSOOmJSlnPd4fnsEZXYu4e7Q0gAxmC8"
},
"_get-e0a81c64.js": {
"file": "assets/get-e0a81c64.js",
"_get-59bb27e9.js": {
"file": "assets/get-59bb27e9.js",
"imports": [
"_format-money-2e5851ad.js"
"_format-money-00d5f4b9.js"
],
"integrity": "sha384-3OBZFmOXNyobpHvnLUfs5EdHG9fjwBAhQRszBlMtAO55MsE+OrB0/xnuNfMN+R7N"
"integrity": "sha384-bmtl6LrtsrNh0rSgcWohzOXmDozXSaQ9BLaXchj8IiJAeqULMGFOx/z6olr9Z6I9"
},
"_parse-downloaded-splits-83822d1b.js": {
"file": "assets/parse-downloaded-splits-83822d1b.js",
"_get-5b2cde80.js": {
"file": "assets/get-5b2cde80.js",
"imports": [
"_create-empty-split-81f71b2e.js",
"_vendor-50e42c6c.js"
"_format-money-00d5f4b9.js",
"_vendor-f5e18451.js"
],
"integrity": "sha384-dPB0/W3hHYE/2m/HPqibSEe2N2XD7wMTePDsRd6fv03fKwgFu6mwOjUx1t3q6Gn6"
"integrity": "sha384-Tr9M0BbSz6qynIKHsBlaiZlTWRqPh5K8oIoFNE9eqXvooEeAW2o0thWfuXBrV5s+"
},
"_put-bb9ad93a.js": {
"file": "assets/put-bb9ad93a.js",
"_parse-downloaded-splits-9e5b1aaf.js": {
"file": "assets/parse-downloaded-splits-9e5b1aaf.js",
"imports": [
"_format-money-2e5851ad.js"
"_create-empty-split-c97f45c5.js",
"_vendor-f5e18451.js"
],
"integrity": "sha384-jk8bB684G+XotWRFO6k57GF1QGy6BUgFBb55JPsvU3clT6DBNA42ELW8vPyX9HbB"
"integrity": "sha384-+u7ce40ZJSnmX7AM9MgiCCgIjXt+TkLmWC/p/A7bocN5A5HLNNEgM5UUkecfGDbU"
},
"_splice-errors-into-transactions-6727b386.js": {
"file": "assets/splice-errors-into-transactions-6727b386.js",
"_put-d6ed6223.js": {
"file": "assets/put-d6ed6223.js",
"imports": [
"_format-money-2e5851ad.js",
"_get-28b9aa25.js",
"_vendor-50e42c6c.js"
"_format-money-00d5f4b9.js"
],
"integrity": "sha384-x5WY5RRt7J6TLl+7nLZi1TBBKgUmjNokUh2ezaofCTQp0sEkHQrmJYAGUX/ZwPY4"
"integrity": "sha384-AqFeuR/08blTrxpgzmtTKao9mbSJhBT6N/0clyeAgTtSUyH4w0gKJ2MTvt8lOujk"
},
"_vendor-50e42c6c.js": {
"_splice-errors-into-transactions-bba1ef64.js": {
"file": "assets/splice-errors-into-transactions-bba1ef64.js",
"imports": [
"_format-money-00d5f4b9.js",
"_get-0097c30a.js",
"_vendor-f5e18451.js"
],
"integrity": "sha384-ld/QxFbIFbqpxeHPa3i4Rs58YFPmt265gYQThhJqTDxuXYD0vBS5fiMzKYccFI7g"
},
"_vendor-f5e18451.js": {
"assets": [
"assets/layers-1dbbe9d0.png",
"assets/layers-2x-066daca8.png",
"assets/marker-icon-574c3a5c.png"
],
"css": [
"assets/vendor-6fbf50c2.css"
"assets/vendor-52daf6b6.css"
],
"file": "assets/vendor-50e42c6c.js",
"integrity": "sha384-dsSpdnRISp7bIdL3fpyygvNfUizujkwhA11RPZehiEUvEt52ESLYt+Hlp3c9/iZp"
"file": "assets/vendor-f5e18451.js",
"integrity": "sha384-BzNMP5uYsiVrJylPJoLh/L+4WMPug3bTJN6sHZQpTe/UM3l1gwxier4DHUeuKNwq"
},
"grid-ff3-theme.css": {
"file": "assets/grid-ff3-theme-badb0a41.css",
"src": "grid-ff3-theme.css",
"integrity": "sha384-YOSw8Sq/038Q+it1zgud/qsZiSuNu8tf099eME8psK3OAg1G3FfV063w7Q+LGQj0"
},
"node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf": {
"file": "assets/fa-brands-400-5656d596.ttf",
@ -108,66 +121,76 @@
"src": "node_modules/leaflet/dist/images/marker-icon.png",
"integrity": "sha384-wg83fCOXjBtqzFAWhTL9Sd9vmLUNhfEEzfmNUX9zwv2igKlz/YQbdapF4ObdxF+R"
},
"resources/assets/v2/pages/dashboard/dashboard.js": {
"file": "assets/dashboard-5f195194.js",
"resources/assets/v2/pages/accounts/index.js": {
"css": [
"assets/grid-ff3-theme-badb0a41.css"
],
"file": "assets/index-bcaedc78.js",
"imports": [
"_format-money-2e5851ad.js",
"_vendor-50e42c6c.js",
"_get-e0a81c64.js",
"_get-28b9aa25.js"
"_format-money-00d5f4b9.js",
"_vendor-f5e18451.js",
"_get-5b2cde80.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/accounts/index.js",
"integrity": "sha384-76KRUQFORlmJ/leXDqMLlPq4/4wiW8vFbtL5qUk+bE86/oKpY8CIuk17zFU1emoS"
},
"resources/assets/v2/pages/dashboard/dashboard.js": {
"file": "assets/dashboard-41a4b449.js",
"imports": [
"_format-money-00d5f4b9.js",
"_vendor-f5e18451.js",
"_get-5b2cde80.js",
"_get-59bb27e9.js",
"_get-0097c30a.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/dashboard/dashboard.js",
"integrity": "sha384-/MzTdILeI9XL/Q9uFEP734/m9XlPpRFcLFQxe8i+di4fuU218Q4W+NSG3DGcKQ11"
"integrity": "sha384-1kybqwSzdo4Qfy5jIkvNgbYJwALV6jnXqKM/aGUAM1sjIJvfGnxK2DjHX4lZN3IZ"
},
"resources/assets/v2/pages/transactions/create.js": {
"file": "assets/create-5a4939e8.js",
"file": "assets/create-b721a410.js",
"imports": [
"_format-money-2e5851ad.js",
"_create-empty-split-81f71b2e.js",
"_splice-errors-into-transactions-6727b386.js",
"_vendor-50e42c6c.js",
"_get-28b9aa25.js"
"_format-money-00d5f4b9.js",
"_create-empty-split-c97f45c5.js",
"_splice-errors-into-transactions-bba1ef64.js",
"_vendor-f5e18451.js",
"_get-0097c30a.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/transactions/create.js",
"integrity": "sha384-o9vKV/iUV+9XIsX8rG9jk8bMPSxuV827U7ScOca2b6I6f/lyJBUDmzoMqMIfaqce"
"integrity": "sha384-cAEOcjbtExGHfptyeQTlY4AINMi2a/DH22mgeTzvjnzY2KbZ0UydbBze9XmXNq9v"
},
"resources/assets/v2/pages/transactions/edit.js": {
"file": "assets/edit-4c7ba005.js",
"file": "assets/edit-195027dc.js",
"imports": [
"_format-money-2e5851ad.js",
"_get-e0a81c64.js",
"_parse-downloaded-splits-83822d1b.js",
"_splice-errors-into-transactions-6727b386.js",
"_vendor-50e42c6c.js",
"_create-empty-split-81f71b2e.js",
"_put-bb9ad93a.js",
"_get-28b9aa25.js"
"_format-money-00d5f4b9.js",
"_get-59bb27e9.js",
"_parse-downloaded-splits-9e5b1aaf.js",
"_splice-errors-into-transactions-bba1ef64.js",
"_vendor-f5e18451.js",
"_create-empty-split-c97f45c5.js",
"_put-d6ed6223.js",
"_get-0097c30a.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/transactions/edit.js",
"integrity": "sha384-MOmzHUGTHwnuic0yw3wUp8MJO+Y9iLdXGy+V5iWtiZLpG2ts8z8R8Ms9Gp9L7Rc0"
},
"resources/assets/v2/pages/transactions/index.css": {
"file": "assets/index-badb0a41.css",
"src": "resources/assets/v2/pages/transactions/index.css",
"integrity": "sha384-YOSw8Sq/038Q+it1zgud/qsZiSuNu8tf099eME8psK3OAg1G3FfV063w7Q+LGQj0"
"integrity": "sha384-yZLBSDqjmV3bx4u+464kapaHom6+82OkAyZfuITONyH8DbtMsQ1HrNPTJbeBZ3qN"
},
"resources/assets/v2/pages/transactions/index.js": {
"css": [
"assets/index-badb0a41.css"
"assets/grid-ff3-theme-badb0a41.css"
],
"file": "assets/index-90222973.js",
"file": "assets/index-943f66ab.js",
"imports": [
"_format-money-2e5851ad.js",
"_vendor-50e42c6c.js",
"_put-bb9ad93a.js",
"_get-e0a81c64.js"
"_format-money-00d5f4b9.js",
"_vendor-f5e18451.js",
"_put-d6ed6223.js",
"_get-59bb27e9.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/transactions/index.js",
"integrity": "sha384-Sl6bKzSJxtyjnJoamDiBSZwdCi+AA8ZV/s40dXKFznm6RfYLiBLSS2qmQMlrVPZT"
"integrity": "sha384-gaqmZQLTNbfSpNmb87PYF8FD5c7jq3Ng+4ghR3Q3aKxOLhOLyBXoVb7ryCnkpfcF"
},
"resources/assets/v2/pages/transactions/show.css": {
"file": "assets/show-8b1429e5.css",
@ -178,17 +201,17 @@
"css": [
"assets/show-8b1429e5.css"
],
"file": "assets/show-3ef1a32a.js",
"file": "assets/show-63e01089.js",
"imports": [
"_format-money-2e5851ad.js",
"_vendor-50e42c6c.js",
"_get-e0a81c64.js",
"_parse-downloaded-splits-83822d1b.js",
"_create-empty-split-81f71b2e.js"
"_format-money-00d5f4b9.js",
"_vendor-f5e18451.js",
"_get-59bb27e9.js",
"_parse-downloaded-splits-9e5b1aaf.js",
"_create-empty-split-c97f45c5.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/transactions/show.js",
"integrity": "sha384-SbD7zS5YgCmKGz4yqp3mEIvEye39FouSpd2+WCld8l7Wij23HCUdfGgF/cD+b21y"
"integrity": "sha384-x17vRUDjWuxqceuMDNz/325LQPW85BoFU7068e9G3TPT01C1vfmQAfPcYxlWaYxn"
},
"resources/assets/v2/sass/app.scss": {
"file": "assets/app-fb7b26ec.css",
@ -197,8 +220,8 @@
"integrity": "sha384-asG3EmbviAZntc1AzgJpoF+jBChn+oq/7eQfYWrCdJ1Ku/c7rJ82sstr6Eptxqgd"
},
"vendor.css": {
"file": "assets/vendor-6fbf50c2.css",
"file": "assets/vendor-52daf6b6.css",
"src": "vendor.css",
"integrity": "sha384-0YVccvAWchjHnv20Seb0KZBtTsv9I25fn2FuQ23kgKg6ST+hYNb95i7F6T/1sC58"
"integrity": "sha384-Fc+BMCTQkokqH7JnKp0yiICeLn8STx2f8o4TbcqhtXweJCOlM/cHwVILvX9LkK9t"
}
}

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

@ -2,7 +2,7 @@
"config": {
"html_language": "de",
"date_time_fns": "dd. MMM. yyyy um HH:mm:ss",
"date_time_fns_short": "dd. MMM. yyyy um HH:mm"
"date_time_fns_short": "MMMM do, yyyy @ HH:mm"
},
"firefly": {
"spent": "Ausgegeben",

View File

@ -2,7 +2,7 @@
"config": {
"html_language": "de",
"date_time_fns": "dd. MMM. yyyy um HH:mm:ss",
"date_time_fns_short": "dd. MMM. yyyy um HH:mm"
"date_time_fns_short": "MMMM do, yyyy @ HH:mm"
},
"firefly": {
"spent": "Ausgegeben",

View File

@ -17,7 +17,7 @@
"money_flowing_in": "In",
"money_flowing_out": "Ut",
"category": "Kategori",
"unknown_category_plain": "No category",
"unknown_category_plain": "Ingen kategori",
"all_money": "All your money",
"unknown_source_plain": "Unknown source account",
"unknown_dest_plain": "Unknown destination account",

View File

@ -17,7 +17,7 @@
"money_flowing_in": "In",
"money_flowing_out": "Ut",
"category": "Kategori",
"unknown_category_plain": "No category",
"unknown_category_plain": "Ingen kategori",
"all_money": "All your money",
"unknown_source_plain": "Unknown source account",
"unknown_dest_plain": "Unknown destination account",

View File

@ -82,7 +82,7 @@ The most exciting features are:
Then the things that make you go "yeah OK, makes sense".
* A [double-entry](https://en.wikipedia.org/wiki/Double-entry_bookkeeping_system) bookkeeping system.
* Save towards a goal using [piggy banks](https://docs.firefly-iii.org/explanation/financial-concepts/piggies/).
* Save towards a goal using [piggy banks](https://docs.firefly-iii.org/explanation/financial-concepts/piggy-banks/).
* View [income and expense reports](https://docs.firefly-iii.org/how-to/firefly-iii/finances/reports/).
And the things you would hope for but not expect:

View File

@ -5,8 +5,8 @@
"flash_warning": "Achtung!",
"flash_success": "Geschafft!",
"close": "Schlie\u00dfen",
"select_dest_account": "Bitte einen g\u00fcltigen Zielkontonamen ausw\u00e4hlen oder eingeben",
"select_source_account": "Bitte einen g\u00fcltigen Quellkontonamen ausw\u00e4hlen oder eingeben",
"select_dest_account": "Please select or type a valid destination account name",
"select_source_account": "Please select or type a valid source account name",
"split_transaction_title": "Beschreibung der Splittbuchung",
"errors_submission": "Bei Ihren Eingaben stimmt etwas nicht. Bitte \u00fcberpr\u00fcfen Sie die unten stehenden Fehler.",
"split": "Teilen",

View File

@ -26,17 +26,22 @@ export default class Get {
/**
*
* @param identifier
* @param date
* @param params
* @returns {Promise<AxiosResponse<any>>}
*/
get(identifier, date) {
let params = {date: format(date, 'y-MM-dd').slice(0, 10)};
if (!date) {
return api.get('/api/v2/accounts/' + identifier);
}
show(identifier, params) {
return api.get('/api/v2/accounts/' + identifier, {params: params});
}
/**
*
* @param params
* @returns {Promise<AxiosResponse<any>>}
*/
index(params) {
return api.get('/api/v2/accounts', {params: params});
}
/**
*
* @param identifier

View File

@ -0,0 +1,111 @@
/*
* show.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 '../../boot/bootstrap.js';
import dates from "../shared/dates.js";
import i18next from "i18next";
import {format} from "date-fns";
import formatMoney from "../../util/format-money.js";
import '@ag-grid-community/styles/ag-grid.css';
import '@ag-grid-community/styles/ag-theme-alpine.css';
import '../../css/grid-ff3-theme.css';
import Get from "../../api/v2/model/account/get.js";
// set type from URL
const urlParts = window.location.href.split('/');
const type = urlParts[urlParts.length - 1];
let index = function () {
return {
// notifications
notifications: {
error: {
show: false, text: '', url: '',
}, success: {
show: false, text: '', url: '',
}, wait: {
show: false, text: '',
}
}, totalPages: 1, page: 1, // available columns:
tableColumns: {
name: {
enabled: true
},
},
accounts: [],
formatMoney(amount, currencyCode) {
return formatMoney(amount, currencyCode);
},
format(date) {
return format(date, i18next.t('config.date_time_fns'));
},
init() {
this.notifications.wait.show = true;
this.notifications.wait.text = i18next.t('firefly.wait_loading_data')
this.loadAccounts();
},
loadAccounts() {
// one page only.
(new Get()).index({type: type, page: this.page}).then(response => {
for (let i = 0; i < response.data.data.length; i++) {
if (response.data.data.hasOwnProperty(i)) {
let current = response.data.data[i];
let account = {
id: parseInt(current.id),
name: current.attributes.name,
};
this.accounts.push(account);
}
}
this.notifications.wait.show = false;
});
},
}
}
let comps = {index, dates};
function loadPage() {
Object.keys(comps).forEach(comp => {
console.log(`Loading page component "${comp}"`);
let data = comps[comp]();
Alpine.data(comp, () => data);
});
Alpine.start();
}
// wait for load until bootstrapped event is received.
document.addEventListener('firefly-iii-bootstrapped', () => {
console.log('Loaded through event listener.');
loadPage();
});
// or is bootstrapped before event is triggered.
if (window.bootstrapped) {
console.log('Loaded through window variable.');
loadPage();
}

View File

@ -191,7 +191,7 @@ export default () => ({
if (account.hasOwnProperty(i)) {
let accountId = account[i];
// grab account info for box:
(new Get).get(accountId, new Date(window.store.get('end'))).then((response) => {
(new Get).show(accountId, new Date(window.store.get('end'))).then((response) => {
let parent = response.data.data;
// get groups for account:

View File

@ -44,12 +44,6 @@ const urlParts = window.location.href.split('/');
const type = urlParts[urlParts.length - 1];
ds.setType(type);
document.addEventListener('cellEditRequest', () => {
console.log('Loaded through event listener.');
//loadPage();
});
let rowImmutableStore = [];
let dataTable;
const editableFields = ['description', 'amount', 'date'];
@ -90,22 +84,18 @@ const onCellEditRequestMethod = (event) => {
};
document.addEventListener('cellValueChanged', () => {
console.log('I just realized a cell value has changed.');
});
document.addEventListener('onCellValueChanged', () => {
console.log('I just realized a cell value has changed.');
});
let doOnCellValueChanged = function (e) {
console.log('I just realized a cell value has changed.');
};
const gridOptions = {
rowModelType: 'infinite',
datasource: ds,
cacheOverflowSize: 1,
cacheBlockSize: 20,
onCellEditRequest: onCellEditRequestMethod,
readOnlyEdit: true,
getRowId: function (params) {
console.log('getRowId', params.data.id);
return params.data.id;
},
// Row Data: The data to be displayed.
// rowData: [
// { description: "Tesla", model: "Model Y", price: 64950, electric: true },

View File

@ -64,7 +64,7 @@ return [
// 'date_time' => '%B %e, %Y, @ %T',
'date_time_js' => 'Do MMMM YYYY um HH:mm:ss',
'date_time_fns' => 'dd. MMM. yyyy um HH:mm:ss',
'date_time_fns_short' => 'dd. MMM. yyyy um HH:mm',
'date_time_fns_short' => 'MMMM do, yyyy @ HH:mm',
// 'specific_day' => '%e %B %Y',
'specific_day_js' => 'D. MMMM YYYY',

View File

@ -317,8 +317,8 @@ return [
'update_new_version_alert' => 'Eine neue Version von Firefly III ist verfügbar. Sie verwenden :your_version, die neueste Version ist :new_version, die am :date veröffentlicht wurde.',
'update_version_beta' => 'Seien Sie vorsichtig bei der Verwendung dieser BETA-Version. Sie könnte noch Fehler enthaltern.',
'update_version_alpha' => 'Seien Sie vorsichtig bei der Verwendung dieser APLPHA-Version. Sie kann Fehler enthaltern.',
'update_current_dev_older' => 'Sie verwenden die Entwicklungsversion „:version”, die älter ist als die neueste Version :new_version. Bitte aktualisieren Sie!',
'update_current_dev_newer' => 'Sie verwenden die Entwicklungsversion „:version”, die neuer ist als die letzte Version :new_version.',
'update_current_dev_older' => 'You are running development release ":version", which is older than the latest release :new_version. Please update!',
'update_current_dev_newer' => 'You are running development release ":version", which is newer than the latest release :new_version.',
'update_current_version_alert' => 'Sie verwenden Version :version. Dies ist die neueste verfügbare Version.',
'update_newer_version_alert' => 'Sie verwenden :your_version. Ihre Version ist neuer als die neueste Version (:new_version).',
'update_check_error' => 'Bei der Suche nach Aktualisierungen ist ein Fehler aufgetreten: :error',
@ -896,12 +896,12 @@ return [
'rule_trigger_budget_is' => 'Budget ist „:trigger_value”',
'rule_trigger_tag_is_choice' => 'Irgendein Schlagwort lautet..',
'rule_trigger_tag_is' => 'Irgendein Schlagwort lautet ":trigger_value"',
'rule_trigger_tag_contains_choice' => 'Beliebiges Schlagwort enthält …',
'rule_trigger_tag_contains' => 'Beliebiges Schlagwort enthält „:trigger_value”',
'rule_trigger_tag_ends_choice' => 'Beliebiges Schlagwort endet auf ...',
'rule_trigger_tag_ends' => 'Beliebiges Schlagwort endet auf „:trigger_value”',
'rule_trigger_tag_starts_choice' => 'Beliebiges Schlagwort beginnt mit ...',
'rule_trigger_tag_starts' => 'Beliebiges Schlagwort beginnt mit „:trigger_value”',
'rule_trigger_tag_contains_choice' => 'Any tag contains..',
'rule_trigger_tag_contains' => 'Any tag contains ":trigger_value"',
'rule_trigger_tag_ends_choice' => 'Any tag ends with..',
'rule_trigger_tag_ends' => 'Any tag ends with ":trigger_value"',
'rule_trigger_tag_starts_choice' => 'Any tag starts with..',
'rule_trigger_tag_starts' => 'Any tag starts with ":trigger_value"',
'rule_trigger_currency_is_choice' => 'Buchungswährung ist..',
'rule_trigger_currency_is' => 'Buchungswährung ist „:trigger_value”',
'rule_trigger_foreign_currency_is_choice' => 'Fremdwährung der Buchung ist..',
@ -1586,8 +1586,8 @@ return [
'submission_options' => 'Übermittlungsoptionen',
'apply_rules_checkbox' => 'Regeln anwenden',
'fire_webhooks_checkbox' => 'Webhooks abfeuern',
'select_source_account' => 'Bitte einen gültigen Quellkontonamen auswählen oder eingeben',
'select_dest_account' => 'Bitte einen gültigen Zielkontonamen auswählen oder eingeben',
'select_source_account' => 'Please select or type a valid source account name',
'select_dest_account' => 'Please select or type a valid destination account name',
// convert stuff:
'convert_is_already_type_Withdrawal' => 'Diese Buchung ist bereits eine Ausgabe',
@ -2459,7 +2459,7 @@ return [
'block_code_bounced' => 'E-Mail-Nachricht(en) wurden abgewiesen',
'block_code_expired' => 'Demo-Konto abgelaufen',
'no_block_code' => 'Kein Grund für Block oder Benutzer nicht blockiert',
'demo_user_export' => 'Der Demo-Benutzer kann keine Daten exportieren',
'demo_user_export' => 'The demo user cannot export data',
'block_code_email_changed' => 'Der Benutzer hat die neue E-Mail-Adresse noch nicht bestätigt',
'admin_update_email' => 'Im Gegensatz zur Profilseite wird der Benutzer NICHT benachrichtigt, dass seine E-Mail-Adresse geändert wurde!',
'update_user' => 'Benutzer aktualisieren',

View File

@ -1941,7 +1941,7 @@ return [
'categories' => 'Kategorier',
'edit_category' => 'Redigera kategori ":name"',
'no_category' => '(utan kategori)',
'unknown_category_plain' => 'No category',
'unknown_category_plain' => 'Ingen kategori',
'category' => 'Kategori',
'delete_category' => 'Ta bort kategori ":name"',
'deleted_category' => 'Kategori ":name" togs bort',

View File

@ -34,6 +34,6 @@
declare(strict_types=1);
return [
'failed' => 'Credentials นี้ไม่ตรงกับบันทึกของเรา',
'throttle' => 'พยายามเข้าสู่ระบบมากเกินไป โปรดลองใหม่อีกครั้งใน :seconds วินาที',
'failed' => 'ข้อมูลนี้ไม่ตรงกับในระบบ',
'throttle' => 'คุณได้พยายามเข้าระบบหลายครั้งเกินไป กรุณาลองใหม่ใน :seconds วินาทีข้างหน้า.',
];

View File

@ -38,7 +38,7 @@ return [
'see_help_icon' => 'อย่างไรก็ตาม ไอคอน <i class="fa fa-question-circle"></i> ในมุมบนขวาจะช่วยอธิบายเพิ่มเติมให้คุณ',
'index' => 'ยินดีต้อนรับเข้าสู่ <strong>Firefly III</strong>! ในหน้านี้แสดงรายละเอียดทางการเงินอย่างรวดเร็วให้คุณรับชม สำหรับรายละเอียดเพิ่มเติม สามารถดูได้ที่บัญชี &rarr; <a href=":asset"> บัญชีสินทรัพย์ </a> และแน่นอน จะมีส่วนของหน้า <a href=":budgets"> งบประมาณ </a> และหน้า <a href=":reports"> รายงาน </a> หรือ คุณสามารถที่จะดูทุกเมนูได้ตามสบาย',
'accounts-index' => 'บัญชีทรัพย์สินคือบัญชีส่วนตัวของคุณ, ส่วนบัญชีค่าใช้จ่ายคือบัญชีสำหรับให้คุณทำการสร้างรายจ่ายขึ้นมาในบัญชีนี้ เช่น การเก็บเงิน หรือ ให้เพื่อน เป็นต้น, บัญชีรายได้คือบัญชีที่ได้รับเงินจากส่วนต่างๆ เช่น เงินได้จากงานของคุณ, เงินได้จากภาครัฐ หรือ รายรับอื่นๆ, ส่วนบัญชีหนี้สิน คือ บัญชีสำหรับกรณีมีหนี้ และ การกู้ยืมต่างๆ เช่น หนี้จากบัตรเครดิต หรือ หนี้จากกองทุนยืมเรียน ซึ่งในหน้านี้คุณสามารถแก้ไขหรือลบได้',
'budgets-index' => 'This page shows you an overview of your budgets. The top bar shows the amount that is available to be budgeted. This can be customized for any period by clicking the amount on the right. The amount you\'ve actually spent is shown in the bar below. Below that are the expenses per budget and what you\'ve budgeted for them.',
'budgets-index' => 'หน้านี้แสดงภาพรวมงบประมาณของคุณ แถบด้านบนแสดงจำนวนเงินที่สามารถจัดงบประมาณได้ ซึ่งสามารถปรับแต่งในช่วงเวลาใดก็ได้โดยคลิกจำนวนเงินทางด้านขวา จำนวนเงินที่คุณใช้จ่ายจริงจะแสดงอยู่ในแถบด้านล่าง ด้านล่างนี้คือค่าใช้จ่ายต่องบประมาณและสิ่งที่คุณตั้งงบประมาณไว้',
'reports-index-start' => 'Firefly III รองรับจำนวนรายงานหลายรูปแบบ อ่านเพิ่มเติมโดยคลิ๊กไปที่ไอคอน <i class="fa fa-question-circle"></i> ที่อยู่ด้านบนขวาของมุม',
'reports-index-examples' => 'ลองดูตัวอย่างของเรา: <a href=":one">รายละเอียดการเงินรายเดือน</a>, <a href=":two">รายละเอียดการเงินรายปี</a> และ <a href=":three">รายละเอียดงบประมาณโดยรวม</a>',
'currencies-index' => 'Firefly III รองรับค่าเงินหลายประเภท ถึงแม้ว่าค่าเงินเดิมจะตั้งค่าไว้ที่ Euro สามารถที่จะเปลี่ยนเป็น US Dollar ได้ หรือค่าเงินอื่นๆได้ โดยคุณจะเห็นหน้าของการตั้งค่าสกุลเงิน โดยจะมีค่าเงินต่างๆ อยู่แล้ว ซึ่งคุณสามารถเพิ่มค่าเงินของคุณเข้าไปได้ โดยที่การเปลี่ยนค่าเงินตั้งต้น จะไม่ส่งผลต่อการเปลี่ยนค่าเงินในธุรกรรมที่ถูกบันทึกไปก่อนหน้านี้ อย่างไรก็ตาม Firefly III รองรับค่าเงินหลายประเภทในเวลาเดียวกัน',

View File

@ -56,10 +56,10 @@ return [
*/
// invite
'invitation_created_subject' => 'An invitation has been created',
'invitation_created_body' => 'Admin user ":email" created a user invitation which can be used by whoever is behind email address ":invitee". The invite will be valid for 48hrs.',
'invite_user_subject' => 'You\'ve been invited to create a Firefly III account.',
'invitation_introduction' => 'You\'ve been invited to create a Firefly III account on **:host**. Firefly III is a personal, self-hosted, private personal finance manager. All the cool kids are using it.',
'invitation_created_subject' => 'คำเชิญถูกสร้างขึ้นสร้างแล้ว',
'invitation_created_body' => 'ผู้ดูแลระบบ ":email" ได้สร้างคำเชิญให้ผู้ใช้ตามที่อยู่อีเมล ":invitee" คำเชิญจะมีอายุการใช้งาน 48 ชั่วโมง',
'invite_user_subject' => 'คุณได้รับเชิญให้สร้างบัญชี Firefly III',
'invitation_introduction' => 'คุณได้รับเชิญให้สร้างบัญชี Firefly III บน **:host** Firefly III เป็นผู้จัดการการเงินส่วนบุคคลส่วนตัวที่โฮสต์เอง',
'invitation_invited_by' => 'You\'ve been invited by ":admin" and this invitation was sent to ":invitee". That\'s you, right?',
'invitation_url' => 'The invitation is valid for 48 hours and can be redeemed by surfing to [Firefly III](:url). Enjoy!',

View File

@ -38,29 +38,29 @@ return [
'index_intro' => 'ยินดีต้อนรับสู่หน้าหลักของ Firefly III โปรดสละเวลาอ่านบทแนะนำนี้เพื่อทำความเข้าใจว่า Firefly III ทำงานอย่างไร',
'index_accounts-chart' => 'กราฟนี้แสดงยอดคงเหลือปัจจุบันของบัญชีสินทรัพย์ของคุณ คุณสามารถเลือกบัญชีที่แสดงได้ที่นี่ในการตั้งค่าของคุณ',
'index_box_out_holder' => 'กล่องเล็กๆ นี้และกล่องข้างๆ จะทำให้คุณเห็นภาพรวมโดยย่อเกี่ยวกับสถานการณ์ทางการเงินของคุณ',
'index_help' => 'If you ever need help with a page or a form, press this button.',
'index_outro' => 'Most pages of Firefly III will start with a little tour like this one. Please contact me when you have questions or comments. Enjoy!',
'index_sidebar-toggle' => 'To create new transactions, accounts or other things, use the menu under this icon.',
'index_cash_account' => 'These are the accounts created so far. You can use the cash account to track cash expenses but it\'s not mandatory of course.',
'index_help' => 'หากคุณต้องการความช่วยเหลือเกี่ยวกับหน้าหรือแบบฟอร์ม ให้กดปุ่มนี้',
'index_outro' => 'หน้าเพจส่วนใหญ่ของ Firefly III จะเริ่มต้นด้วยการทัวร์เล็กๆ น้อยๆ แบบนี้ โปรดติดต่อฉันเมื่อคุณมีคำถามหรือความคิดเห็น Enjoy!',
'index_sidebar-toggle' => 'หากต้องการสร้างธุรกรรม บัญชี หรือสิ่งอื่นๆ ใหม่ ให้ใช้เมนูใต้ไอคอนนี้',
'index_cash_account' => 'นี่คือบัญชีที่สร้างขึ้นจนถึงตอนนี้ คุณสามารถใช้บัญชีเงินสดเพื่อติดตามค่าใช้จ่ายเงินสดได้ แต่ไม่ได้บังคับแน่นอน',
// transactions
'transactions_create_basic_info' => 'Enter the basic information of your transaction. Source, destination, date and description.',
'transactions_create_amount_info' => 'Enter the amount of the transaction. If necessary the fields will auto-update for foreign amount info.',
'transactions_create_optional_info' => 'All of these fields are optional. Adding meta-data here will make your transactions better organised.',
'transactions_create_split' => 'If you want to split a transaction, add more splits with this button',
'transactions_create_basic_info' => 'ป้อนข้อมูลพื้นฐานของการทำธุรกรรมของคุณ อย่างเช่น แหล่งที่มา ปลายทาง วันที่ และคำอธิบาย',
'transactions_create_amount_info' => 'ป้อนจำนวนเงินของธุรกรรม หากจำเป็น ฟิลด์จะอัปเดตอัตโนมัติสำหรับข้อมูลจำนวนเงินต่างประเทศ',
'transactions_create_optional_info' => 'ฟิลด์ทั้งหมดนี้ไม่จำเป็นต้องกรอก การเพิ่มข้อมูลเมตาที่นี่จะทำให้ธุรกรรมของคุณจัดระเบียบได้ดีขึ้น',
'transactions_create_split' => 'หากคุณต้องการแยกธุรกรรม ให้เพิ่มด้วยปุ่มนี้',
// create account:
'accounts_create_iban' => 'Give your accounts a valid IBAN. This could make a data import very easy in the future.',
'accounts_create_asset_opening_balance' => 'Assets accounts may have an "opening balance", indicating the start of this account\'s history in Firefly III.',
'accounts_create_asset_currency' => 'Firefly III supports multiple currencies. Asset accounts have one main currency, which you must set here.',
'accounts_create_asset_virtual' => 'It can sometimes help to give your account a virtual balance: an extra amount always added to or removed from the actual balance.',
'accounts_create_iban' => 'ให้ IBAN ที่ถูกต้องแก่บัญชีของคุณ ซึ่งอาจทำให้การนำเข้าข้อมูลในอนาคตเป็นเรื่องง่ายมาก',
'accounts_create_asset_opening_balance' => 'บัญชีสินทรัพย์อาจมี "ยอดดุลยกมา" ซึ่งบ่งบอกถึงจุดเริ่มต้นของประวัติของบัญชีนี้ใน Firefly III',
'accounts_create_asset_currency' => 'Firefly III รองรับหลายสกุลเงิน บัญชีสินทรัพย์จำเป็นต้องมีสกุลเงินหลักหนึ่งสกุล ซึ่งคุณต้องตั้งค่าที่นี่',
'accounts_create_asset_virtual' => 'บางครั้งการมอบยอดเสมือนให้กับบัญชีของคุณอาจช่วยได้: จำนวนเงินพิเศษจะถูกบวกเข้าหรือลบออกจากยอดจริงเสมอ',
// budgets index
'budgets_index_intro' => 'Budgets are used to manage your finances and form one of the core functions of Firefly III.',
'budgets_index_see_expenses_bar' => 'Spending money will slowly fill this bar.',
'budgets_index_navigate_periods' => 'Navigate through periods to easily set budgets ahead of time.',
'budgets_index_new_budget' => 'Create new budgets as you see fit.',
'budgets_index_list_of_budgets' => 'Use this table to set the amounts for each budget and see how you are doing.',
'budgets_index_intro' => 'งบประมาณใช้เพื่อจัดการการเงินของคุณคือหนึ่งในหน้าที่หลักของ Firefly III',
'budgets_index_see_expenses_bar' => 'การใช้จ่ายเงินจะค่อยๆทำให้แถบนี้เต็ม',
'budgets_index_navigate_periods' => 'เลื่อนไปตามช่วงเวลาเพื่อตั้งงบประมาณล่วงหน้าได้อย่างง่ายดาย',
'budgets_index_new_budget' => 'สร้างงบประมาณใหม่ตามที่คุณต้องการ',
'budgets_index_list_of_budgets' => 'ใช้ตารางนี้เพื่อกำหนดจำนวนเงินสำหรับงบประมาณแต่ละอย่างและดูว่าคุณใช้จ่ายไปแล้วเท่าไร',
'budgets_index_outro' => 'To learn more about budgeting, checkout the help icon in the top right corner.',
/*

View File

@ -0,0 +1,109 @@
@extends('layout.v2')
@section('vite')
@vite(['resources/assets/v2/sass/app.scss', 'resources/assets/v2/pages/accounts/index.js'])
@endsection
@section('content')
<div class="app-content">
<div class="container-fluid" x-data="index">
<x-messages></x-messages>
<div class="row mb-3">
<div class="col-xl-4 col-lg-6 col-md-12 col-sm-12 col-xs-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Info</h3>
</div>
<div class="card-body">
some chart
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6 col-md-12 col-sm-12 col-xs-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Info</h3>
</div>
<div class="card-body">
Same
</div>
</div>
</div>
<div class="col-xl-4 col-lg-6 col-md-12 col-sm-12 col-xs-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Info</h3>
</div>
<div class="card-body">
Same
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col">
Nav
</div>
</div>
<div class="row mb-3">
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="card mb-3">
<div class="card-header">
<div class="row">
<div class="col">
<h3 class="card-title">Accounts (ungrouped)</h3>
</div>
<div class="col text-end">
</div>
</div>
</div>
<div class="card-body p-0">
<table class="table">
<thead>
<tr>
<td>&nbsp;</td>
<td>Active?</td>
<td>Name</td>
<td>Type</td>
<td>Account number</td>
<td>Current balance</td>
<td>Last activity</td>
<td>Balance difference</td>
<td>&nbsp;</td>
</tr>
</thead>
<tbody>
<template x-for="(account, index) in accounts" :key="index">
<tr>
<td>&nbsp;</td>
<td>
&nbsp;
</td>
<td>
<a :href="'./accounts/show/' + account.id">
<span x-text="account.name"></span>
</a>
</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>&nbsp;</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col">
Nav
</div>
</div>
</div>
</div>
@endsection

View File

@ -1,3 +1,9 @@
<li class="nav-item">
<a class="nav-link" href="{{ route(Route::current()->getName(), Route::current()->parameters()) }}?force_default_layout=true">
<i class="fa-solid fa-landmark"></i>
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link" data-bs-toggle="dropdown" href="#">
<i class="fa-solid fa-gears"></i>

View File

@ -104,6 +104,7 @@ Route::group(
'as' => 'api.v2.accounts.',
],
static function (): void {
Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']);
Route::get('{account}', ['uses' => 'ShowController@show', 'as' => 'show']);
}
);
@ -173,7 +174,6 @@ Route::group(
);
// V2 API route for budgets and budget limits:
// TODO Validate from here down.
Route::group(
[
'namespace' => 'FireflyIII\Api\V2\Controllers\Model',

View File

@ -1204,11 +1204,11 @@ Route::group(
// show groups:
// TODO improve these routes
Route::get('{what}/all', ['uses' => 'Transaction\IndexController@indexAll', 'as' => 'index.all'])->where(
['what' => 'withdrawal|deposit|transfers|transfer']
['what' => 'withdrawal|deposit|transfers|transfer|all']
);
Route::get('{what}/{start_date?}/{end_date?}', ['uses' => 'Transaction\IndexController@index', 'as' => 'index'])->where(
['what' => 'withdrawal|deposit|transfers|transfer']
['what' => 'withdrawal|deposit|transfers|transfer|all']
)->where(['start_date' => DATEFORMAT])
->where(['end_date' => DATEFORMAT])
;

View File

@ -44,11 +44,15 @@ export default defineConfig({
'resources/assets/v2/sass/app.scss',
'resources/assets/v2/pages/dashboard/dashboard.js',
// accounts
'resources/assets/v2/pages/accounts/index.js',
// transactions
'resources/assets/v2/pages/transactions/create.js',
'resources/assets/v2/pages/transactions/edit.js',
'resources/assets/v2/pages/transactions/show.js',
'resources/assets/v2/pages/transactions/index.js',
],
refresh: true,
}),