diff --git a/app/Domains/Journal/Actions/TransactionBulkApprove.php b/app/Domains/Journal/Actions/TransactionBulkApprove.php index eedc64cc..23b25709 100644 --- a/app/Domains/Journal/Actions/TransactionBulkApprove.php +++ b/app/Domains/Journal/Actions/TransactionBulkApprove.php @@ -4,14 +4,14 @@ use Illuminate\Foundation\Auth\User; use Illuminate\Support\Facades\Gate; -use Insane\Journal\Contracts\TransactionBulkApproves; use Insane\Journal\Models\Core\Transaction; +use Insane\Journal\Contracts\TransactionBulkApproves; class TransactionBulkApprove implements TransactionBulkApproves { public function validate(User $user) { - Gate::forUser($user)->authorize('update', Transaction::class); + Gate::forUser($user)->authorize('updateBulk', Transaction::class); } public function approveAllDrafts(User $user) diff --git a/app/Domains/Journal/Policies/TransactionPolicy.php b/app/Domains/Journal/Policies/TransactionPolicy.php index de24a3fb..91f14372 100644 --- a/app/Domains/Journal/Policies/TransactionPolicy.php +++ b/app/Domains/Journal/Policies/TransactionPolicy.php @@ -3,8 +3,8 @@ namespace App\Domains\Journal\Policies; use App\Models\User; -use Illuminate\Auth\Access\HandlesAuthorization; use Insane\Journal\Models\Core\Transaction; +use Illuminate\Auth\Access\HandlesAuthorization; class TransactionPolicy { @@ -25,6 +25,11 @@ public function update(User $user, Transaction $transaction) return $user->current_team_id == $transaction->team_id; } + public function updateBulk(User $user) + { + return $user->current_team_id; + } + public function delete(User $user, Transaction $transaction) { return $user->current_team_id == $transaction->team_id; diff --git a/app/Domains/LogerProfile/Http/Controllers/LogerProfileController.php b/app/Domains/LogerProfile/Http/Controllers/LogerProfileController.php index 85483e46..7968faae 100644 --- a/app/Domains/LogerProfile/Http/Controllers/LogerProfileController.php +++ b/app/Domains/LogerProfile/Http/Controllers/LogerProfileController.php @@ -2,10 +2,11 @@ namespace App\Domains\LogerProfile\Http\Controllers; -use App\Domains\LogerProfile\Data\LogerProfileData; -use App\Domains\LogerProfile\Services\LogerProfileService; +use App\Models\Setting; use App\Http\Controllers\Controller; +use App\Domains\LogerProfile\Data\LogerProfileData; use App\Http\Controllers\Traits\HasEnrichedRequest; +use App\Domains\LogerProfile\Services\LogerProfileService; class LogerProfileController extends Controller { @@ -28,7 +29,6 @@ public function store(LogerProfileService $profileService) public function show(LogerProfileService $profileService, int $profileId) { - return inertia('LogerProfile/ProfileView', [ 'profiles' => $profileService->list(auth()->user()->current_team_id), 'profile' => $profileService->getById($profileId), @@ -37,4 +37,14 @@ public function show(LogerProfileService $profileService, int $profileId) }, ]); } + + public function transactions(int $profileId, LogerProfileService $profileService) + { + $queryParams = request()->query(); + + $filters = isset($queryParams['filter']) ? $queryParams['filter'] : []; + [$startDate, $endDate] = $this->getFilterDates($filters); + + return $profileService->getTransactionsByProfileId($profileId, $startDate, $endDate); + } } diff --git a/app/Domains/LogerProfile/Http/Controllers/LogerProfileEntityController.php b/app/Domains/LogerProfile/Http/Controllers/LogerProfileEntityController.php index 89aef381..3be1692c 100644 --- a/app/Domains/LogerProfile/Http/Controllers/LogerProfileEntityController.php +++ b/app/Domains/LogerProfile/Http/Controllers/LogerProfileEntityController.php @@ -2,10 +2,10 @@ namespace App\Domains\LogerProfile\Http\Controllers; -use App\Domains\LogerProfile\Data\ProfileEntityData; -use App\Domains\LogerProfile\Services\LogerProfileService; use App\Http\Controllers\Controller; use App\Http\Controllers\Traits\HasEnrichedRequest; +use App\Domains\LogerProfile\Data\ProfileEntityData; +use App\Domains\LogerProfile\Services\LogerProfileService; class LogerProfileEntityController extends Controller { diff --git a/app/Domains/LogerProfile/Services/LogerProfileService.php b/app/Domains/LogerProfile/Services/LogerProfileService.php index 5d6032bc..edb0bf5c 100644 --- a/app/Domains/LogerProfile/Services/LogerProfileService.php +++ b/app/Domains/LogerProfile/Services/LogerProfileService.php @@ -2,10 +2,13 @@ namespace App\Domains\LogerProfile\Services; +use App\Domains\AppCore\Models\Category; +use App\Domains\LogerProfile\Models\LogerProfile; use App\Domains\LogerProfile\Data\LogerProfileData; +use App\Domains\Transaction\Models\TransactionLine; use App\Domains\LogerProfile\Data\ProfileEntityData; -use App\Domains\LogerProfile\Models\LogerProfile; use App\Domains\LogerProfile\Models\LogerProfileEntity; +use App\Domains\Transaction\Services\TransactionService; class LogerProfileService { @@ -40,4 +43,28 @@ public function getEntitiesByProfileId(int $profileId) 'profile_id' => $profileId, ])->get()); } + + public function getTransactionsByProfileId(int $profileId, $startDate, $endDate) + { + $entities = LogerProfileEntity::where([ + 'profile_id' => $profileId, + 'entity_type' => Category::class + ])->get(); + + $categories = $entities->map(fn ($entity) => $entity->entity->id)->all(); + + $teamId = $entities[0]->team_id; + + $transactions = TransactionLine::byTeam($teamId) + ->inDateFrame($startDate, $endDate) + ->expenseCategories($categories) + ->verified() + ->orderByDesc('transactions.date') + ->get(); + + return [ + "data" => $transactions, + "total" => $transactions->sum('total'), + ]; + } } diff --git a/app/Domains/LogerProfile/routes.php b/app/Domains/LogerProfile/routes.php index 2309cc30..f266f3f3 100644 --- a/app/Domains/LogerProfile/routes.php +++ b/app/Domains/LogerProfile/routes.php @@ -1,11 +1,12 @@ group(function () { // Route::resource('/loger-profiles', [LogerProfileController::class, 'index'])->name('profiles.index'); Route::resource('/loger-profiles', LogerProfileController::class); Route::resource('/loger-profiles/{profileId}/entities', LogerProfileEntityController::class); + Route::get('/loger-profiles/{profileId}/transactions', [LogerProfileController::class, 'transactions']); }); diff --git a/app/Domains/Transaction/Services/ReportService.php b/app/Domains/Transaction/Services/ReportService.php index ef7e09c0..56d3af0d 100644 --- a/app/Domains/Transaction/Services/ReportService.php +++ b/app/Domains/Transaction/Services/ReportService.php @@ -2,10 +2,10 @@ namespace App\Domains\Transaction\Services; -use App\Domains\Transaction\Models\Transaction; -use App\Domains\Transaction\Models\TransactionLine; use Carbon\Carbon; use Illuminate\Support\Facades\DB; +use App\Domains\Transaction\Models\Transaction; +use App\Domains\Transaction\Models\TransactionLine; class ReportService { @@ -40,12 +40,12 @@ public function revenueReport($teamId, $methodName = 'payments') return $results; } - public static function generateExpensesByPeriod($teamId, $timeUnit = 'month', $timeUnitDiff = 2, $type = 'expenses') + public static function generateExpensesByPeriod($teamId, $startDate, $timeUnitDiff = 2, $timeUnit = 'month') { - $endDate = Carbon::now()->endOfMonth()->format('Y-m-d'); - $startDate = Carbon::now()->subMonth($timeUnitDiff)->startOfMonth()->format('Y-m-d'); + $rangeEndAt = Carbon::createFromFormat('Y-m-d', $startDate)->endOfMonth()->format('Y-m-d'); + $rangeStartAt = Carbon::now()->subMonth($timeUnitDiff)->startOfMonth()->format('Y-m-d'); - $results = self::getExpensesByCategoriesInPeriod($teamId, $startDate, $endDate); + $results = self::getExpensesByCategoriesInPeriod($teamId, $rangeStartAt, $rangeEndAt); $resultGroup = $results->groupBy('date'); return $resultGroup->map(function ($monthItems) { @@ -57,6 +57,36 @@ public static function generateExpensesByPeriod($teamId, $timeUnit = 'month', $t }, $resultGroup)->sortBy('date'); } + public static function getIncomeVsExpenses($teamId, $timeUnitDiff = 2, $startDate = null, $timeUnit = 'month') + { + $endDate = Carbon::now()->endOfMonth()->format('Y-m-d'); + $startDate = Carbon::now()->subMonth($timeUnitDiff)->startOfMonth()->format('Y-m-d'); + + $expenses = self::getExpensesByCategoriesInPeriod($teamId, $startDate, $endDate); + $expensesGroup = $expenses->groupBy('date'); + + + + $income = self::getIncomeByPayeeInPeriod($teamId, $startDate, $endDate); + $incomeCategories = $income->groupBy('date'); + + $dates = $expensesGroup->keys(); + + + return $dates->map(function ($month) use ($incomeCategories, $expensesGroup) { + $incomeData = $incomeCategories->get($month); + $expenseData = $expensesGroup->get($month); + return [ + 'date' => $month, + 'month_date' => $month, + 'income' => $incomeData?->values()->all() ?? [], + "expense" => $expenseData?->values()->all() ?? [], + 'assets' => $incomeData?->sum('total_amount') ?? 0, + 'debts' => $expenseData?->sum('total') ?? 0, + ]; + })->sortBy('date')->values()->toArray(); + } + public static function generateCurrentPreviousReport($teamId, $timeUnit = 'month', $timeUnitDiff = 2, $type = 'expenses') { $endDate = Carbon::now()->endOfMonth()->format('Y-m-d'); @@ -101,6 +131,19 @@ public static function getExpensesByYear($year, $teamId) ->get(); } + public static function getIncomeByPayeeInPeriod($teamId, $startDate, $endDate) + { + return TransactionLine::byTeam($teamId) + ->balance() + ->inDateFrame($startDate, $endDate) + ->incomePayees() + ->selectRaw('date_format(transaction_lines.date, "%Y-%m-%01") as date, payees.name, payees.id') + ->groupByRaw('date_format(transaction_lines.date, "%Y-%m"), payees.id') + ->orderByDesc('date') + ->join('transactions', 'transactions.id', 'transaction_lines.transaction_id') + ->get(); + } + public static function getExpensesByCategoriesInPeriod($teamId, $startDate, $endDate, $categories = null) { return Transaction::byTeam($teamId) diff --git a/app/Domains/Transaction/Traits/TransactionLineTrait.php b/app/Domains/Transaction/Traits/TransactionLineTrait.php index c3c2c84d..4285410b 100644 --- a/app/Domains/Transaction/Traits/TransactionLineTrait.php +++ b/app/Domains/Transaction/Traits/TransactionLineTrait.php @@ -2,9 +2,9 @@ namespace App\Domains\Transaction\Traits; -use App\Domains\Budget\Data\BudgetReservedNames; use Illuminate\Support\Facades\DB; use Insane\Journal\Models\Core\Transaction; +use App\Domains\Budget\Data\BudgetReservedNames; trait TransactionLineTrait { @@ -91,6 +91,19 @@ public function scopeExpenseCategories($query, array $categories = null) return $query; } + public function scopeIncomePayees($query, array $payees = null) + { + $query->where('categories.name', BudgetReservedNames::READY_TO_ASSIGN->value) + ->join('categories', 'transaction_lines.category_id', '=', 'categories.id') + ->join('payees', 'transaction_lines.payee_id', '=', 'payees.id'); + + if ($payees) { + $query->whereIn('transaction_lines.payee_id', $payees); + } + + return $query; + } + public function scopePayees($query, array $payees) { return $query->whereIn('transaction_lines.payee_id', $payees) diff --git a/app/Http/Controllers/Finance/FinanceTrendController.php b/app/Http/Controllers/Finance/FinanceTrendController.php index 4b336b5c..faf016d1 100644 --- a/app/Http/Controllers/Finance/FinanceTrendController.php +++ b/app/Http/Controllers/Finance/FinanceTrendController.php @@ -21,6 +21,7 @@ class FinanceTrendController extends Controller 'payees' => 'payee', 'net-worth' => 'NetWorth', 'income-expenses' => 'IncomeExpenses', + 'spending-year' => 'spendingYear', 'income-expenses-graph' => 'IncomeExpensesGraph', 'year-summary' => 'yearSummary', ]; @@ -143,17 +144,40 @@ public function incomeExpensesGraph() $teamId = request()->user()->current_team_id; return [ - 'data' => ReportService::generateExpensesByPeriod($teamId, 'month', 12), + 'data' => ReportService::getIncomeVsExpenses($teamId, 12), 'metaData' => [ 'name' => 'incomeExpensesGraph', 'title' => 'Income vs Expenses', 'props' => [ 'headerTemplate' => 'grid', + "assetsLabel" => "income", + "debtsLabel" => "expense" ], ], ]; } + public function spendingYear() + { + $queryParams = request()->query(); + $filters = isset($queryParams['filter']) ? $queryParams['filter'] : []; + [$startDate, $endDate] = $this->getFilterDates($filters); + $teamId = request()->user()->current_team_id; + + return [ + 'data' => ReportService::generateExpensesByPeriod($teamId, $startDate, 12), + 'metaData' => [ + 'name' => 'spendingYear', + 'title' => 'Expenses', + 'props' => [ + 'headerTemplate' => 'grid', + ], + ], + ]; + } + + + public function yearSummary() { // $queryParams = request()->query(); diff --git a/app/Http/Controllers/System/DashboardController.php b/app/Http/Controllers/System/DashboardController.php index 2577dcda..3e7cfb37 100644 --- a/app/Http/Controllers/System/DashboardController.php +++ b/app/Http/Controllers/System/DashboardController.php @@ -39,7 +39,7 @@ public function __invoke() 'budgetTotal' => $budget, 'transactionTotal' => $transactionsTotal, 'expenses' => ReportService::generateCurrentPreviousReport($teamId, 'month', 1), - 'revenue' => ReportService::generateExpensesByPeriod($teamId), + 'revenue' => ReportService::generateExpensesByPeriod($teamId, $startDate), 'onboarding' => function () use ($team) { $onboarding = $team->onboarding(); diff --git a/components.d.ts b/components.d.ts index 0c1c4cd3..c48e99ae 100644 --- a/components.d.ts +++ b/components.d.ts @@ -7,12 +7,10 @@ export {} declare module 'vue' { export interface GlobalComponents { - IClarityContractLine: typeof import('~icons/clarity/contract-line')['default'] IFluentFoodApple20Filled: typeof import('~icons/fluent/food-apple20-filled')['default'] IIcRoundQueryStats: typeof import('~icons/ic/round-query-stats')['default'] IIonEllipsisVertical: typeof import('~icons/ion/ellipsis-vertical')['default'] IMaterialSymbolsBrightnessAlertOutlineRounded: typeof import('~icons/material-symbols/brightness-alert-outline-rounded')['default'] - IMaterialSymbolsHomeWorkOutline: typeof import('~icons/material-symbols/home-work-outline')['default'] IMdiBankTransfer: typeof import('~icons/mdi/bank-transfer')['default'] IMdiBankTransferIn: typeof import('~icons/mdi/bank-transfer-in')['default'] IMdiBankTransferOut: typeof import('~icons/mdi/bank-transfer-out')['default'] @@ -22,10 +20,7 @@ declare module 'vue' { IMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] IMdiClose: typeof import('~icons/mdi/close')['default'] IMdiEdit: typeof import('~icons/mdi/edit')['default'] - IMdiEllipsis: typeof import('~icons/mdi/ellipsis')['default'] - IMdiEllipsisV: typeof import('~icons/mdi/ellipsis-v')['default'] IMdiEllipsisVertical: typeof import('~icons/mdi/ellipsis-vertical')['default'] - IMdiEllipsys: typeof import('~icons/mdi/ellipsys')['default'] IMdiExport: typeof import('~icons/mdi/export')['default'] IMdiFile: typeof import('~icons/mdi/file')['default'] IMdiFilter: typeof import('~icons/mdi/filter')['default'] @@ -41,5 +36,6 @@ declare module 'vue' { IMdiStarOutline: typeof import('~icons/mdi/star-outline')['default'] IMdiSync: typeof import('~icons/mdi/sync')['default'] IMdiTrash: typeof import('~icons/mdi/trash')['default'] + IMdiWallet: typeof import('~icons/mdi/wallet')['default'] } } diff --git a/package-lock.json b/package-lock.json index 064e77e1..c5f4ef72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@iconify-json/material-symbols": "^1.1.57", "@iconify-json/mdi": "^1.1.54", "@inertiajs/vue3": "^1.0.11", + "@mertasan/tailwindcss-variables": "^2.7.0", "@tailwindcss/typography": "^0.5.9", "@vitejs/plugin-vue": "^4.3.1", "@vue/server-renderer": "^3.3.4", @@ -3793,6 +3794,22 @@ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.1.tgz", "integrity": "sha512-hW0GwZj06z/ZFUW2Espl7toVDjghJN+EKqyXzPSV8NV89d5BYp5rRMBJoc+aUN0x5OXDMeRQHazejr2Xmqj2tw==" }, + "node_modules/@mertasan/tailwindcss-variables": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@mertasan/tailwindcss-variables/-/tailwindcss-variables-2.7.0.tgz", + "integrity": "sha512-rKPhxi/0r6XWP0+OjPmsfrloX/TtQmvONj2Pr3Nl8BNBznQVP3M9sphguDBUDC0AiKYx2xgup3XzAhlIDLPLIA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "autoprefixer": "^10.0.2", + "postcss": "^8.0.9" + } + }, "node_modules/@nicolo-ribaudo/semver-v6": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", @@ -18310,6 +18327,15 @@ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.1.tgz", "integrity": "sha512-hW0GwZj06z/ZFUW2Espl7toVDjghJN+EKqyXzPSV8NV89d5BYp5rRMBJoc+aUN0x5OXDMeRQHazejr2Xmqj2tw==" }, + "@mertasan/tailwindcss-variables": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@mertasan/tailwindcss-variables/-/tailwindcss-variables-2.7.0.tgz", + "integrity": "sha512-rKPhxi/0r6XWP0+OjPmsfrloX/TtQmvONj2Pr3Nl8BNBznQVP3M9sphguDBUDC0AiKYx2xgup3XzAhlIDLPLIA==", + "dev": true, + "requires": { + "lodash": "^4.17.21" + } + }, "@nicolo-ribaudo/semver-v6": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", diff --git a/package.json b/package.json index e74f145e..6c01b147 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@iconify-json/material-symbols": "^1.1.57", "@iconify-json/mdi": "^1.1.54", "@inertiajs/vue3": "^1.0.11", + "@mertasan/tailwindcss-variables": "^2.7.0", "@tailwindcss/typography": "^0.5.9", "@vitejs/plugin-vue": "^4.3.1", "@vue/server-renderer": "^3.3.4", diff --git a/resources/js/Components/atoms/LogerButtonCircle.vue b/resources/js/Components/atoms/LogerButtonCircle.vue index 25795dba..d3fcb6a0 100644 --- a/resources/js/Components/atoms/LogerButtonCircle.vue +++ b/resources/js/Components/atoms/LogerButtonCircle.vue @@ -1,13 +1,15 @@ + + + - diff --git a/resources/js/Components/molecules/SectionNav.vue b/resources/js/Components/molecules/SectionNav.vue index 056d5c4a..9366aad1 100644 --- a/resources/js/Components/molecules/SectionNav.vue +++ b/resources/js/Components/molecules/SectionNav.vue @@ -8,6 +8,7 @@ import { NDropdown } from 'naive-ui'; interface NavSection { url?: string; + action?: string; value: string; label: string; } @@ -17,7 +18,7 @@ const props = defineProps<{ modelValue?: string; }>(); -const emit = defineEmits(['update:modelValue']); +const emit = defineEmits(['update:modelValue', 'action']); const currentPath = computed(() => { return document?.location?.pathname @@ -32,6 +33,8 @@ const isSelected = (section: NavSection) => { const handleClick = (section: NavSection) => { if (section?.url) { router.visit(section.url) + } else if (section?.action) { + emit('action', section.action) } else { emit('update:modelValue', section?.value) } diff --git a/resources/js/Components/organisms/LogerApiSimpleSelect.vue b/resources/js/Components/organisms/LogerApiSimpleSelect.vue index e727aaed..274d948d 100644 --- a/resources/js/Components/organisms/LogerApiSimpleSelect.vue +++ b/resources/js/Components/organisms/LogerApiSimpleSelect.vue @@ -1,31 +1,54 @@ diff --git a/resources/js/Components/organisms/logerAssistant.vue b/resources/js/Components/organisms/logerAssistant.vue new file mode 100644 index 00000000..029ef141 --- /dev/null +++ b/resources/js/Components/organisms/logerAssistant.vue @@ -0,0 +1,62 @@ + + + + + + diff --git a/resources/js/Components/templates/AppLayout.vue b/resources/js/Components/templates/AppLayout.vue index 503bcd04..8c41447f 100644 --- a/resources/js/Components/templates/AppLayout.vue +++ b/resources/js/Components/templates/AppLayout.vue @@ -1,6 +1,5 @@