Update, rebuild, and add a new API endpoint.

This commit is contained in:
James Cole 2024-01-17 20:23:02 +01:00
parent 66b0d9d309
commit 44df07a5f5
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
52 changed files with 1638 additions and 241 deletions

View File

@ -226,16 +226,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.46.0",
"version": "v3.47.1",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "be6831c9af1740470d2a773119b9273f8ac1c3d2"
"reference": "173c60d1eff911c9c54322704623a45561d3241d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/be6831c9af1740470d2a773119b9273f8ac1c3d2",
"reference": "be6831c9af1740470d2a773119b9273f8ac1c3d2",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/173c60d1eff911c9c54322704623a45561d3241d",
"reference": "173c60d1eff911c9c54322704623a45561d3241d",
"shasum": ""
},
"require": {
@ -305,7 +305,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.46.0"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.47.1"
},
"funding": [
{
@ -313,7 +313,7 @@
"type": "github"
}
],
"time": "2024-01-03T21:38:46+00:00"
"time": "2024-01-16T18:54:21+00:00"
},
{
"name": "psr/container",

View File

@ -0,0 +1,93 @@
<?php
/*
* UpdateController.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\Transaction;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Model\Transaction\UpdateRequest;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use FireflyIII\Transformers\V2\TransactionGroupTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
class UpdateController extends Controller
{
private TransactionGroupRepositoryInterface $groupRepository;
/**
* TransactionController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->groupRepository = app(TransactionGroupRepositoryInterface::class);
return $next($request);
}
);
}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/transactions/updateTransaction
*
* Update a transaction.
*
* @throws FireflyException
*/
public function update(UpdateRequest $request, TransactionGroup $transactionGroup): JsonResponse
{
app('log')->debug('Now in update routine for transaction group [v2]!');
$data = $request->getAll();
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
$applyRules = $data['apply_rules'] ?? true;
$fireWebhooks = $data['fire_webhooks'] ?? true;
event(new UpdatedTransactionGroup($transactionGroup, $applyRules, $fireWebhooks));
app('preferences')->mark();
/** @var User $admin */
$admin = auth()->user();
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setUser($admin)->setTransactionGroup($transactionGroup);
$selectedGroup = $collector->getGroups()->first();
if (null === $selectedGroup) {
throw new FireflyException('200032: Cannot find transaction. Possibly, a rule deleted this transaction after its creation.');
}
$transformer = new TransactionGroupTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject('transactions', $selectedGroup, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
}

View File

@ -0,0 +1,366 @@
<?php
/*
* UpdateRequest.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\Transaction;
use FireflyIII\Api\V1\Requests\Models\AvailableBudget\Request;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Rules\BelongsUser;
use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsDateOrTime;
use FireflyIII\Rules\IsValidPositiveAmount;
use FireflyIII\Rules\IsValidZeroOrMoreAmount;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use FireflyIII\Validation\GroupValidation;
use FireflyIII\Validation\TransactionValidation;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Validator;
/**
* Class UpdateRequest
*
* TODO it's the same as the v1
*/
class UpdateRequest extends Request
{
use ChecksLogin;
use ConvertsDataTypes;
use GroupValidation;
use TransactionValidation;
private array $arrayFields;
private array $booleanFields;
private array $dateFields;
private array $floatFields;
private array $integerFields;
private array $stringFields;
private array $textareaFields;
/**
* Get all data. Is pretty complex because of all the ??-statements.
*
* @throws FireflyException
*/
public function getAll(): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
$this->integerFields = ['order', 'currency_id', 'foreign_currency_id', 'transaction_journal_id', 'source_id', 'destination_id', 'budget_id', 'category_id', 'bill_id', 'recurrence_id'];
$this->dateFields = ['date', 'interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date'];
$this->textareaFields = ['notes'];
// not really floats, for validation.
$this->floatFields = ['amount', 'foreign_amount'];
$this->stringFields = ['type', 'currency_code', 'foreign_currency_code', 'description', 'source_name', 'source_iban', 'source_number', 'source_bic', 'destination_name', 'destination_iban', 'destination_number', 'destination_bic', 'budget_name', 'category_name', 'bill_name', 'internal_reference', 'external_id', 'bunq_payment_id', 'sepa_cc', 'sepa_ct_op', 'sepa_ct_id', 'sepa_db', 'sepa_country', 'sepa_ep', 'sepa_ci', 'sepa_batch_id', 'external_url'];
$this->booleanFields = ['reconciled'];
$this->arrayFields = ['tags'];
$data = [];
if ($this->has('transactions')) {
$data['transactions'] = $this->getTransactionData();
}
if ($this->has('apply_rules')) {
$data['apply_rules'] = $this->boolean('apply_rules', true);
}
if ($this->has('fire_webhooks')) {
$data['fire_webhooks'] = $this->boolean('fire_webhooks', true);
}
if ($this->has('group_title')) {
$data['group_title'] = $this->convertString('group_title');
}
return $data;
}
/**
* The rules that the incoming request must be matched against.
*/
public function rules(): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
$validProtocols = config('firefly.valid_url_protocols');
return [
// basic fields for group:
'group_title' => 'min:1|max:1000|nullable',
'apply_rules' => [new IsBoolean()],
// transaction rules (in array for splits):
'transactions.*.type' => 'in:withdrawal,deposit,transfer,opening-balance,reconciliation',
'transactions.*.date' => [new IsDateOrTime()],
'transactions.*.order' => 'numeric|min:0',
// group id:
'transactions.*.transaction_journal_id' => ['nullable', 'numeric', new BelongsUser()],
// currency info
'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id|nullable',
'transactions.*.currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
'transactions.*.foreign_currency_id' => 'nullable|numeric|exists:transaction_currencies,id',
'transactions.*.foreign_currency_code' => 'nullable|min:3|max:51|exists:transaction_currencies,code',
// amount
'transactions.*.amount' => ['nullable', new IsValidPositiveAmount()],
'transactions.*.foreign_amount' => ['nullable', new IsValidZeroOrMoreAmount()],
// description
'transactions.*.description' => 'nullable|min:1|max:1000',
// source of transaction
'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.source_name' => 'min:1|max:255|nullable',
// destination of transaction
'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUser()],
'transactions.*.destination_name' => 'min:1|max:255|nullable',
// budget, category, bill and piggy
'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUser(), 'nullable'],
'transactions.*.budget_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUser(), 'nullable'],
'transactions.*.category_name' => 'min:1|max:255|nullable',
'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser()],
'transactions.*.bill_name' => ['min:1', 'max:255', 'nullable', new BelongsUser()],
// other interesting fields
'transactions.*.reconciled' => [new IsBoolean()],
'transactions.*.notes' => 'min:1|max:32768|nullable',
'transactions.*.tags' => 'min:0|max:255|nullable',
'transactions.*.tags.*' => 'min:0|max:255',
// meta info fields
'transactions.*.internal_reference' => 'min:1|max:255|nullable',
'transactions.*.external_id' => 'min:1|max:255|nullable',
'transactions.*.recurrence_id' => 'min:1|max:255|nullable',
'transactions.*.bunq_payment_id' => 'min:1|max:255|nullable',
'transactions.*.external_url' => sprintf('min:1|max:255|nullable|url:%s', $validProtocols),
// SEPA fields:
'transactions.*.sepa_cc' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_op' => 'min:1|max:255|nullable',
'transactions.*.sepa_ct_id' => 'min:1|max:255|nullable',
'transactions.*.sepa_db' => 'min:1|max:255|nullable',
'transactions.*.sepa_country' => 'min:1|max:255|nullable',
'transactions.*.sepa_ep' => 'min:1|max:255|nullable',
'transactions.*.sepa_ci' => 'min:1|max:255|nullable',
'transactions.*.sepa_batch_id' => 'min:1|max:255|nullable',
// dates
'transactions.*.interest_date' => 'date|nullable',
'transactions.*.book_date' => 'date|nullable',
'transactions.*.process_date' => 'date|nullable',
'transactions.*.due_date' => 'date|nullable',
'transactions.*.payment_date' => 'date|nullable',
'transactions.*.invoice_date' => 'date|nullable',
];
}
/**
* Configure the validator instance.
*/
public function withValidator(Validator $validator): void
{
app('log')->debug('Now in withValidator');
/** @var TransactionGroup $transactionGroup */
$transactionGroup = $this->route()->parameter('userGroupTransaction');
$validator->after(
function (Validator $validator) use ($transactionGroup): void {
// if more than one, verify that there are journal ID's present.
$this->validateJournalIds($validator, $transactionGroup);
// all transaction types must be equal:
$this->validateTransactionTypesForUpdate($validator);
// user wants to update a reconciled transaction.
// source, destination, amount + foreign_amount cannot be changed
// and must be omitted from the request.
$this->preventUpdateReconciled($validator, $transactionGroup);
// validate source/destination is equal, depending on the transaction journal type.
$this->validateEqualAccountsForUpdate($validator, $transactionGroup);
// see method:
// $this->preventNoAccountInfo($validator, );
// validate that the currency fits the source and/or destination account.
// validate all account info
$this->validateAccountInformationUpdate($validator, $transactionGroup);
}
);
if($validator->fails()) {
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
}
}
/**
* Get transaction data.
*
* @throws FireflyException
*/
private function getTransactionData(): array
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
$return = [];
/** @var null|array $transactions */
$transactions = $this->get('transactions');
if (!is_countable($transactions)) {
return $return;
}
/** @var null|array $transaction */
foreach ($transactions as $transaction) {
if (!is_array($transaction)) {
throw new FireflyException('Invalid data submitted: transaction is not array.');
}
// default response is to update nothing in the transaction:
$current = [];
$current = $this->getIntegerData($current, $transaction);
$current = $this->getStringData($current, $transaction);
$current = $this->getNlStringData($current, $transaction);
$current = $this->getDateData($current, $transaction);
$current = $this->getBooleanData($current, $transaction);
$current = $this->getArrayData($current, $transaction);
$current = $this->getFloatData($current, $transaction);
$return[] = $current;
}
return $return;
}
/**
* For each field, add it to the array if a reference is present in the request:
*
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getIntegerData(array $current, array $transaction): array
{
foreach ($this->integerFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->integerFromValue((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getStringData(array $current, array $transaction): array
{
foreach ($this->stringFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearString((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getNlStringData(array $current, array $transaction): array
{
foreach ($this->textareaFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearStringKeepNewlines((string) $transaction[$fieldName]); // keep newlines
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getDateData(array $current, array $transaction): array
{
foreach ($this->dateFields as $fieldName) {
app('log')->debug(sprintf('Now at date field %s', $fieldName));
if (array_key_exists($fieldName, $transaction)) {
app('log')->debug(sprintf('New value: "%s"', (string) $transaction[$fieldName]));
$current[$fieldName] = $this->dateFromValue((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getBooleanData(array $current, array $transaction): array
{
foreach ($this->booleanFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->convertBoolean((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getArrayData(array $current, array $transaction): array
{
foreach ($this->arrayFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->arrayFromValue($transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getFloatData(array $current, array $transaction): array
{
foreach ($this->floatFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$value = $transaction[$fieldName];
if (is_float($value)) {
$current[$fieldName] = sprintf('%.12f', $value);
}
if (!is_float($value)) {
$current[$fieldName] = (string) $value;
}
}
}
return $current;
}
}

View File

@ -76,12 +76,12 @@ class IndexController extends Controller
}
// add a split for the (future) v2 release.
$periods = [];
$groups = [];
$subTitle = 'TODO page subtitle in v2';
$periods = [];
$groups = [];
$subTitle = 'TODO page subtitle in v2';
$subTitleIcon = config('firefly.transactionIconsByType.' . $objectType);
$types = config('firefly.transactionTypesByType.' . $objectType);
$subTitleIcon = config('firefly.transactionIconsByType.'.$objectType);
$types = config('firefly.transactionTypesByType.'.$objectType);
$page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
@ -97,31 +97,31 @@ class IndexController extends Controller
}
[$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);
$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 = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)
->setTypes($types)
->setLimit($pageSize)
->setPage($page)
->withBudgetInformation()
->withCategoryInformation()
->withAccountInformation()
->withAttachmentInformation();
$groups = $collector->getPaginatedGroups();
->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'));
}
@ -132,8 +132,8 @@ class IndexController extends Controller
*/
public function indexAll(Request $request, string $objectType)
{
$subTitleIcon = config('firefly.transactionIconsByType.' . $objectType);
$types = config('firefly.transactionTypesByType.' . $objectType);
$subTitleIcon = config('firefly.transactionIconsByType.'.$objectType);
$types = config('firefly.transactionTypesByType.'.$objectType);
$page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
$path = route('transactions.index.all', [$objectType]);
@ -141,20 +141,21 @@ class IndexController extends Controller
$start = null === $first ? new Carbon() : $first->date;
$last = $this->repository->getLast();
$end = null !== $last ? $last->date : today(config('app.timezone'));
$subTitle = (string)trans('firefly.all_' . $objectType);
$subTitle = (string)trans('firefly.all_'.$objectType);
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)
->setTypes($types)
->setLimit($pageSize)
->setPage($page)
->withAccountInformation()
->withBudgetInformation()
->withCategoryInformation()
->withAttachmentInformation();
$groups = $collector->getPaginatedGroups();
->setTypes($types)
->setLimit($pageSize)
->setPage($page)
->withAccountInformation()
->withBudgetInformation()
->withCategoryInformation()
->withAttachmentInformation()
;
$groups = $collector->getPaginatedGroups();
$groups->setPath($path);
return view('transactions.index', compact('subTitle', 'objectType', 'subTitleIcon', 'groups', 'start', 'end'));

View File

@ -23,12 +23,12 @@ declare(strict_types=1);
namespace FireflyIII\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Passport;
use Laravel\Sanctum\Sanctum;
use Illuminate\Support\Facades\Blade;
/**
* Class AppServiceProvider
@ -52,12 +52,13 @@ class AppServiceProvider extends ServiceProvider
return response()
->json($value)
->withHeaders($headers);
->withHeaders($headers)
;
});
// blade extension
Blade::directive('activeXRoutePartial', function (string $route) {
$name = \Route::getCurrentRoute()->getName() ?? '';
$name = \Route::getCurrentRoute()->getName() ?? '';
if (str_contains($name, $route)) {
return 'menu-open';
}
@ -65,7 +66,7 @@ class AppServiceProvider extends ServiceProvider
return '';
});
Blade::if('partialroute', function (string $route) {
$name = \Route::getCurrentRoute()->getName() ?? '';
$name = \Route::getCurrentRoute()->getName() ?? '';
if (str_contains($name, $route)) {
return true;
}

View File

@ -123,7 +123,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface
public function setUser(null|Authenticatable|User $user): void
{
if ($user instanceof user) {
if ($user instanceof User) {
$this->user = $user;
}
}

View File

@ -29,6 +29,10 @@ use Illuminate\Support\Facades\Log;
class BillDateCalculator
{
// #8401 we start keeping track of the diff in periods, because if it can't jump over a period (happens often in February)
// we can force the process along.
private int $diffInMonths = 0;
/**
* Returns the dates a bill needs to be paid.
*
@ -43,12 +47,16 @@ class BillDateCalculator
Log::debug(sprintf('Dates must be between %s and %s.', $earliest->format('Y-m-d'), $latest->format('Y-m-d')));
Log::debug(sprintf('Bill started on %s, period is "%s", skip is %d, last paid = "%s".', $billStart->format('Y-m-d'), $period, $skip, $lastPaid?->format('Y-m-d')));
$daysUntilEOM = app('navigation')->daysUntilEndOfMonth($billStart);
Log::debug(sprintf('For bill start, days until end of month is %d', $daysUntilEOM));
$set = new Collection();
$currentStart = clone $earliest;
// 2023-06-23 subDay to fix 7655
$currentStart->subDay();
$loop = 0;
Log::debug('Start of loop');
while ($currentStart <= $latest) {
Log::debug(sprintf('Current start is %s', $currentStart->format('Y-m-d')));
@ -79,8 +87,23 @@ class BillDateCalculator
$set->push(clone $nextExpectedMatch);
}
// #8401
// a little check for when the day of the bill (ie 30th of the month) is not possible in
// the next expected month because that month has only 28 days (i.e. february).
// this applies to leap years as well.
if ($daysUntilEOM < 4) {
$nextUntilEOM = app('navigation')->daysUntilEndOfMonth($nextExpectedMatch);
$diffEOM = $daysUntilEOM - $nextUntilEOM;
if ($diffEOM > 0) {
Log::debug(sprintf('Bill start is %d days from the end of the month. nextExceptedMatch is %d days from the end of the month.', $daysUntilEOM, $nextUntilEOM));
$nextExpectedMatch->subDays(1);
Log::debug(sprintf('Subtract %d days from next expected match, which is now %s', $diffEOM, $nextExpectedMatch->format('Y-m-d')));
}
}
// 2023-10
// for the next loop, go to end of period, THEN add day.
Log::debug('Add one day to nextExpectedMatch/currentStart.');
$nextExpectedMatch->addDay();
$currentStart = clone $nextExpectedMatch;
@ -117,8 +140,13 @@ class BillDateCalculator
return $billStartDate;
}
$steps = app('navigation')->diffInPeriods($period, $skip, $earliest, $billStartDate);
$result = clone $billStartDate;
$steps = app('navigation')->diffInPeriods($period, $skip, $earliest, $billStartDate);
if ($steps === $this->diffInMonths) {
Log::debug(sprintf('Steps is %d, which is the same as diffInMonths (%d), so we add another 1.', $steps, $this->diffInMonths));
++$steps;
}
$this->diffInMonths = $steps;
$result = clone $billStartDate;
if ($steps > 0) {
--$steps;
Log::debug(sprintf('Steps is %d, because addPeriod already adds 1.', $steps));

View File

@ -302,6 +302,13 @@ class Navigation
return $currentEnd;
}
public function daysUntilEndOfMonth(Carbon $date): int
{
$endOfMonth = $date->copy()->endOfMonth();
return $date->diffInDays($endOfMonth);
}
public function diffInPeriods(string $period, int $skip, Carbon $beginning, Carbon $end): int
{
Log::debug(sprintf(

View File

@ -33,7 +33,7 @@ use FireflyIII\Models\TransactionJournal;
*/
class SetNotes implements ActionInterface
{
private RuleACtion $action;
private RuleAction $action;
/**
* TriggerInterface constructor.

View File

@ -21,6 +21,25 @@
declare(strict_types=1);
use FireflyIII\Providers\AccountServiceProvider;
use FireflyIII\Providers\AdminServiceProvider;
use FireflyIII\Providers\AppServiceProvider;
use FireflyIII\Providers\AttachmentServiceProvider;
use FireflyIII\Providers\BillServiceProvider;
use FireflyIII\Providers\BudgetServiceProvider;
use FireflyIII\Providers\CategoryServiceProvider;
use FireflyIII\Providers\CurrencyServiceProvider;
use FireflyIII\Providers\EventServiceProvider;
use FireflyIII\Providers\FireflyServiceProvider;
use FireflyIII\Providers\JournalServiceProvider;
use FireflyIII\Providers\PiggyBankServiceProvider;
use FireflyIII\Providers\RecurringServiceProvider;
use FireflyIII\Providers\RouteServiceProvider;
use FireflyIII\Providers\RuleGroupServiceProvider;
use FireflyIII\Providers\RuleServiceProvider;
use FireflyIII\Providers\SearchServiceProvider;
use FireflyIII\Providers\SessionServiceProvider;
use FireflyIII\Providers\TagServiceProvider;
use FireflyIII\Support\Facades\AccountForm;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\CurrencyForm;
@ -31,6 +50,67 @@ use FireflyIII\Support\Facades\PiggyBankForm;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Facades\RuleForm;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Auth\AuthServiceProvider;
use Illuminate\Auth\Passwords\PasswordResetServiceProvider;
use Illuminate\Broadcasting\BroadcastServiceProvider;
use Illuminate\Bus\BusServiceProvider;
use Illuminate\Cache\CacheServiceProvider;
use Illuminate\Cookie\CookieServiceProvider;
use Illuminate\Database\DatabaseServiceProvider;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Encryption\EncryptionServiceProvider;
use Illuminate\Filesystem\FilesystemServiceProvider;
use Illuminate\Foundation\Providers\ConsoleSupportServiceProvider;
use Illuminate\Foundation\Providers\FoundationServiceProvider;
use Illuminate\Hashing\HashServiceProvider;
use Illuminate\Mail\MailServiceProvider;
use Illuminate\Notifications\NotificationServiceProvider;
use Illuminate\Pagination\PaginationServiceProvider;
use Illuminate\Pipeline\PipelineServiceProvider;
use Illuminate\Queue\QueueServiceProvider;
use Illuminate\Redis\RedisServiceProvider;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Cookie;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Str;
use Illuminate\Translation\TranslationServiceProvider;
use Illuminate\Validation\ValidationServiceProvider;
use Illuminate\View\ViewServiceProvider;
use PragmaRX\Google2FALaravel\Facade;
use Spatie\Html\Facades\Html;
use TwigBridge\Facade\Twig;
use TwigBridge\ServiceProvider;
return [
'name' => envNonEmpty('APP_NAME', 'Firefly III'),
@ -44,94 +124,94 @@ return [
'cipher' => 'AES-256-CBC',
'providers' => [
// Laravel Framework Service Providers...
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
FireflyIII\Providers\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
AuthServiceProvider::class,
BroadcastServiceProvider::class,
BusServiceProvider::class,
CacheServiceProvider::class,
ConsoleSupportServiceProvider::class,
CookieServiceProvider::class,
DatabaseServiceProvider::class,
EncryptionServiceProvider::class,
FilesystemServiceProvider::class,
FoundationServiceProvider::class,
HashServiceProvider::class,
MailServiceProvider::class,
NotificationServiceProvider::class,
PaginationServiceProvider::class,
PipelineServiceProvider::class,
QueueServiceProvider::class,
RedisServiceProvider::class,
PasswordResetServiceProvider::class,
SessionServiceProvider::class,
TranslationServiceProvider::class,
ValidationServiceProvider::class,
ViewServiceProvider::class,
// Package Service Providers...
// Application Service Providers...
FireflyIII\Providers\AppServiceProvider::class,
AppServiceProvider::class,
FireflyIII\Providers\AuthServiceProvider::class,
// FireflyIII\Providers\BroadcastServiceProvider::class,
FireflyIII\Providers\EventServiceProvider::class,
FireflyIII\Providers\RouteServiceProvider::class,
EventServiceProvider::class,
RouteServiceProvider::class,
// own stuff:
PragmaRX\Google2FALaravel\ServiceProvider::class,
TwigBridge\ServiceProvider::class,
ServiceProvider::class,
// More service providers.
FireflyIII\Providers\AccountServiceProvider::class,
FireflyIII\Providers\AttachmentServiceProvider::class,
FireflyIII\Providers\BillServiceProvider::class,
FireflyIII\Providers\BudgetServiceProvider::class,
FireflyIII\Providers\CategoryServiceProvider::class,
FireflyIII\Providers\CurrencyServiceProvider::class,
FireflyIII\Providers\FireflyServiceProvider::class,
FireflyIII\Providers\JournalServiceProvider::class,
FireflyIII\Providers\PiggyBankServiceProvider::class,
FireflyIII\Providers\RuleServiceProvider::class,
FireflyIII\Providers\RuleGroupServiceProvider::class,
FireflyIII\Providers\SearchServiceProvider::class,
FireflyIII\Providers\TagServiceProvider::class,
FireflyIII\Providers\AdminServiceProvider::class,
FireflyIII\Providers\RecurringServiceProvider::class,
AccountServiceProvider::class,
AttachmentServiceProvider::class,
BillServiceProvider::class,
BudgetServiceProvider::class,
CategoryServiceProvider::class,
CurrencyServiceProvider::class,
FireflyServiceProvider::class,
JournalServiceProvider::class,
PiggyBankServiceProvider::class,
RuleServiceProvider::class,
RuleGroupServiceProvider::class,
SearchServiceProvider::class,
TagServiceProvider::class,
AdminServiceProvider::class,
RecurringServiceProvider::class,
],
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
'Html' => Spatie\Html\Facades\Html::class,
'App' => App::class,
'Artisan' => Artisan::class,
'Auth' => Auth::class,
'Blade' => Blade::class,
'Broadcast' => Broadcast::class,
'Bus' => Bus::class,
'Cache' => Cache::class,
'Config' => Config::class,
'Cookie' => Cookie::class,
'Crypt' => Crypt::class,
'DB' => DB::class,
'Eloquent' => Model::class,
'Event' => Event::class,
'File' => File::class,
'Gate' => Gate::class,
'Hash' => Hash::class,
'Lang' => Lang::class,
'Log' => Log::class,
'Mail' => Mail::class,
'Notification' => Notification::class,
'Password' => Password::class,
'Queue' => Queue::class,
'Redirect' => Redirect::class,
'Redis' => Redis::class,
'Request' => Request::class,
'Response' => Response::class,
'Route' => Route::class,
'Schema' => Schema::class,
'Session' => Session::class,
'Storage' => Storage::class,
'URL' => URL::class,
'Validator' => Validator::class,
'View' => View::class,
'Html' => Html::class,
'Preferences' => Preferences::class,
'FireflyConfig' => FireflyConfig::class,
'Navigation' => Navigation::class,
@ -142,12 +222,12 @@ return [
'AccountForm' => AccountForm::class,
'PiggyBankForm' => PiggyBankForm::class,
'RuleForm' => RuleForm::class,
'Google2FA' => PragmaRX\Google2FALaravel\Facade::class,
'Twig' => TwigBridge\Facade\Twig::class,
'Google2FA' => Facade::class,
'Twig' => Twig::class,
'Arr' => Illuminate\Support\Arr::class,
'Http' => Illuminate\Support\Facades\Http::class,
'Str' => Illuminate\Support\Str::class,
'Arr' => Arr::class,
'Http' => Http::class,
'Str' => Str::class,
],
'asset_url' => env('ASSET_URL', null),

View File

@ -20,6 +20,7 @@
*/
declare(strict_types=1);
use FireflyIII\User;
if ('ldap' === strtolower((string)env('AUTHENTICATION_GUARD'))) {
exit('LDAP is no longer supported by Firefly III v5.7+. Sorry about that. You will have to switch to "remote_user_guard", and use tools like Authelia or Keycloak to use LDAP together with Firefly III.');
@ -96,11 +97,11 @@ return [
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => FireflyIII\User::class,
'model' => User::class,
],
'remote_user_provider' => [
'driver' => 'remote_user_provider',
'model' => FireflyIII\User::class,
'model' => User::class,
],
],

View File

@ -21,6 +21,8 @@
*/
declare(strict_types=1);
use Diglactic\Breadcrumbs\Generator;
use Diglactic\Breadcrumbs\Manager;
return [
/*
@ -90,8 +92,8 @@ return [
*/
// Manager
'manager-class' => Diglactic\Breadcrumbs\Manager::class,
'manager-class' => Manager::class,
// Generator
'generator-class' => Diglactic\Breadcrumbs\Generator::class,
'generator-class' => Generator::class,
];

View File

@ -20,6 +20,7 @@
*/
declare(strict_types=1);
use FireflyIII\User;
return [
/*
@ -51,7 +52,7 @@ return [
],
'stripe' => [
'model' => FireflyIII\User::class,
'model' => User::class,
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],

16
package-lock.json generated
View File

@ -14,7 +14,6 @@
"chart.js": "^4.4.0",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-chart-sankey": "^0.12.0",
"dark-editable": "github:DarKsandr/dark-editable",
"date-fns": "^3.2.0",
"i18next": "^23.7.16",
"i18next-chained-backend": "^4.6.2",
@ -32,9 +31,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz",
"integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==",
"version": "7.23.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz",
"integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@ -592,9 +591,6 @@
"node-fetch": "^2.6.12"
}
},
"node_modules/dark-editable": {
"resolved": "git+ssh://git@github.com/DarKsandr/dark-editable.git#8601f10ef5a49ff9ad6f1c0921d523f17f174036"
},
"node_modules/date-fns": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.2.0.tgz",
@ -663,9 +659,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"dev": true,
"funding": [
{

View File

@ -22,7 +22,6 @@
"chart.js": "^4.4.0",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-chart-sankey": "^0.12.0",
"dark-editable": "github:DarKsandr/dark-editable",
"date-fns": "^3.2.0",
"i18next": "^23.7.16",
"i18next-chained-backend": "^4.6.2",

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{f as n}from"./vendor-fca45885.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-0ba1deae.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-43e05102.js";class p{list(a){return s.get("/api/v2/transactions",{params:a})}show(a,t){return s.get("/api/v2/transactions/"+a,{params:t})}}export{p as G};
import{a as s}from"./format-money-a71f277a.js";class p{list(a){return s.get("/api/v2/transactions",{params:a})}show(a,t){return s.get("/api/v2/transactions/"+a,{params:t})}}export{p as G};

View File

@ -1 +1 @@
import{a as s}from"./format-money-43e05102.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-a71f277a.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 +0,0 @@
import{d as c,f as d}from"./format-money-43e05102.js";import{f,i as r}from"./vendor-fca45885.js";import{G as p}from"./get-a29f6df7.js";let g=function(){return{notifications:{error:{show:!1,text:"",url:""},success:{show:!1,text:"",url:""},wait:{show:!1,text:""}},transactions:[],totalPages:1,perPage:50,page:1,tableColumns:{description:{enabled:!0},source:{enabled:!0},destination:{enabled:!0},amount:{enabled:!0}},formatMoney(a,i){return d(a,i)},format(a){return f(a,r.t("config.date_time_fns"))},init(){this.notifications.wait.show=!0,this.notifications.wait.text=r.t("firefly.wait_loading_data"),this.getTransactions(this.page)},getTransactions(a){new p().list({page:a}).then(t=>{this.parseTransactions(t.data.data),this.totalPages=t.data.meta.pagination.total_pages,this.perPage=t.data.meta.pagination.per_page,this.page=t.data.meta.pagination.current_page}).catch(t=>{this.notifications.wait.show=!1,this.notifications.error.show=!0,this.notifications.error.text=t.response.data.message})},parseTransactions(a){for(let i in a)if(a.hasOwnProperty(i)){let t=a[i],u=t.attributes.transactions.length>1,s=!0;for(let o in t.attributes.transactions)if(t.attributes.transactions.hasOwnProperty(o)){let e=t.attributes.transactions[o];e.split=u,e.firstSplit=s,e.group_title=t.attributes.group_title,e.id=t.id,e.created_at=t.attributes.created_at,e.updated_at=t.attributes.updated_at,e.user=t.attributes.user,e.user_group=t.attributes.user_group,s=!1,console.log(e),this.transactions.push(e)}}this.notifications.wait.show=!1}}},n={index:g,dates:c};function l(){Object.keys(n).forEach(a=>{console.log(`Loading page component "${a}"`);let i=n[a]();Alpine.data(a,()=>i)}),Alpine.start()}document.addEventListener("firefly-iii-bootstrapped",()=>{console.log("Loaded through event listener."),l()});window.bootstrapped&&(console.log("Loaded through window variable."),l());

View File

@ -0,0 +1 @@
import{d as c,f as d}from"./format-money-a71f277a.js";import{f as p,i as n}from"./vendor-0ba1deae.js";import{G as f}from"./get-971543fd.js";let g=function(){return{notifications:{error:{show:!1,text:"",url:""},success:{show:!1,text:"",url:""},wait:{show:!1,text:""}},transactions:[],totalPages:1,perPage:50,page:1,tableColumns:{description:{enabled:!0},source:{enabled:!0},destination:{enabled:!0},amount:{enabled:!0}},formatMoney(t,i){return d(t,i)},format(t){return p(t,n.t("config.date_time_fns"))},init(){this.notifications.wait.show=!0,this.notifications.wait.text=n.t("firefly.wait_loading_data"),this.getTransactions(this.page)},getTransactions(t){const i=window.location.href.split("/"),a=i[i.length-1];new f().list({page:t,type:a}).then(s=>{this.parseTransactions(s.data.data),this.totalPages=s.data.meta.pagination.total_pages,this.perPage=s.data.meta.pagination.per_page,this.page=s.data.meta.pagination.current_page}).catch(s=>{this.notifications.wait.show=!1,this.notifications.error.show=!0,this.notifications.error.text=s.response.data.message})},parseTransactions(t){for(let i in t)if(t.hasOwnProperty(i)){let a=t[i],o=a.attributes.transactions.length>1,s=!0;for(let r in a.attributes.transactions)if(a.attributes.transactions.hasOwnProperty(r)){let e=a.attributes.transactions[r];e.split=o,e.firstSplit=s,e.group_title=a.attributes.group_title,e.id=a.id,e.created_at=a.attributes.created_at,e.updated_at=a.attributes.updated_at,e.user=a.attributes.user,e.user_group=a.attributes.user_group,s=!1,console.log(e),this.transactions.push(e)}}this.notifications.wait.show=!1}}},l={index:g,dates:c};function u(){Object.keys(l).forEach(t=>{console.log(`Loading page component "${t}"`);let i=l[t]();Alpine.data(t,()=>i)}),Alpine.start()}document.addEventListener("firefly-iii-bootstrapped",()=>{console.log("Loaded through event listener."),u()});window.bootstrapped&&(console.log("Loaded through window variable."),u());

View File

@ -1 +1 @@
import{c as r}from"./create-empty-split-fb5bda92.js";import{f as o}from"./vendor-fca45885.js";function c(a){let n=[];for(let i in a)if(a.hasOwnProperty(i)){let e=a[i],t=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=o(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{c as p};
import{c as r}from"./create-empty-split-b2c15454.js";import{f as o}from"./vendor-0ba1deae.js";function c(a){let n=[];for(let i in a)if(a.hasOwnProperty(i)){let e=a[i],t=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=o(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{c as p};

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{d,f as p}from"./format-money-43e05102.js";import{f as c,i as o}from"./vendor-fca45885.js";import{G as l}from"./get-a29f6df7.js";import{p as f}from"./parse-downloaded-splits-4afea167.js";import"./create-empty-split-fb5bda92.js";let h=function(){return{notifications:{error:{show:!1,text:"",url:""},success:{show:!1,text:"",url:""},wait:{show:!1,text:""}},groupProperties:{id:0,transactionType:"",transactionTypeTranslated:"",title:"",date:new Date},dateFields:["book_date","due_date","interest_date","invoice_date","payment_date","process_date"],metaFields:["external_id","internal_reference","sepa_batch_id","sepa_ct_id","sepa_ct_op","sepa_db","sepa_country","sepa_cc","sepa_ep","sepa_ci","external_url"],amounts:{},entries:[],pageProperties:{},formatMoney(e,s){return p(e,s)},format(e){return c(e,o.t("config.date_time_fns"))},init(){this.notifications.wait.show=!0,this.notifications.wait.text=o.t("firefly.wait_loading_data");const e=window.location.href.split("/"),s=parseInt(e[e.length-1]);new l().show(s,{}).then(t=>{const i=t.data.data;this.groupProperties.id=parseInt(i.id),this.groupProperties.transactionType=i.attributes.transactions[0].type,this.groupProperties.transactionTypeTranslated=o.t("firefly."+i.attributes.transactions[0].type),this.groupProperties.title=i.attributes.title??i.attributes.transactions[0].description,this.entries=f(i.attributes.transactions),this.notifications.wait.show=!1}).then(()=>{for(let t in this.entries)if(this.entries.hasOwnProperty(t)){const i=this.entries[t].currency_code,a=this.entries[t].foreign_currency_code;this.amounts[i]===void 0&&(this.amounts[i]=0,this.amounts[i]+=parseFloat(this.entries[t].amount)),a!==null&&a!==""&&this.amounts[a]===void 0&&(this.amounts[a]=0,this.amounts[a]+=parseFloat(this.entries[t].foreign_amount)),parseInt(t)===0&&(this.groupProperties.date=this.entries[t].date)}}).catch(t=>{this.notifications.error.show=!0,this.notifications.error.text=t.message})}}},r={show:h,dates:d};function n(){Object.keys(r).forEach(e=>{console.log(`Loading page component "${e}"`);let s=r[e]();Alpine.data(e,()=>s)}),Alpine.start()}document.addEventListener("firefly-iii-bootstrapped",()=>{console.log("Loaded through event listener."),n()});window.bootstrapped&&(console.log("Loaded through window variable."),n());

View File

@ -0,0 +1 @@
.dark-editable-element{border-bottom:dashed 1px #0088cc;text-decoration:none;cursor:pointer}.dark-editable-element-disabled{border-bottom:none;cursor:default}.dark-editable-element-empty{font-style:italic;color:#d14}.dark-editable{max-width:none}.dark-editable-loader{font-size:5px;left:50%;top:50%;width:1em;height:1em;border-radius:50%;position:relative;text-indent:-9999em;-webkit-animation:load5 1.1s infinite ease;animation:load5 1.1s infinite ease;-webkit-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0)}@-webkit-keyframes load5{0%,to{box-shadow:0 -2.6em #000,1.8em -1.8em #0003,2.5em 0 #0003,1.75em 1.75em #0003,0 2.5em #0003,-1.8em 1.8em #0003,-2.6em 0 #00000080,-1.8em -1.8em #000000b3}12.5%{box-shadow:0 -2.6em #000000b3,1.8em -1.8em #000,2.5em 0 #0003,1.75em 1.75em #0003,0 2.5em #0003,-1.8em 1.8em #0003,-2.6em 0 #0003,-1.8em -1.8em #00000080}25%{box-shadow:0 -2.6em #00000080,1.8em -1.8em #000000b3,2.5em 0 #000,1.75em 1.75em #0003,0 2.5em #0003,-1.8em 1.8em #0003,-2.6em 0 #0003,-1.8em -1.8em #0003}37.5%{box-shadow:0 -2.6em #0003,1.8em -1.8em #00000080,2.5em 0 #000000b3,1.75em 1.75em #000,0 2.5em #0003,-1.8em 1.8em #0003,-2.6em 0 #0003,-1.8em -1.8em #0003}50%{box-shadow:0 -2.6em #0003,1.8em -1.8em #0003,2.5em 0 #00000080,1.75em 1.75em #000000b3,0 2.5em #000,-1.8em 1.8em #0003,-2.6em 0 #0003,-1.8em -1.8em #0003}62.5%{box-shadow:0 -2.6em #0003,1.8em -1.8em #0003,2.5em 0 #0003,1.75em 1.75em #00000080,0 2.5em #000000b3,-1.8em 1.8em #000,-2.6em 0 #0003,-1.8em -1.8em #0003}75%{box-shadow:0 -2.6em #0003,1.8em -1.8em #0003,2.5em 0 #0003,1.75em 1.75em #0003,0 2.5em #00000080,-1.8em 1.8em #000000b3,-2.6em 0 #000,-1.8em -1.8em #0003}87.5%{box-shadow:0 -2.6em #0003,1.8em -1.8em #0003,2.5em 0 #0003,1.75em 1.75em #0003,0 2.5em #0003,-1.8em 1.8em #00000080,-2.6em 0 #000000b3,-1.8em -1.8em #000}}@keyframes load5{0%,to{box-shadow:0 -2.6em #000,1.8em -1.8em #0003,2.5em 0 #0003,1.75em 1.75em #0003,0 2.5em #0003,-1.8em 1.8em #0003,-2.6em 0 #00000080,-1.8em -1.8em #000000b3}12.5%{box-shadow:0 -2.6em #000000b3,1.8em -1.8em #000,2.5em 0 #0003,1.75em 1.75em #0003,0 2.5em #0003,-1.8em 1.8em #0003,-2.6em 0 #0003,-1.8em -1.8em #00000080}25%{box-shadow:0 -2.6em #00000080,1.8em -1.8em #000000b3,2.5em 0 #000,1.75em 1.75em #0003,0 2.5em #0003,-1.8em 1.8em #0003,-2.6em 0 #0003,-1.8em -1.8em #0003}37.5%{box-shadow:0 -2.6em #0003,1.8em -1.8em #00000080,2.5em 0 #000000b3,1.75em 1.75em #000,0 2.5em #0003,-1.8em 1.8em #0003,-2.6em 0 #0003,-1.8em -1.8em #0003}50%{box-shadow:0 -2.6em #0003,1.8em -1.8em #0003,2.5em 0 #00000080,1.75em 1.75em #000000b3,0 2.5em #000,-1.8em 1.8em #0003,-2.6em 0 #0003,-1.8em -1.8em #0003}62.5%{box-shadow:0 -2.6em #0003,1.8em -1.8em #0003,2.5em 0 #0003,1.75em 1.75em #00000080,0 2.5em #000000b3,-1.8em 1.8em #000,-2.6em 0 #0003,-1.8em -1.8em #0003}75%{box-shadow:0 -2.6em #0003,1.8em -1.8em #0003,2.5em 0 #0003,1.75em 1.75em #0003,0 2.5em #00000080,-1.8em 1.8em #000000b3,-2.6em 0 #000,-1.8em -1.8em #0003}87.5%{box-shadow:0 -2.6em #0003,1.8em -1.8em #0003,2.5em 0 #0003,1.75em 1.75em #0003,0 2.5em #0003,-1.8em 1.8em #00000080,-2.6em 0 #000000b3,-1.8em -1.8em #000}}

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,50 +1,50 @@
{
"_create-empty-split-fb5bda92.js": {
"file": "assets/create-empty-split-fb5bda92.js",
"_create-empty-split-b2c15454.js": {
"file": "assets/create-empty-split-b2c15454.js",
"imports": [
"_vendor-fca45885.js"
"_vendor-0ba1deae.js"
],
"integrity": "sha384-hhKUcZf3xSLJgwkzufZGpLjOUeD5gulL71KQu72SoGIUVLPU3EIBT+ICT/znc01b"
"integrity": "sha384-LPTZ0hEPbyGHGP76xXmeyTW22lbs5CNJU8wwXx3SOeUf6jDNYVoSSEe05QdKYKQ0"
},
"_format-money-43e05102.js": {
"file": "assets/format-money-43e05102.js",
"_format-money-a71f277a.js": {
"file": "assets/format-money-a71f277a.js",
"imports": [
"_vendor-fca45885.js"
"_vendor-0ba1deae.js"
],
"integrity": "sha384-/lP2o0Yj4DAtc93ChQvqF5ctDGjH6jakuIZcEQBICvDn/YtSDaPr7PzrZd4w53jV"
"integrity": "sha384-NX1AQwixTtbRYpnSgnTwE6OzMgA0zTe/BOQp4tA+sMFZ7A/Z/8AM85Ia0343u020"
},
"_get-94678769.js": {
"file": "assets/get-94678769.js",
"_get-971543fd.js": {
"file": "assets/get-971543fd.js",
"imports": [
"_format-money-43e05102.js"
"_format-money-a71f277a.js"
],
"integrity": "sha384-/nzI9V1buTN8n1p1uL9UOxOasx6+q6kTpiwDCaZZHBCVrrnFqWSCQCHAPQFcqYGB"
"integrity": "sha384-kRyC3Ngpm92ejZXmYiYd9NWyzp47VMYlXB1QCIMNJf/5g0CM3/id1tMx1wM7sWop"
},
"_get-a29f6df7.js": {
"file": "assets/get-a29f6df7.js",
"_get-c4c00bed.js": {
"file": "assets/get-c4c00bed.js",
"imports": [
"_format-money-43e05102.js"
"_format-money-a71f277a.js"
],
"integrity": "sha384-QRZtv6rXwaf1k9JOU1p1T42Nn2t5En83j/8rbh00dJ+1sSoNLD0maS8FkPKVRbpE"
"integrity": "sha384-KLoBbucwHe+WIBcGunE9+tql5dx39uDkibZZDSq9X9m25oHNXRtVbjfI8ljmGSYe"
},
"_parse-downloaded-splits-4afea167.js": {
"file": "assets/parse-downloaded-splits-4afea167.js",
"_parse-downloaded-splits-19845c1d.js": {
"file": "assets/parse-downloaded-splits-19845c1d.js",
"imports": [
"_create-empty-split-fb5bda92.js",
"_vendor-fca45885.js"
"_create-empty-split-b2c15454.js",
"_vendor-0ba1deae.js"
],
"integrity": "sha384-rTgD6HzwbdSD/qoN3b4vipowqanriBYLYnORI3F23R+uFbr1kyZIE1AHYA9oe2qO"
"integrity": "sha384-fvvNIwjZ9BkUU+LRImRK606sDo4vJs8/Z58Vm7T46vAl66xckgxX+GVjsltwkhus"
},
"_splice-errors-into-transactions-9c9b99c0.js": {
"file": "assets/splice-errors-into-transactions-9c9b99c0.js",
"_splice-errors-into-transactions-995deb6d.js": {
"file": "assets/splice-errors-into-transactions-995deb6d.js",
"imports": [
"_format-money-43e05102.js",
"_get-94678769.js",
"_vendor-fca45885.js"
"_format-money-a71f277a.js",
"_get-c4c00bed.js",
"_vendor-0ba1deae.js"
],
"integrity": "sha384-6QHZSbNE9w8hPmdvtcEi1wqimo8+lyJWXBaYaX7/c8qoveRKX5A2WZVhqb7bMRkV"
"integrity": "sha384-xy9kpKxM6h0137K3N9ZyXSqNwsD3sk+KrMm3tWivk/WtgXy+fxjgk08YSX7Zgc+d"
},
"_vendor-fca45885.js": {
"_vendor-0ba1deae.js": {
"assets": [
"assets/layers-1dbbe9d0.png",
"assets/layers-2x-066daca8.png",
@ -53,8 +53,8 @@
"css": [
"assets/vendor-49001d3f.css"
],
"file": "assets/vendor-fca45885.js",
"integrity": "sha384-xc7wHYzykc3pL46xJ1C3dJ/yReuYh0aAcdywRaE8sPW9QiKDkmpdip8ksd8qWp4b"
"file": "assets/vendor-0ba1deae.js",
"integrity": "sha384-9W7xmHUksAccX1L6MeAPa77/siJb7Gka71e+TA2dfgu4VV7xUgeqeyTCIShNLB4b"
},
"node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf": {
"file": "assets/fa-brands-400-5656d596.ttf",
@ -102,68 +102,76 @@
"integrity": "sha384-wg83fCOXjBtqzFAWhTL9Sd9vmLUNhfEEzfmNUX9zwv2igKlz/YQbdapF4ObdxF+R"
},
"resources/assets/v2/pages/dashboard/dashboard.js": {
"file": "assets/dashboard-2ad8d866.js",
"file": "assets/dashboard-60d58953.js",
"imports": [
"_format-money-43e05102.js",
"_vendor-fca45885.js",
"_get-a29f6df7.js",
"_get-94678769.js"
"_format-money-a71f277a.js",
"_vendor-0ba1deae.js",
"_get-971543fd.js",
"_get-c4c00bed.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/dashboard/dashboard.js",
"integrity": "sha384-drEV1+zgjVGIfpdo1BovDls72a22l303XFZq/n0BE9VMTuydOYDBBgEqjM1GgwQD"
"integrity": "sha384-KGiwIVYJZwLUgiraYW27lW+R5wW0LW30/Z0z6UA1SkfjjoeDXm1DTcSoUtMhltaG"
},
"resources/assets/v2/pages/transactions/create.js": {
"file": "assets/create-46b63de1.js",
"file": "assets/create-dd6750e8.js",
"imports": [
"_format-money-43e05102.js",
"_create-empty-split-fb5bda92.js",
"_splice-errors-into-transactions-9c9b99c0.js",
"_vendor-fca45885.js",
"_get-94678769.js"
"_format-money-a71f277a.js",
"_create-empty-split-b2c15454.js",
"_splice-errors-into-transactions-995deb6d.js",
"_vendor-0ba1deae.js",
"_get-c4c00bed.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/transactions/create.js",
"integrity": "sha384-qacGHBXX2jXhiYdstJdUk5SBbdF1y8uj9RTdWetpmATLMNSjetRemPMk4PJoGxgZ"
"integrity": "sha384-rrUkO07lnTiSQ+5SPMwOLmnu2J+5oQmZFKfsf3ApkQG9ct3qrwBocQ9f0H5cftv2"
},
"resources/assets/v2/pages/transactions/edit.js": {
"file": "assets/edit-b45ed381.js",
"file": "assets/edit-74c97acb.js",
"imports": [
"_format-money-43e05102.js",
"_get-a29f6df7.js",
"_parse-downloaded-splits-4afea167.js",
"_splice-errors-into-transactions-9c9b99c0.js",
"_vendor-fca45885.js",
"_create-empty-split-fb5bda92.js",
"_get-94678769.js"
"_format-money-a71f277a.js",
"_get-971543fd.js",
"_parse-downloaded-splits-19845c1d.js",
"_splice-errors-into-transactions-995deb6d.js",
"_vendor-0ba1deae.js",
"_create-empty-split-b2c15454.js",
"_get-c4c00bed.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/transactions/edit.js",
"integrity": "sha384-CoTAYJsk4/69IMBbgV50+i5mK+pANKXl0hLH9nqErgit1WDdBK6U4VimDsq5fvMf"
"integrity": "sha384-gU2Nw6D5i8slOZwzR1Tlpf82FvESkn7cNoT24Pk/VJzqY3JQo3Mwr0vb09swPwEZ"
},
"resources/assets/v2/pages/transactions/index.js": {
"file": "assets/index-8df22335.js",
"file": "assets/index-cf70f728.js",
"imports": [
"_format-money-43e05102.js",
"_vendor-fca45885.js",
"_get-a29f6df7.js"
"_format-money-a71f277a.js",
"_vendor-0ba1deae.js",
"_get-971543fd.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/transactions/index.js",
"integrity": "sha384-UcghdFUsV8Bp01abyYn4u6sI/mWjXuY677vyQ76p1gVaMl9pVMGjdPa8Cd9Lg6M5"
"integrity": "sha384-aCoGtjOpn/zM6UGPc4bOZEGISPRJd8Vd2l/7VbFnJzjrSzMRIv5hhU1vpvw0JoTh"
},
"resources/assets/v2/pages/transactions/show.css": {
"file": "assets/show-8b1429e5.css",
"src": "resources/assets/v2/pages/transactions/show.css",
"integrity": "sha384-D0+ADFJ1uVTJKTOYOVIjDXI7vzboM0WuC0AjpA2jhwEXxAVGnZTzqFZPuSYWBWiL"
},
"resources/assets/v2/pages/transactions/show.js": {
"file": "assets/show-61e79d17.js",
"css": [
"assets/show-8b1429e5.css"
],
"file": "assets/show-19f421da.js",
"imports": [
"_format-money-43e05102.js",
"_vendor-fca45885.js",
"_get-a29f6df7.js",
"_parse-downloaded-splits-4afea167.js",
"_create-empty-split-fb5bda92.js"
"_format-money-a71f277a.js",
"_vendor-0ba1deae.js",
"_get-971543fd.js",
"_parse-downloaded-splits-19845c1d.js",
"_create-empty-split-b2c15454.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/transactions/show.js",
"integrity": "sha384-JBLT+bepNQ1x9TTUWy1H/ekjto18Jzudb4suKq+rF1YvqeSpouHzJU2l1YeObQ7y"
"integrity": "sha384-Ml/m6l4ZRqzuhW7/z2hKnrLYZKKkrswMA60W6eWSMeQkNXgzcgO4lO9YS8iYHO2O"
},
"resources/assets/v2/sass/app.scss": {
"file": "assets/app-fb7b26ec.css",

View File

@ -0,0 +1,42 @@
/*
* BaseMode.js
* Copyright (c) 2024 https://github.com/DarKsandr/dark-editable
*
* License: MIT
*
* Copied and slightly edited by James Cole <james@firefly-iii.org>
*/
export default class BaseMode{
constructor(context) {
if(this.constructor === BaseMode){
throw new Error(`It's abstract class`);
}
this.context = context;
}
event_show(){
this.context.typeElement.hideError();
this.context.typeElement.element.value = this.context.value;
this.context.element.dispatchEvent(new CustomEvent("show"));
}
event_shown(){
this.context.element.dispatchEvent(new CustomEvent("shown"));
}
event_hide(){
this.context.element.dispatchEvent(new CustomEvent("hide"));
}
event_hidden(){
this.context.element.dispatchEvent(new CustomEvent("hidden"));
}
init(){
throw new Error('Method `init` not define!');
}
enable(){
throw new Error('Method `enable` not define!');
}
disable(){
throw new Error('Method `disable` not define!');
}
hide(){
throw new Error('Method `hide` not define!');
}
}

View File

@ -0,0 +1,40 @@
/*
* InlineMode.js
* Copyright (c) 2024 https://github.com/DarKsandr/dark-editable
*
* License: MIT
*
* Copied and slightly edited by James Cole <james@firefly-iii.org>
*/
import BaseMode from "./BaseMode.js";
export default class InlineMode extends BaseMode{
init(){
const open = () => {
if(!this.context.disabled){
const item = this.context.typeElement.create();
this.event_show();
this.context.element.removeEventListener('click', open);
this.context.element.innerHTML = '';
this.context.element.append(item);
this.event_shown();
}
}
this.context.element.addEventListener('click', open);
}
enable(){
}
disable(){
}
hide(){
this.event_hide();
this.context.element.innerHTML = this.context.value;
setTimeout(() => {
this.init();
this.event_hidden();
}, 100);
}
}

View File

@ -0,0 +1,53 @@
/*
* PopupMode.js
* Copyright (c) 2024 https://github.com/DarKsandr/dark-editable
*
* License: MIT
*
* Copied and slightly edited by James Cole <james@firefly-iii.org>
*/
import BaseMode from "./BaseMode.js";
export default class PopupMode extends BaseMode{
init(){
this.popover = new bootstrap.Popover(this.context.element, {
container: "body",
content: this.context.typeElement.create(),
html: true,
customClass: "dark-editable",
title: this.context.title,
});
this.context.element.addEventListener('show.bs.popover', () => {
this.event_show();
});
this.context.element.addEventListener('shown.bs.popover', () => {
this.event_shown();
});
this.context.element.addEventListener('hide.bs.popover', () => {
this.event_hide();
});
this.context.element.addEventListener('hidden.bs.popover', () => {
this.event_hidden();
});
document.addEventListener('click', (e) => {
const target = e.target;
if(target === this.popover.tip || target === this.context.element) return;
let current = target;
while(current = current.parentNode){
if(current === this.popover.tip) return;
}
this.hide();
})
}
enable(){
this.popover.enable();
}
disable(){
this.popover.disable();
}
hide(){
this.popover.hide();
}
}

View File

@ -0,0 +1,255 @@
/*
* BaseMode.js
* Copyright (c) 2024 https://github.com/DarKsandr/dark-editable
*
* License: MIT
*
* Copied and slightly edited by James Cole <james@firefly-iii.org>
*/
export default class BaseType {
context = null;
element = null;
error = null;
form = null;
load = null;
buttonGroup = null;
buttons = {success: null, cancel: null};
constructor(context) {
if (this.constructor === BaseType) {
throw new Error(`It's abstract class`);
}
this.context = context;
}
create() {
throw new Error('Method `create` not define!');
}
createContainer(element) {
const div = document.createElement(`div`);
// original list of elements:
this.element = element;
this.error = this.createContainerError();
this.form = this.createContainerForm();
this.load = this.createContainerLoad();
this.buttons.success = this.createButtonSuccess();
this.buttons.cancel = this.createButtonCancel();
// create first div, with label and input:
const topDiv = document.createElement(`div`);
topDiv.classList.add("col-12");
// create label:
const label = document.createElement(`label`);
label.classList.add("visually-hidden");
label.for = element.id;
// add label + input to top div:
topDiv.append(label, element);
// create second div, with button group:
const bottomDiv = document.createElement(`div`);
bottomDiv.classList.add("col-12");
// create button group:
this.buttonGroup = this.createButtonGroup();
// append buttons to button group:
this.buttonGroup.append(this.buttons.success, this.buttons.cancel);
bottomDiv.append(this.buttonGroup);
// append bottom and top div to form:
this.form.append(topDiv, bottomDiv);
//this.form.append(element, this.load, this.buttons.success, this.buttons.cancel);
//this.form.append(element, this.load, this.buttonGroup);
div.append(this.error, this.form);
return div;
}
createButtonGroup() {
const div = document.createElement(`div`);
div.classList.add("btn-group", "btn-group-sm");
return div;
}
createContainerError() {
const div = document.createElement(`div`);
div.classList.add("text-danger", "fst-italic", "mb-2", "fw-bold");
div.style.display = "none";
return div;
}
createContainerForm() {
const form = document.createElement(`form`);
form.classList.add("row", "row-cols-lg-auto", "g-3", "align-items-center");
//form.style.gap = "20px";
form.addEventListener('submit', async e => {
e.preventDefault();
const newValue = this.getValue();
if (this.context.send && this.context.pk && this.context.url && (this.context.value !== newValue)) {
this.showLoad();
let msg;
try {
const response = await this.ajax(newValue);
if (response.ok) {
msg = await this.context.success(response, newValue);
} else {
msg = await this.context.error(response, newValue) || `${response.status} ${response.statusText}`;
}
} catch (error) {
console.error(error);
msg = error;
}
if (msg) {
this.setError(msg);
this.showError();
} else {
this.setError(null);
this.hideError();
this.context.value = this.getValue();
this.context.modeElement.hide();
this.initText();
}
this.hideLoad();
} else {
this.context.value = this.getValue();
this.context.modeElement.hide();
this.initText();
}
this.context.element.dispatchEvent(new CustomEvent("save"));
})
return form;
}
createContainerLoad() {
const div = document.createElement(`div`);
div.style.display = "none";
div.style.position = "absolute";
div.style.background = "white";
div.style.width = "100%";
div.style.height = "100%";
div.style.top = 0;
div.style.left = 0;
const loader = document.createElement(`div`);
loader.classList.add("dark-editable-loader");
div.append(loader);
return div;
}
createButton() {
const button = document.createElement("button");
button.type = "button";
button.classList.add("btn", "btn-sm");
button.style.color = "transparent";
button.style.textShadow = "0 0 0 white";
return button;
}
createButtonSuccess() {
const btn_success = this.createButton();
btn_success.type = "submit";
btn_success.classList.add("btn-success");
btn_success.innerHTML = "✔";
return btn_success;
}
createButtonCancel() {
const btn_cancel = this.createButton();
btn_cancel.classList.add("btn-danger");
const div = document.createElement("div");
div.innerHTML = "✖";
btn_cancel.append(div);
btn_cancel.addEventListener("click", () => {
this.context.modeElement.hide();
});
return btn_cancel;
}
hideLoad() {
this.load.style.display = "none";
}
showLoad() {
this.load.style.display = "block";
}
ajax(new_value) {
let url = this.context.url;
const form = new FormData;
form.append("pk", this.context.pk);
form.append("name", this.context.name);
form.append("value", new_value);
const option = {};
option.method = this.context.ajaxOptions.method;
if (option.method === "POST") {
option.body = form;
} else {
url += "?" + new URLSearchParams(form).toString();
}
return fetch(url, option);
}
async successResponse(response, newValue) {
}
async errorResponse(response, newValue) {
}
setError(errorMsg) {
this.error.innerHTML = errorMsg;
}
showError() {
this.error.style.display = "block";
}
hideError() {
if (this.error) {
this.error.style.display = "none";
}
}
createElement(name) {
const element = document.createElement(name);
console.log(element);
element.classList.add("form-control");
if (this.context.required) {
element.required = this.context.required;
}
this.add_focus(element);
return element;
}
add_focus(element) {
this.context.element.addEventListener('shown', function () {
element.focus();
});
}
initText() {
if (this.context.value === "") {
this.context.element.innerHTML = this.context.emptytext;
return true;
} else {
this.context.element.innerHTML = this.context.value;
return false;
}
}
initOptions() {
}
getValue() {
return this.element.value;
}
}

View File

@ -0,0 +1,16 @@
import DateType from "./DateType.js";
export default class DateTimeType extends DateType{
create(){
const input = this.createElement(`input`);
input.type = "datetime-local";
return this.createContainer(input);
}
initOptions(){
this.context.get_opt("format", "YYYY-MM-DD HH:mm");
this.context.get_opt("viewformat", "YYYY-MM-DD HH:mm");
this.context.value = moment(this.context.value).format("YYYY-MM-DDTHH:mm");
}
}

View File

@ -0,0 +1,25 @@
import BaseType from "./BaseType.js";
export default class DateType extends BaseType{
create(){
const input = this.createElement(`input`);
input.type = "date";
return this.createContainer(input);
}
initText(){
if(this.value === ""){
this.context.element.innerHTML = this.context.emptytext;
return true;
} else {
this.context.element.innerHTML = moment(this.context.value).format(this.context.viewformat);
return false;
}
}
initOptions(){
this.context.get_opt("format", "YYYY-MM-DD");
this.context.get_opt("viewformat", "YYYY-MM-DD");
}
}

View File

@ -0,0 +1,19 @@
import BaseType from "./BaseType.js";
export default class InputType extends BaseType{
create(){
// expand input element with necessary classes and things.
// <input type="text" class="form-control form-control-md" id="inlineFormInputGroupUsername" placeholder="Username">
const input = this.createElement(`input`);
const id = this.context.element.id + '_input';
input.type = this.context.type;
input.id = id;
input.autocomplete = 'off';
input.placeholder = this.context.element.innerText;
input.classList.add("form-control", "form-control-md");
return this.createContainer(input);
}
}

View File

@ -0,0 +1,36 @@
import BaseType from "./BaseType.js";
export default class SelectType extends BaseType{
create(){
const select = this.createElement(`select`);
this.context.source.forEach(item => {
const opt = document.createElement(`option`);
opt.value = item.value;
opt.innerHTML = item.text;
select.append(opt);
});
return this.createContainer(select);
}
initText(){
this.context.element.innerHTML = this.context.emptytext;
if(this.context.value !== "" && this.context.source.length > 0){
for(const key in this.context.source){
const item = this.context.source[ key ];
if(item.value == this.context.value){
this.context.element.innerHTML = item.text;
return false;
}
}
}
return true;
}
initOptions(){
this.context.get_opt("source", []);
if(typeof this.context.source === "string" && this.context.source !== ""){
this.context.source = JSON.parse(this.context.source);
}
}
}

View File

@ -0,0 +1,9 @@
import BaseType from "./BaseType.js";
export default class TextAreaType extends BaseType{
create(){
const textarea = this.createElement(`textarea`);
return this.createContainer(textarea);
}
}

View File

@ -0,0 +1,87 @@
.dark-editable-element{
border-bottom: dashed 1px #0088cc;
text-decoration: none;
cursor: pointer;
}
.dark-editable-element-disabled{
border-bottom: none;
cursor: default;
}
.dark-editable-element-empty{
font-style: italic;
color: #DD1144;
}
.dark-editable{
max-width: none;
}
.dark-editable-loader {
font-size: 5px;
left: 50%;
top: 50%;
width: 1em;
height: 1em;
border-radius: 50%;
position: relative;
text-indent: -9999em;
-webkit-animation: load5 1.1s infinite ease;
animation: load5 1.1s infinite ease;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
@-webkit-keyframes load5 {
0%,
100% {
box-shadow: 0em -2.6em 0em 0em #000000, 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.5), -1.8em -1.8em 0 0em rgba(0,0,0, 0.7);
}
12.5% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.7), 1.8em -1.8em 0 0em #000000, 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.5);
}
25% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.5), 1.8em -1.8em 0 0em rgba(0,0,0, 0.7), 2.5em 0em 0 0em #000000, 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
}
37.5% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.5), 2.5em 0em 0 0em rgba(0,0,0, 0.7), 1.75em 1.75em 0 0em #000000, 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
}
50% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.5), 1.75em 1.75em 0 0em rgba(0,0,0, 0.7), 0em 2.5em 0 0em #000000, -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
}
62.5% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.5), 0em 2.5em 0 0em rgba(0,0,0, 0.7), -1.8em 1.8em 0 0em #000000, -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
}
75% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.5), -1.8em 1.8em 0 0em rgba(0,0,0, 0.7), -2.6em 0em 0 0em #000000, -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
}
87.5% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.5), -2.6em 0em 0 0em rgba(0,0,0, 0.7), -1.8em -1.8em 0 0em #000000;
}
}
@keyframes load5 {
0%,
100% {
box-shadow: 0em -2.6em 0em 0em #000000, 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.5), -1.8em -1.8em 0 0em rgba(0,0,0, 0.7);
}
12.5% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.7), 1.8em -1.8em 0 0em #000000, 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.5);
}
25% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.5), 1.8em -1.8em 0 0em rgba(0,0,0, 0.7), 2.5em 0em 0 0em #000000, 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
}
37.5% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.5), 2.5em 0em 0 0em rgba(0,0,0, 0.7), 1.75em 1.75em 0 0em #000000, 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
}
50% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.5), 1.75em 1.75em 0 0em rgba(0,0,0, 0.7), 0em 2.5em 0 0em #000000, -1.8em 1.8em 0 0em rgba(0,0,0, 0.2), -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
}
62.5% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.5), 0em 2.5em 0 0em rgba(0,0,0, 0.7), -1.8em 1.8em 0 0em #000000, -2.6em 0em 0 0em rgba(0,0,0, 0.2), -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
}
75% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.5), -1.8em 1.8em 0 0em rgba(0,0,0, 0.7), -2.6em 0em 0 0em #000000, -1.8em -1.8em 0 0em rgba(0,0,0, 0.2);
}
87.5% {
box-shadow: 0em -2.6em 0em 0em rgba(0,0,0, 0.2), 1.8em -1.8em 0 0em rgba(0,0,0, 0.2), 2.5em 0em 0 0em rgba(0,0,0, 0.2), 1.75em 1.75em 0 0em rgba(0,0,0, 0.2), 0em 2.5em 0 0em rgba(0,0,0, 0.2), -1.8em 1.8em 0 0em rgba(0,0,0, 0.5), -2.6em 0em 0 0em rgba(0,0,0, 0.7), -1.8em -1.8em 0 0em #000000;
}
}

View File

@ -0,0 +1,181 @@
/*
* dark-editable.js
* Copyright (c) 2024 https://github.com/DarKsandr/dark-editable
*
* License: MIT
*
* Copied and slightly edited by James Cole <james@firefly-iii.org>
*/
import "./dark-editable.css";
import PopupMode from "./Modes/PopupMode.js";
import InlineMode from "./Modes/InlineMode.js";
import BaseType from "./Types/BaseType.js";
import InputType from "./Types/InputType.js";
import TextAreaType from "./Types/TextAreaType.js";
import SelectType from "./Types/SelectType.js";
import DateType from "./Types/DateType.js";
import DateTimeType from "./Types/DateTimeType.js";
export default class DarkEditable{
modeElement = null;
typeElement = null;
mode = null;
type = null;
emptytext = null;
viewformat = null;
pk = null;
name = null;
constructor(element, options = {}){
this.element = element;
this.options = options;
this.init_options();
this.typeElement = this.route_type();
this.typeElement.initOptions();
this.modeElement = this.route_mode();
this.modeElement.init();
this.init_text();
this.init_style();
if(this.disabled){
this.disable();
}
this.element.dispatchEvent(new CustomEvent("init"));
}
/* INIT METHODS */
get_opt(name, default_value){
return this[ name ] = this.element.dataset?.[ name ] ?? this.options?.[ name ] ?? default_value;
}
get_opt_bool(name, default_value){
this.get_opt(name, default_value);
if(typeof this[ name ] !== "boolean"){
if(this[ name ] === "true") {
this[ name ] = true;
} else if(this[ name ] === "false") {
this[ name ] = false;
} else {
this[ name ] = default_value;
}
}
return this[ name ];
}
init_options(){
//priority date elements
this.get_opt("value", this.element.innerHTML);
this.get_opt("name", this.element.id);
this.get_opt("pk", null);
this.get_opt("title", "");
this.get_opt("type", "text");
this.get_opt("emptytext", "Empty");
this.get_opt("mode", "popup");
this.get_opt("url", null);
this.get_opt("ajaxOptions", {});
this.ajaxOptions = Object.assign({
method: "POST",
dataType: "text",
}, this.ajaxOptions);
this.get_opt_bool("send", true);
this.get_opt_bool("disabled", false);
this.get_opt_bool("required", false);
if(this.options?.success && typeof this.options?.success == "function"){
this.success = this.options.success;
}
if(this.options?.error && typeof this.options?.error == "function"){
this.error = this.options.error;
}
}
init_text(){
const empty_class = "dark-editable-element-empty";
this.element.classList.remove(empty_class);
if(this.typeElement.initText()){
this.element.classList.add(empty_class);
}
}
init_style(){
this.element.classList.add("dark-editable-element");
}
/* INIT METHODS END */
route_mode(){
switch (this.mode){
default:
throw new Error(`Mode ${this.mode} not found!`)
case 'popup':
return new PopupMode(this);
case 'inline':
return new InlineMode(this);
}
}
route_type(){
if(this.type.prototype instanceof BaseType){
return new this.type(this);
}
if(typeof this.type === 'string'){
switch(this.type){
case "text":
case "password":
case "email":
case "url":
case "tel":
case "number":
case "range":
case "time":
return new InputType(this);
case "textarea":
return new TextAreaType(this);
case "select":
return new SelectType(this);
case "date":
return new DateType(this);
case "datetime":
return new DateTimeType(this);
}
}
throw new Error(`Undefined type`);
}
/* AJAX */
async success(response, newValue){
return await this.typeElement.successResponse(response, newValue);
}
async error(response, newValue){
return await this.typeElement.errorResponse(response, newValue);
}
/* AJAX END */
/* METHODS */
enable(){
this.disabled = false;
this.element.classList.remove("dark-editable-element-disabled");
this.modeElement.enable();
}
disable(){
this.disabled = true;
this.element.classList.add("dark-editable-element-disabled");
this.modeElement.enable();
}
setValue(value){
this.value = value;
this.init_text();
}
getValue(){
return this.value;
}
/* METHODS END */
}

View File

@ -349,7 +349,7 @@ let transactions = function () {
this.detectTransactionType();
// parse transaction:
let transactions = parseFromEntries(this.entries, this.groupProperties.transactionType);
let transactions = parseFromEntries(this.entries, null, this.groupProperties.transactionType);
let submission = {
group_title: this.groupProperties.title,
fire_webhooks: this.formStates.webhooksButton,
@ -466,7 +466,7 @@ let transactions = function () {
// onRenderItem: renderAccount,
onChange: changeSourceAccount,
onSelectItem: selectSourceAccount,
hiddenValue: this.items[count].source_account.alpine_name,
hiddenValue: this.entries[count].source_account.alpine_name
});
addAutocomplete({
selector: 'input.ac-dest',

View File

@ -58,6 +58,7 @@ let transactions = function () {
return {
// transactions are stored in "entries":
entries: [],
originals: [],
// state of the form is stored in formState:
formStates: {
@ -126,7 +127,7 @@ let transactions = function () {
this.formStates.isSubmitting = true;
// parse transaction:
let transactions = parseFromEntries(this.entries, this.groupProperties.transactionType);
let transactions = parseFromEntries(this.entries, this.originals, this.groupProperties.transactionType);
let submission = {
group_title: this.groupProperties.title,
fire_webhooks: this.formStates.webhooksButton,
@ -263,7 +264,7 @@ let transactions = function () {
getter.show(groupId, {}).then((response) => {
const data = response.data.data;
this.groupProperties.id = parseInt(data.id);
this.groupProperties.transactionType = data.attributes.transactions[0].type;
this.groupProperties.transactionType = data.attributes.transactions[0].type.toLowerCase();
this.groupProperties.title = data.attributes.title ?? data.attributes.transactions[0].description;
this.entries = parseDownloadedSplits(data.attributes.transactions);

View File

@ -43,12 +43,14 @@ function addPointToMap(e) {
markers[index].on('dragend', dragEnd);
markers[index].addTo(maps[index]);
const setEvent = new CustomEvent('location-set', {detail: {
const setEvent = new CustomEvent('location-set', {
detail: {
latitude: e.latlng.lat,
longitude: e.latlng.lng,
index: index,
zoomLevel: maps[index].getZoom()
}});
}
});
document.dispatchEvent(setEvent);
}
}
@ -56,10 +58,12 @@ function addPointToMap(e) {
function saveZoomOfMap(e) {
//let index = parseInt(e.sourceTarget._container.attributes['data-index'].value);
let index = 0;
const zoomEvent = new CustomEvent('location-zoom', {detail: {
const zoomEvent = new CustomEvent('location-zoom', {
detail: {
index: index,
zoomLevel: maps[index].getZoom()
}});
}
});
document.dispatchEvent(zoomEvent);
}
@ -87,13 +91,15 @@ export function addLocation(index) {
//let holder = document.getElementById('location_map_' + index);
let holder = document.getElementById('location_map');
maps[index] = L.map(holder).setView([holder.dataset.latitude, holder.dataset.longitude], holder.dataset.zoomLevel);
if (holder) {
maps[index] = L.map(holder).setView([holder.dataset.latitude, holder.dataset.longitude], holder.dataset.zoomLevel);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(maps[index]);
maps[index].on('click', addPointToMap);
maps[index].on('zoomend', saveZoomOfMap);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(maps[index]);
maps[index].on('click', addPointToMap);
maps[index].on('zoomend', saveZoomOfMap);
}
}
}

View File

@ -21,15 +21,23 @@
/**
*
*/
export function parseFromEntries(entries, transactionType) {
export function parseFromEntries(entries, originals, transactionType) {
let returnArray = [];
for (let i in entries) {
if (entries.hasOwnProperty(i)) {
const entry = entries[i];
let compare = false;
let original = {};
if (originals !== null && originals.hasOwnProperty(i)) {
compare = true;
let original = originals[i];
}
let current = {};
// fields for transaction
current.description = entry.description;
if ((compare && original.description !== entry.description) || !compare) {
current.description = entry.description;
}
// source and destination
current.source_name = entry.source_account.name;

View File

@ -25,6 +25,8 @@ import Get from "../../api/v2/model/transaction/get.js";
import {parseDownloadedSplits} from "./shared/parse-downloaded-splits.js";
import {format} from "date-fns";
import formatMoney from "../../util/format-money.js";
import DarkEditable from "../../libraries/dark-editable/dark-editable.js";
let show = function () {
return {
@ -56,6 +58,10 @@ let show = function () {
pageProperties: {},
formatMoney(amount, currencyCode) {
console.log('formatting', amount, currencyCode);
if('' === currencyCode) {
currencyCode = 'EUR';
}
return formatMoney(amount, currencyCode);
},
format(date) {
@ -95,6 +101,12 @@ let show = function () {
}
}
}
// at this point do the inline change fields
//inlineEdit('journal_description')
const usernameEl = document.getElementById('journal_description');
const popover = new DarkEditable(usernameEl, {mode: 'inline', url: '/something-else'});
}).catch((error) => {
// todo auto generated.
this.notifications.error.show = true;

View File

@ -45,6 +45,16 @@
</tbody>
</table>
</div>
<div class="card-footer text-end">
<div class="btn-group btn-group-sm">
<a class="btn btn-primary" :href="'./transactions/edit/' + groupProperties.id">
<em class="fa-solid fa-edit"></em> {{ __('firefly.edit') }}
</a>
<a class="btn btn-danger" :href="'./transactions/delete/' + groupProperties.id">
<em class="fa-solid fa-trash"></em> {{ __('firefly.delete') }}
</a>
</div>
</div>
</div>
</div>
<div class="col-xl-6 col-lg-12 col-md-12 col-sm-12 col-xs-12">
@ -123,10 +133,17 @@
<div class="row">
<template x-for="(entry, index) in entries">
<div class="col-xl-4 col-lg-6 col-md-12 col-sm-12 col-xs-12">
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title">
<span x-text="entry.description"></span>
<span
id="journal_description"
data-type="text"
data-pk="12"
data-title="{{ __('firefly.description') }}" x-text="entry.description"></span>
<template x-if="entries.length > 1">
<span class="badge bg-secondary">
<span x-text="index + 1"></span> / <span x-text="entries.length"></span>
@ -215,7 +232,7 @@
</template>
</template>
<template x-for="meta in metaFields">
<template x-if="null !== entry[meta] and '' !== entry[meta]">
<template x-if="typeof entry[meta] !== 'undefined' && null !== entry[meta] && '' !== entry[meta]">
<tr>
<th><span x-text="meta"></span></th>
<td><span x-text="entry[meta]"></span></td>
@ -224,7 +241,7 @@
</template>
<tr>
<th>recurring things</th>
<td>TODO meta</td>
<td>TODO recurring</td>
</tr>
<template x-if="entry.tags.length > 0">
<tr>

View File

@ -55,6 +55,12 @@ final class BillDateCalculatorTest extends TestCase
// already paid on the 12th, expect it next month.
'1Mc' => ['earliest' => Carbon::parse('2023-11-01'), 'latest' => Carbon::parse('2023-11-30'), 'billStart' => Carbon::parse('2023-01-01'), 'period' => 'monthly', 'skip' => 0, 'lastPaid' => Carbon::parse('2023-11-12'), 'expected' => ['2023-12-01']],
// every month, start on 2024-01-30, view is quarterly
'1Md' => ['earliest' => Carbon::parse('2023-01-01'), 'latest' => Carbon::parse('2023-03-31'), 'billStart' => Carbon::parse('2023-01-29'), 'period' => 'monthly', 'skip' => 0, 'lastPaid' => null, 'expected' => ['2023-01-29', '2023-02-28', '2023-03-29']],
// every month, start on 2024-01-30, view is quarterly
'1Me' => ['earliest' => Carbon::parse('2024-01-01'), 'latest' => Carbon::parse('2024-03-31'), 'billStart' => Carbon::parse('2023-01-30'), 'period' => 'monthly', 'skip' => 0, 'lastPaid' => null, 'expected' => ['2024-01-30', '2024-02-29', '2024-03-30']],
// yearly not due this month. Should jump to next year.
'1Ya' => ['earliest' => Carbon::parse('2023-11-01'), 'latest' => Carbon::parse('2023-11-30'), 'billStart' => Carbon::parse('2021-05-01'), 'period' => 'yearly', 'skip' => 0, 'lastPaid' => Carbon::parse('2023-05-02'), 'expected' => ['2024-05-01']],
];