diff --git a/app/Http/Controllers/Account/ReconcileController.php b/app/Http/Controllers/Account/ReconcileController.php new file mode 100644 index 0000000000..2ef5905eeb --- /dev/null +++ b/app/Http/Controllers/Account/ReconcileController.php @@ -0,0 +1,173 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Account; + + +use Carbon\Carbon; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use Illuminate\Http\Request; +use Illuminate\Support\Collection; +use Navigation; +use Preferences; +use Response; +use View; + +/** + * Class ReconcileController + * + * @package FireflyIII\Http\Controllers\Account + */ +class ReconcileController extends Controller +{ + /** + * + */ + public function __construct() + { + parent::__construct(); + + // translations: + $this->middleware( + function ($request, $next) { + View::share('mainTitleIcon', 'fa-credit-card'); + View::share('title', trans('firefly.accounts')); + + return $next($request); + } + ); + } + + /** + * @param Request $request + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + */ + public function overview(Request $request, Account $account, Carbon $start, Carbon $end) + { + $startBalance = $request->get('startBalance'); + $endBalance = $request->get('endBalance'); + $transactions = $request->get('transactions'); + + $return = [ + 'is_zero' => false, + 'post_uri' => route('accounts.reconcile.submit', [$account->id, $start->format('Ymd'), $end->format('Ymd')]), + 'html' => '', + ]; + $return['html'] = view('accounts.reconcile.overview', compact('account', 'start', 'end'))->render(); + + return Response::json($return); + + } + + /** + * @param Account $account + * @param Carbon|null $start + * @param Carbon|null $end + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function reconcile(Account $account, Carbon $start = null, Carbon $end = null) + { + if ($account->accountType->type === AccountType::INITIAL_BALANCE) { + return $this->redirectToOriginalAccount($account); + } + /** @var CurrencyRepositoryInterface $currencyRepos */ + $currencyRepos = app(CurrencyRepositoryInterface::class); + $currencyId = intval($account->getMeta('currency_id')); + $currency = $currencyRepos->find($currencyId); + if ($currencyId === 0) { + $currency = app('amount')->getDefaultCurrency(); + } + + // no start or end: + $range = Preferences::get('viewRange', '1M')->data; + + // get start and end + if (is_null($start) && is_null($end)) { + $start = clone session('start', Navigation::startOfPeriod(new Carbon, $range)); + $end = clone session('end', Navigation::endOfPeriod(new Carbon, $range)); + } + if (is_null($end)) { + $end = Navigation::endOfPeriod($start, $range); + } + + $startDate = clone $start; + $startDate->subDays(1); + $startBalance = round(app('steam')->balance($account, $startDate), $currency->decimal_places); + $endBalance = round(app('steam')->balance($account, $end), $currency->decimal_places); + $subTitleIcon = config('firefly.subIconsByIdentifier.' . $account->accountType->type); + $subTitle = trans('firefly.reconcile_account', ['account' => $account->name]); + + // various links + $transactionsUri = route('accounts.reconcile.transactions', [$account->id, '%start%', '%end%']); + $overviewUri = route('accounts.reconcile.overview', [$account->id, '%start%', '%end%']); + $indexUri = route('accounts.reconcile', [$account->id, '%start%', '%end%']); + + return view( + 'accounts.reconcile.index', compact( + 'account', 'currency', 'subTitleIcon', 'start', 'end', 'subTitle', 'startBalance', 'endBalance', 'transactionsUri', + 'selectionStart', 'selectionEnd', 'overviewUri', 'indexUri' + ) + ); + } + + /** + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return mixed + */ + public function transactions(Account $account, Carbon $start, Carbon $end) + { + if ($account->accountType->type === AccountType::INITIAL_BALANCE) { + return $this->redirectToOriginalAccount($account); + } + + // get the transactions + $selectionStart = clone $start; + $selectionStart->subDays(3); + $selectionEnd = clone $end; + $selectionEnd->addDays(3); + + + // grab transactions: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAccounts(new Collection([$account])) + ->setRange($selectionStart, $selectionEnd)->withBudgetInformation()->withOpposingAccount()->withCategoryInformation(); + $transactions = $collector->getJournals(); + $html = view('accounts.reconcile.transactions', compact('account', 'transactions', 'start', 'end', 'selectionStart', 'selectionEnd'))->render(); + + return Response::json(['html' => $html]); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 1181029657..ecd68c0b91 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -250,74 +250,6 @@ class AccountController extends Controller return view('accounts.index', compact('what', 'subTitleIcon', 'subTitle', 'accounts')); } - /** - * @param Request $request - * @param Account $account - * @param string $moment - * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - */ - public function reconcile(Request $request, Account $account, Carbon $start = null, Carbon $end = null) - { - if ($account->accountType->type === AccountType::INITIAL_BALANCE) { - return $this->redirectToOriginalAccount($account); - } - /** @var CurrencyRepositoryInterface $currencyRepos */ - $currencyRepos = app(CurrencyRepositoryInterface::class); - $currencyId = intval($account->getMeta('currency_id')); - $currency = $currencyRepos->find($currencyId); - if ($currencyId === 0) { - $currency = app('amount')->getDefaultCurrency(); - } - - // no start or end: - $range = Preferences::get('viewRange', '1M')->data; - - // get start and end - if(is_null($start) && is_null($end)) { - $start = clone session('start', Navigation::startOfPeriod(new Carbon, $range)); - $end = clone session('end', Navigation::endOfPeriod(new Carbon, $range)); - } - if(is_null($end)) { - $end = Navigation::endOfPeriod($start, $range); - } - - $startDate = clone $start; - $startDate->subDays(1); - $startBalance = round(app('steam')->balance($account, $startDate), $currency->decimal_places); - $endBalance = round(app('steam')->balance($account, $end), $currency->decimal_places); - $subTitleIcon = config('firefly.subIconsByIdentifier.' . $account->accountType->type); - $subTitle = trans('firefly.reconcile_account', ['account' => $account->name]); - - // get the transactions - $selectionStart = clone $start; - $selectionStart->subDays(7); - $selectionEnd = clone $end; - $selectionEnd->addDays(5); - - // grab transactions: - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setAccounts(new Collection([$account])) - ->setRange($selectionStart, $selectionEnd)->withBudgetInformation()->withOpposingAccount()->withCategoryInformation(); - $transactions = $collector->getJournals(); - - return view('accounts.reconcile', compact('account', 'currency', 'subTitleIcon', 'start', 'end', 'subTitle', 'startBalance', 'endBalance','transactions','selectionStart','selectionEnd')); - - // prep for "specific date" view. - if (strlen($moment) > 0 && $moment !== 'all') { - $start = new Carbon($moment); - $end = Navigation::endOfPeriod($start, $range); - } - - - - return view( - 'accounts.show', - compact('account', 'currency', 'moment', 'periods', 'subTitleIcon', 'transactions', 'subTitle', 'start', 'end', 'chartUri') - ); - } - /** * Show an account. * diff --git a/public/js/ff/accounts/reconcile.js b/public/js/ff/accounts/reconcile.js index c19b64d482..8611b24f74 100644 --- a/public/js/ff/accounts/reconcile.js +++ b/public/js/ff/accounts/reconcile.js @@ -18,20 +18,178 @@ * along with Firefly III. If not, see . */ +var balanceDifference = 0; +var difference = 0; +var selectedAmount = 0; +var reconcileStarted = false; +/** + * + */ $(function () { "use strict"; - $('input[type="date"]').on('change', showUpdateButton); - $('.update_view').on('click', updateView); + + /* + Respond to changes in balance statements. + */ + $('input[type="number"]').on('change', function () { + if (reconcileStarted) { + calculateBalanceDifference(); + difference = balanceDifference - selectedAmount; + updateDifference(); + } + }); + + /* + Respond to changes in the date range. + */ + $('input[type="date"]').on('change', function () { + if (reconcileStarted) { + // hide original instructions. + $('.select_transactions_instruction').hide(); + + // show date-change warning + $('.date_change_warning').show(); + + // show update button + $('.change_date_button').show(); + } + }); + + $('.change_date_button').click(startReconcile); + $('.start_reconcile').click(startReconcile); + $('.store_reconcile').click(storeReconcile); + }); -function showUpdateButton() { - $('.update_date_button').show(); +function storeReconcile() { + // get modal HTML: + var ids = []; + $.each($('.reconcile_checkbox:checked'), function (i, v) { + ids.push($(v).data('id')); + }); + var variables = { + startBalance: parseFloat($('input[name="start_balance"]').val()), + endBalance: parseFloat($('input[name="end_balance"]').val()), + startDate: $('input[name="start_date"]').val(), + startEnd: $('input[name="end_date"]').val(), + transactions: ids + }; + var uri = overviewUri.replace('%start%', $('input[name="start_date"]').val()).replace('%end%', $('input[name="end_date"]').val()); + + + $.getJSON(uri, variables).done(function (data) { + if (data.is_zero === false) { + $('#defaultModal').empty().html(data.html).modal('show'); + } + }); } -function updateView() { - var startDate = $('input[name="start_date"]').val(); - var endDate = $('input[name="end_date"]').val(); - window.location = '/accounts/reconcile/2/' + startDate + '/' + endDate; +/** + * What happens when you check a checkbox: + * @param e + */ +function checkReconciledBox(e) { + var el = $(e.target); + var amount = parseFloat(el.val()); + console.log('Amount is ' + amount); + // if checked, add to selected amount + if (el.prop('checked') === true && el.data('younger') === false) { + console.log("Sum is: " + selectedAmount + " - " + amount + " = " + (selectedAmount - amount)); + selectedAmount = selectedAmount - amount; + } + if (el.prop('checked') === false && el.data('younger') === false) { + console.log("Sum is: " + selectedAmount + " + " + amount + " = " + (selectedAmount + amount)); + selectedAmount = selectedAmount + amount; + } + difference = balanceDifference - selectedAmount; + updateDifference(); + allowReconcile(); +} + +/** + * + */ +function allowReconcile() { + var count = $('.reconcile_checkbox:checked').length; + console.log('Count checkboxes is ' + count); + if (count > 0) { + $('.store_reconcile').prop('disabled', false); + } +} + +/** + * Calculate the difference between given start and end balance + * and put it in balanceDifference. + */ +function calculateBalanceDifference() { + var startBalance = parseFloat($('input[name="start_balance"]').val()); + var endBalance = parseFloat($('input[name="end_balance"]').val()); + balanceDifference = startBalance - endBalance; + if (balanceDifference < 0) { + balanceDifference = balanceDifference * -1; + } +} + +function getTransactionsForRange() { + // clear out the box: + $('#transactions_holder').empty().append($('

').addClass('text-center').html('')); + var uri = transactionsUri.replace('%start%', $('input[name="start_date"]').val()).replace('%end%', $('input[name="end_date"]').val()); + var index = indexUri.replace('%start%', $('input[name="start_date"]').val()).replace('%end%', $('input[name="end_date"]').val()); + window.history.pushState('object or string', "Reconcile account", index); + + $.getJSON(uri).done(placeTransactions); +} + +function placeTransactions(data) { + $('#transactions_holder').empty().html(data.html); + + + // as long as the dates are equal, changing the balance does not matter. + calculateBalanceDifference(); + difference = balanceDifference; + updateDifference(); + + // enable the check buttons: + $('.reconcile_checkbox').prop('disabled', false).unbind('change').change(checkReconciledBox); + + // show the other instruction: + $('.select_transactions_instruction').show(); +} + +/** + * + * @returns {boolean} + */ +function startReconcile() { + + reconcileStarted = true; + + // hide the start button. + $('.start_reconcile').hide(); + + // hide the start instructions: + $('.update_balance_instruction').hide(); + + // hide date-change warning + $('.date_change_warning').hide(); + + // hide update button + $('.change_date_button').hide(); + + getTransactionsForRange(); + + return false; } + +function updateDifference() { + var addClass = 'text-info'; + if (difference > 0) { + addClass = 'text-success'; + } + if (difference < 0) { + addClass = 'text-danger'; + } + $('#difference').addClass(addClass).text(accounting.formatMoney(difference)); +} \ No newline at end of file diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index c6b211e967..d387359f22 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -619,8 +619,19 @@ return [ 'cash_accounts' => 'Cash accounts', 'Cash account' => 'Cash account', 'reconcile_account' => 'Reconcile account ":account"', - 'end_of_reconcile_period' => 'End of reconcile period: :period', - 'start_of_reconcile_period' => 'Start of reconcile period: :period', + 'end_of_reconcile_period' => 'End of reconcile period: :period', + 'start_of_reconcile_period' => 'Start of reconcile period: :period', + 'start_balance' => 'Start balance', + 'end_balance' => 'End balance', + 'update_balance_dates_instruction' => 'Match the amounts and dates above to your bank statement, and press "Start reconciling"', + 'select_transactions_instruction' => 'Select the transactions that appear on your bank statement.', + 'select_range_and_balance' => 'First verify the date-range and balances. Then press "Start reconciling"', + 'date_change_instruction' => 'If you change the date range now, any progress will be lost.', + 'update_selection' => 'Update selection', + 'store_reconcile' => 'Store reconciliation', + 'reconcile_options' => 'Reconciliation options', + 'reconcile_range' => 'Reconciliation range', + 'start_reconcile' => 'Start reconciling', 'cash' => 'cash', 'account_type' => 'Account type', 'save_transactions_by_moving' => 'Save these transaction(s) by moving them to another account:', diff --git a/resources/views/accounts/reconcile/index.twig b/resources/views/accounts/reconcile/index.twig index e111c69190..a5aade3415 100644 --- a/resources/views/accounts/reconcile/index.twig +++ b/resources/views/accounts/reconcile/index.twig @@ -75,9 +75,13 @@

+ {{ 'start_reconcile'|_ }} + @@ -124,120 +128,9 @@

{{ 'transactions'|_ }}

- - - - - - - - - - - - - - - - - {# data for previous/next markers #} - {% set endSet = false %} - {% set startSet = false %} - {% for transaction in transactions %} - {# start marker #} - {% if transaction.date < start and startSet == false %} - - - - - - {% set startSet = true %} - {% endif %} - - {# end marker #} - {% if transaction.date <= end and endSet == false %} - - - - - - {% set endSet = true %} - {% endif %} - - - - {# icon #} - - - {# description #} - - - - - - - - - - - {% endfor %} - -
{{ trans('list.description') }}{{ trans('list.amount') }}
-   - - - {{ trans('firefly.start_of_reconcile_period', {period: start.formatLocalized(monthAndDayFormat) }) }} - - -   -
-   - - - {{ trans('firefly.end_of_reconcile_period', {period: end.formatLocalized(monthAndDayFormat) }) }} - - -   -
- - {{ transaction|transactionDescription }} - - {# is a split journal #} - {{ transaction|transactionIsSplit }} - - {# count attachments #} - {{ transaction|transactionHasAtt }} - - - {{ transaction|transactionAmount }} - {% if transaction.reconciled %} - {{ transaction|transactionReconciled }} - {% else %} - - {% endif %} -
+
+

{{ 'select_range_and_balance'|_ }}

+
@@ -253,6 +146,9 @@ var accountID = {{ account.id }}; var startBalance = {{ startBalance }}; var endBalance = {{ endBalance }}; + var transactionsUri = '{{ transactionsUri }}'; + var overviewUri = '{{ overviewUri }}'; + var indexUri = '{{ indexUri }}'; {% endblock %} diff --git a/resources/views/accounts/reconcile/overview.twig b/resources/views/accounts/reconcile/overview.twig new file mode 100644 index 0000000000..4ae512ff15 --- /dev/null +++ b/resources/views/accounts/reconcile/overview.twig @@ -0,0 +1,39 @@ + + diff --git a/resources/views/accounts/reconcile/transactions.twig b/resources/views/accounts/reconcile/transactions.twig new file mode 100644 index 0000000000..3b10c7b2a2 --- /dev/null +++ b/resources/views/accounts/reconcile/transactions.twig @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + {# data for previous/next markers #} + {% set endSet = false %} + {% set startSet = false %} + {% for transaction in transactions %} + {# start marker #} + {% if transaction.date < start and startSet == false %} + + + + + + {% set startSet = true %} + {% endif %} + + {# end marker #} + {% if transaction.date <= end and endSet == false %} + + + + + + {% set endSet = true %} + {% endif %} + + + + {# icon #} + + + {# description #} + + + + + + + + + + {% endfor %} + + {# if the start marker has not been generated yet, do it now, at the end of the loop. #} + {% if startSet == false %} + + + + + + {% set startSet = true %} + {% endif %} + +
{{ trans('list.description') }}{{ trans('list.amount') }}
+   + + + {{ trans('firefly.start_of_reconcile_period', {period: start.formatLocalized(monthAndDayFormat) }) }} + + +   +
+   + + + {{ trans('firefly.end_of_reconcile_period', {period: end.formatLocalized(monthAndDayFormat) }) }} + + +   +
+ + {{ transaction|transactionDescription }} + + {# is a split journal #} + {{ transaction|transactionIsSplit }} + + {# count attachments #} + {{ transaction|transactionHasAtt }} + + + {{ transaction|transactionAmount }} + {% if transaction.reconciled %} + {{ transaction|transactionReconciled }} + {% else %} + + {% endif %} +
+   + + + {{ trans('firefly.start_of_reconcile_period', {period: start.formatLocalized(monthAndDayFormat) }) }} + + +   +
\ No newline at end of file diff --git a/resources/views/partials/transaction-row.twig b/resources/views/partials/transaction-row.twig index d823971f9b..e969a07888 100644 --- a/resources/views/partials/transaction-row.twig +++ b/resources/views/partials/transaction-row.twig @@ -21,7 +21,7 @@ {# description #} - {# count attachments #} + {# is reconciled? #} {{ transaction|transactionReconciled }} diff --git a/routes/web.php b/routes/web.php index 163db19429..a7fa87e95e 100755 --- a/routes/web.php +++ b/routes/web.php @@ -90,10 +90,15 @@ Route::group( Route::get('{what}', ['uses' => 'AccountController@index', 'as' => 'index'])->where('what', 'revenue|asset|expense'); Route::get('create/{what}', ['uses' => 'AccountController@create', 'as' => 'create'])->where('what', 'revenue|asset|expense'); Route::get('edit/{account}', ['uses' => 'AccountController@edit', 'as' => 'edit']); - Route::get('reconcile/{account}/{start_date?}/{end_date?}', ['uses' => 'AccountController@reconcile', 'as' => 'reconcile']); Route::get('delete/{account}', ['uses' => 'AccountController@delete', 'as' => 'delete']); Route::get('show/{account}/{moment?}', ['uses' => 'AccountController@show', 'as' => 'show']); + // reconcile routes: + Route::get('reconcile/{account}/index/{start_date?}/{end_date?}', ['uses' => 'Account\ReconcileController@reconcile', 'as' => 'reconcile']); + Route::get('reconcile/{account}/transactions/{start_date?}/{end_date?}', ['uses' => 'Account\ReconcileController@transactions', 'as' => 'reconcile.transactions']); + Route::get('reconcile/{account}/overview/{start_date?}/{end_date?}', ['uses' => 'Account\ReconcileController@overview', 'as' => 'reconcile.overview']); + Route::post('reconcile/{account}/submit/{start_date?}/{end_date?}', ['uses' => 'Account\ReconcileController@submit', 'as' => 'reconcile.submit']); + Route::post('store', ['uses' => 'AccountController@store', 'as' => 'store']); Route::post('update/{account}', ['uses' => 'AccountController@update', 'as' => 'update']); Route::post('destroy/{account}', ['uses' => 'AccountController@destroy', 'as' => 'destroy']);