From 5112bc4ebb48ebeb37c508884bbce403eb0e9ea5 Mon Sep 17 00:00:00 2001 From: Jesus Guerrero Date: Fri, 1 Sep 2023 21:01:36 -0400 Subject: [PATCH 01/10] feat: add delete and update reconciliation --- .../Controllers/ReconciliationController.php | 41 ++++++ .../Listeners/UpdateOpenReconciliations.php | 28 ++++ .../Services/ReconciliationService.php | 54 +++++++- app/Domains/Transaction/routes.php | 5 + app/Providers/EventServiceProvider.php | 4 +- components.d.ts | 1 + resources/js/Components/OccurrencePreview.vue | 4 +- resources/js/Components/atoms/LogerButton.vue | 20 ++- resources/js/Components/shared/BaseTable.vue | 14 +- .../templates/TransactionAddButton.vue | 45 +++--- resources/js/Pages/Finance/Account.vue | 4 +- resources/js/Pages/Finance/Category.vue | 6 +- resources/js/Pages/Finance/Index.vue | 16 +-- .../Partials/AccountReconciliationBanner.vue | 2 +- .../Reconciliation/AccountReconciliations.vue | 6 +- .../Pages/Finance/Reconciliation/Create.vue | 10 +- .../js/Pages/Finance/Reconciliation/Show.vue | 128 +++++++++++++++--- resources/js/Pages/Finance/Transactions.vue | 4 +- .../js/Pages/LogerProfile/ProfileView.vue | 6 +- .../components/ReconciliationTable.vue | 120 ++++++++++++++++ .../components/TransactionSearch.vue | 2 +- ...ctionTemplate.vue => TransactionTable.vue} | 0 ...sactionsTable.vue => TransactionsList.vue} | 0 .../transactions/reconciliationCols.ts | 10 -- .../domains/transactions/tableAccountCols.ts | 12 +- resources/js/utils/useResponsive.ts | 14 ++ 26 files changed, 464 insertions(+), 92 deletions(-) create mode 100644 app/Domains/Transaction/Listeners/UpdateOpenReconciliations.php create mode 100644 resources/js/domains/transactions/components/ReconciliationTable.vue rename resources/js/domains/transactions/components/{TransactionTemplate.vue => TransactionTable.vue} (100%) rename resources/js/domains/transactions/components/{TransactionsTable.vue => TransactionsList.vue} (100%) create mode 100644 resources/js/utils/useResponsive.ts diff --git a/app/Domains/Transaction/Http/Controllers/ReconciliationController.php b/app/Domains/Transaction/Http/Controllers/ReconciliationController.php index f6b30b28..86bbbac8 100644 --- a/app/Domains/Transaction/Http/Controllers/ReconciliationController.php +++ b/app/Domains/Transaction/Http/Controllers/ReconciliationController.php @@ -6,8 +6,10 @@ use App\Domains\Transaction\Services\ReconciliationService; use App\Http\Controllers\Controller; use App\Http\Controllers\Traits\HasEnrichedRequest; +use Exception; use Illuminate\Support\Facades\Gate; use Insane\Journal\Models\Accounting\Reconciliation; +use Insane\Journal\Models\Accounting\ReconciliationEntry; use Insane\Journal\Models\Core\Account; class ReconciliationController extends Controller { @@ -84,5 +86,44 @@ public function update(Reconciliation $reconciliation, ReconciliationService $se 'banner' => "Updated correctly" ]); } + } + + public function syncTransactions(Reconciliation $reconciliation, ReconciliationService $service) { + $reconciliation = $service->syncTransactions($reconciliation); + + if ($reconciliation->difference) { + return back()->with('flash', [ + 'banner' => "Can't reconcile this account" + ]); + } else { + back()->with('flash', [ + 'banner' => "Updated correctly" + ]); + } + } + + public function delete(Reconciliation $reconciliation, ReconciliationService $service) { + try { + $accountId = $reconciliation->account_id; + $service->delete($reconciliation); + return redirect("/finance/reconciliation/$accountId")->with('flash', [ + 'banner' => "Can't reconcile this account" + ]); + } catch (Exception) { + back()->with('flash', [ + 'banner' => "Updated correctly" + ]); + } + } + + + public function checkReconciliationEntry(Reconciliation $reconciliation, ReconciliationEntry $reconciliationEntry, ReconciliationService $service) { + if(!Gate::forUser(auth()->user())->check('adjust', $reconciliation)) { + back()->with('flash', [ + 'banner' => "Can't reconcile this account" + ]); } + $postData = $this->getPostData(); + $service->checkLine($reconciliation, $reconciliationEntry, $postData['matched']); + } } diff --git a/app/Domains/Transaction/Listeners/UpdateOpenReconciliations.php b/app/Domains/Transaction/Listeners/UpdateOpenReconciliations.php new file mode 100644 index 00000000..5d80664d --- /dev/null +++ b/app/Domains/Transaction/Listeners/UpdateOpenReconciliations.php @@ -0,0 +1,28 @@ +service->checkOpenReconciliation($event->transaction->account, $event->transaction); + $this->service->checkOpenReconciliation($event->transaction->account, $event->transaction); + } +} diff --git a/app/Domains/Transaction/Services/ReconciliationService.php b/app/Domains/Transaction/Services/ReconciliationService.php index 3b6c0c24..83c1e956 100644 --- a/app/Domains/Transaction/Services/ReconciliationService.php +++ b/app/Domains/Transaction/Services/ReconciliationService.php @@ -5,8 +5,10 @@ use App\Domains\AppCore\Models\Category; use App\Domains\Transaction\Data\ReconciliationParamsData; use App\Domains\Transaction\Models\Transaction; +use App\Domains\Transaction\Models\TransactionLine; use Exception; use Insane\Journal\Models\Accounting\Reconciliation; +use Insane\Journal\Models\Accounting\ReconciliationEntry; use Insane\Journal\Models\Core\Account; class ReconciliationService @@ -20,9 +22,11 @@ public function listHistoryOf(Account $account) { public function create(Account $account, ReconciliationParamsData $params) { $transactions = $account->transactionsToReconcile(null, $params->date); - if (!count($transactions)) { - throw new Exception("no transactions matched"); - } + + // if (!count($transactions)) { + // throw new Exception("no transactions matched"); + // } + $diff = $account->balance - $params->balance; $reconciliation = Reconciliation::create([ 'user_id' => $params->user_id, @@ -57,6 +61,21 @@ public function update(Reconciliation $reconciliation, ReconciliationParamsData return $reconciliation; } + public function delete(Reconciliation $reconciliation) { + $entries = $reconciliation->entries()->select(['id', 'transaction_line_id'])->get(); + + TransactionLine::whereIn('id', $entries->pluck('transaction_line_id')) + ->update([ + 'matched' => false + ]); + + $reconciliation->entries()->whereIn('id', $entries->pluck('id'))->delete(); + + $reconciliation->delete(); + + return $reconciliation; + } + public function saveAdjustment(Reconciliation $reconciliation) { $transaction = Transaction::createTransaction([ 'team_id' => $reconciliation->team_id, @@ -77,4 +96,33 @@ public function saveAdjustment(Reconciliation $reconciliation) { $reconciliation->addEntry($transaction->lines()->select(['id', 'transaction_id'])->where('account_id', $reconciliation->account_id)->first()); $reconciliation->checkStatus(); } + + + public function checkLine(Reconciliation $reconciliation, ReconciliationEntry $line, bool $matched = false) { + $reconciliation->entries()->where("id", $line->id)->update([ + "matched" => $matched + ]); + + return $reconciliation; + } + + public function syncTransactions(Reconciliation $reconciliation) { + $extraTransactions = $reconciliation->account->transactionsToReconcile(null, $reconciliation->date); + $reconciliation->addEntries($extraTransactions->toArray()); + $reconciliation->checkStatus(); + + return $reconciliation; + } + + public function checkOpenReconciliation(Account $account, Transaction $transaction) { + $reconciliation = Reconciliation::where([ + 'account_id' => $account->id, + 'status' => Reconciliation::STATUS_PENDING, + ]) + ->where('date', '>=', $transaction->date); + + if (!$reconciliation) return; + + return $this->syncTransactions($reconciliation); + } } diff --git a/app/Domains/Transaction/routes.php b/app/Domains/Transaction/routes.php index 3ec6b40e..f394e502 100644 --- a/app/Domains/Transaction/routes.php +++ b/app/Domains/Transaction/routes.php @@ -8,9 +8,14 @@ Route::get('/finance/reconciliation/accounts/{account}', [ReconciliationController::class, 'create']); Route::post('/finance/reconciliation/accounts/{account}', [ReconciliationController::class, 'store']); Route::get('/finance/accounts/{account}/reconciliations', [ReconciliationController::class, 'accountReconciliations']); + Route::get('/finance/reconciliation/{reconciliation}', [ReconciliationController::class, 'show']); Route::put('/finance/reconciliation/{reconciliation}/save-adjustment', [ReconciliationController::class, 'adjustment']); Route::put('/finance/reconciliation/{reconciliation}', [ReconciliationController::class, 'update']); + Route::put('/finance/reconciliation/{reconciliation}/sync-transactions', [ReconciliationController::class, 'syncTransactions']); + Route::delete('/finance/reconciliation/{reconciliation}', [ReconciliationController::class, 'delete']); + + Route::put('/finance/reconciliation/{reconciliation}/reconciliation-entries/{reconciliationEntry}/check', [ReconciliationController::class, 'checkReconciliationEntry']); }); diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index edf2773b..584ad335 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Domains\Transaction\Listeners\UpdateOpenReconciliations; use App\Events\AutomationEvent; use App\Events\BudgetAssigned; use App\Events\OccurrenceCreated; @@ -56,7 +57,8 @@ class EventServiceProvider extends ServiceProvider TransactionCreated::class => [ CreateBudgetTransactionMovement::class, HandleTransactionCreated::class, - CheckOccurrence::class + CheckOccurrence::class, + UpdateOpenReconciliations::class ], // App events AppCreated::class => [ diff --git a/components.d.ts b/components.d.ts index a970b941..b1e2199e 100644 --- a/components.d.ts +++ b/components.d.ts @@ -15,6 +15,7 @@ declare module 'vue' { IMdiBankTransferIn: typeof import('~icons/mdi/bank-transfer-in')['default'] IMdiBankTransferOut: typeof import('~icons/mdi/bank-transfer-out')['default'] IMdiCallSplit: typeof import('~icons/mdi/call-split')['default'] + IMdiCheck: typeof import('~icons/mdi/check')['default'] IMdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default'] IMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] IMdiClose: typeof import('~icons/mdi/close')['default'] diff --git a/resources/js/Components/OccurrencePreview.vue b/resources/js/Components/OccurrencePreview.vue index 5b39f0b2..e37789f3 100644 --- a/resources/js/Components/OccurrencePreview.vue +++ b/resources/js/Components/OccurrencePreview.vue @@ -1,7 +1,7 @@ diff --git a/resources/js/Components/shared/BaseTable.vue b/resources/js/Components/shared/BaseTable.vue index 859bbb00..28a0de4b 100644 --- a/resources/js/Components/shared/BaseTable.vue +++ b/resources/js/Components/shared/BaseTable.vue @@ -28,6 +28,7 @@ const props = withDefaults( responsive?: boolean; tableClass?: string; layout?: string; + height?: number }>(), { summaryMethod: (data: { columns: TableColumnCtx[]; data: any[] }) => { @@ -93,7 +94,7 @@ defineExpose({
@@ -101,7 +102,7 @@ defineExpose({
diff --git a/resources/js/Pages/Finance/Index.vue b/resources/js/Pages/Finance/Index.vue index 19a923c5..11a7211c 100644 --- a/resources/js/Pages/Finance/Index.vue +++ b/resources/js/Pages/Finance/Index.vue @@ -15,7 +15,7 @@ import FinanceVarianceCard from "@/Components/molecules/FinanceVarianceCard.vue" import FinanceTemplate from "./Partials/FinanceTemplate.vue"; import FinanceSectionNav from "./Partials/FinanceSectionNav.vue"; -import TransactionsTable from "@/domains/transactions/components/TransactionsTable.vue"; +import TransactionsTable from "@/domains/transactions/components/TransactionsList.vue"; import CategoryTrendsPreview from "@/domains/transactions/components/CategoryTrendsPreview.vue"; import BudgetProgress from "@/domains/budget/components/BudgetProgress.vue"; @@ -273,8 +273,8 @@ const deleteBulkTransactions = () => { /> - { Cancel - - Delete Team diff --git a/resources/js/Pages/Finance/Partials/AccountReconciliationBanner.vue b/resources/js/Pages/Finance/Partials/AccountReconciliationBanner.vue index 4b3675bf..05d618f5 100644 --- a/resources/js/Pages/Finance/Partials/AccountReconciliationBanner.vue +++ b/resources/js/Pages/Finance/Partials/AccountReconciliationBanner.vue @@ -24,7 +24,7 @@ const adjustAndFinish = () => { } const differenceStateText = computed(() => { - return (account.reconciliations_pending?.difference || 0) > (account.reconciliations_pending?.amount ?? 0) ? 'higher' : 'lower' + return (account.reconciliations_pending?.difference || 0) < (account.reconciliations_pending?.amount ?? 0) ? 'higher' : 'lower' }) diff --git a/resources/js/Pages/Finance/Reconciliation/AccountReconciliations.vue b/resources/js/Pages/Finance/Reconciliation/AccountReconciliations.vue index 2ad1e1f1..83a5d7fe 100644 --- a/resources/js/Pages/Finance/Reconciliation/AccountReconciliations.vue +++ b/resources/js/Pages/Finance/Reconciliation/AccountReconciliations.vue @@ -8,7 +8,7 @@ import AppLayout from "@/Components/templates/AppLayout.vue"; import FinanceTemplate from "../Partials/FinanceTemplate.vue"; import FinanceSectionNav from "../Partials/FinanceSectionNav.vue"; import TransactionSearch from "@/domains/transactions/components/TransactionSearch.vue"; -import TransactionTemplate from "@/domains/transactions/components/TransactionTemplate.vue"; +import TransactionTable from "@/domains/transactions/components/TransactionTable.vue"; import { useTransactionModal } from "@/domains/transactions"; // import { IServerSearchData, useServerSearch } from "@/composables/useServerSearch"; @@ -44,7 +44,7 @@ provide("selectedAccountId", accountId); const context = useAppContextStore(); const listComponent = computed(() => { - return context.isMobile ? TransactionSearch : TransactionTemplate; + return context.isMobile ? TransactionSearch : TransactionTable; }); const removeTransaction = (transaction: ITransaction) => { @@ -104,7 +104,7 @@ onMounted(() => { /> -
+
{ const context = useAppContextStore(); const listComponent = computed(() => { - return context.isMobile ? TransactionSearch : TransactionTemplate; + return context.isMobile ? TransactionSearch : TransactionTable; }); const removeTransaction = (transaction: ITransaction) => { @@ -125,11 +125,11 @@ const toggleEditing = () => {
- + 0 of {{ transactions.length }} - + { :number-format="true" :disabled="!isEditing" @blur="isEditing = false" - + >