diff --git a/app/Console/Commands/CheckMonthRollover.php b/app/Console/Commands/CheckMonthRollover.php index f99cd815..c7ae73a7 100644 --- a/app/Console/Commands/CheckMonthRollover.php +++ b/app/Console/Commands/CheckMonthRollover.php @@ -2,9 +2,10 @@ namespace App\Console\Commands; +use DateTime; use App\Models\Team; -use Illuminate\Console\Command; +use Illuminate\Console\Command; use App\Domains\Budget\Services\BudgetRolloverService; class CheckMonthRollover extends Command @@ -30,7 +31,16 @@ public function handle(BudgetRolloverService $rolloverService) { $teams = Team::with('timezone')->without(['settings'])->get(); - dd($teams->toArray()); - + $now = now(); + foreach ($teams as $team) { + $timezone = $team["timezone"] ?? null; + if (!$timezone) continue; + + $now->setTimezone($timezone['value']); + if ($now->format('H:i') === '00:00' && $now->format('Y-m-d')) { + $previousMonth = $now->subMonth()->startOf('month')->format('Y-m'); + $rolloverService->startFrom($team->id, $previousMonth); + } + } } } diff --git a/app/Console/Commands/MonthlyRollover.php b/app/Console/Commands/MonthlyRollover.php index be044e90..3bb12b8b 100644 --- a/app/Console/Commands/MonthlyRollover.php +++ b/app/Console/Commands/MonthlyRollover.php @@ -31,6 +31,5 @@ public function handle(BudgetRolloverService $rolloverService) $month = $this->argument('month'); $rolloverService->rollMonth($teamId, $month."-01"); - } } diff --git a/app/Console/Commands/TeamBudgetRollover.php b/app/Console/Commands/TeamBudgetRollover.php index 98d647e8..f006b698 100644 --- a/app/Console/Commands/TeamBudgetRollover.php +++ b/app/Console/Commands/TeamBudgetRollover.php @@ -4,8 +4,6 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; -use App\Domains\AppCore\Models\Category; -use App\Domains\Budget\Data\BudgetReservedNames; use App\Domains\Budget\Services\BudgetRolloverService; class TeamBudgetRollover extends Command @@ -15,7 +13,7 @@ class TeamBudgetRollover extends Command * * @var string */ - protected $signature = 'app:team-budget-rollover {teamId} ?{date}'; + protected $signature = 'app:team-budget-rollover {teamId} {date?}'; /** * The console command description. @@ -33,7 +31,12 @@ public function handle(BudgetRolloverService $rolloverService) $teamId = $this->argument('teamId'); $date = $this->argument('date'); - $monthsWithTransactions = DB::table('transaction_lines') + $monthsWithTransactions = $this->getFirstTransaction($teamId); + $rolloverService->startFrom($teamId, $date ?? $monthsWithTransactions->date); + } + + private function getFirstTransaction(int $teamId) { + return DB::table('transaction_lines') ->where([ "team_id" => $teamId ]) @@ -41,7 +44,5 @@ public function handle(BudgetRolloverService $rolloverService) ->groupBy(DB::raw("date_format(transaction_lines.date, '%Y-%m')")) ->orderBy('date') ->first(); - - $rolloverService->startFrom($teamId, $date ?? $monthsWithTransactions->date); } } diff --git a/app/Domains/Budget/Models/BudgetMonth.php b/app/Domains/Budget/Models/BudgetMonth.php index fab0a043..552f32fd 100644 --- a/app/Domains/Budget/Models/BudgetMonth.php +++ b/app/Domains/Budget/Models/BudgetMonth.php @@ -28,6 +28,12 @@ class BudgetMonth extends Model 'payments', 'left_from_last_month', 'overspending_previous_month', + 'accounts_balance', + 'meta_data' + ]; + + protected $casts = [ + 'meta_data' => 'array' ]; public function category() diff --git a/app/Domains/Budget/Models/BudgetTarget.php b/app/Domains/Budget/Models/BudgetTarget.php index fa727378..4c6815c0 100644 --- a/app/Domains/Budget/Models/BudgetTarget.php +++ b/app/Domains/Budget/Models/BudgetTarget.php @@ -47,9 +47,10 @@ public static function getNextTargets($teamId, $targetTypes = ['spending']) 'frequency' => 'monthly', 'budget_targets.team_id' => $teamId, ]) - ->whereRaw("concat(date_format(now(), '%Y-%m'), '-', frequency_month_date) >= now()") - ->addSelect(DB::raw("budget_targets.*, concat(date_format(now(), '%Y-%m'), '-', frequency_month_date) as due_date")) + ->whereRaw("concat(date_format(now(), '%Y-%m'), '-', LPAD(frequency_month_date, 2, '0')) >= date_format(now(), '%Y-%m-%d')") + ->addSelect(DB::raw("budget_targets.*, concat(date_format(now(), '%Y-%m'), '-', LPAD(frequency_month_date, 2, '0')) as due_date")) ->from('budget_targets') + ->orderByRaw('due_date') ->get(); } diff --git a/app/Domains/Budget/Services/BudgetRolloverService.php b/app/Domains/Budget/Services/BudgetRolloverService.php index 9c530df5..f9583389 100644 --- a/app/Domains/Budget/Services/BudgetRolloverService.php +++ b/app/Domains/Budget/Services/BudgetRolloverService.php @@ -2,15 +2,21 @@ namespace App\Domains\Budget\Services; +use App\Models\Team; use Brick\Money\Money; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; +use Insane\Journal\Models\Core\Account; use Insane\Journal\Models\Core\Category; use App\Domains\Budget\Models\BudgetMonth; use App\Domains\Budget\Data\BudgetAssignData; use App\Domains\Budget\Data\BudgetReservedNames; +use Insane\Journal\Models\Core\AccountDetailType; class BudgetRolloverService { + private Team|null $team = null; + private mixed $accounts = []; + public function __construct(private BudgetCategoryService $budgetCategoryService) {} public function rollMonth($teamId, $month, $categories = null) { @@ -92,8 +98,12 @@ private function moveReadyToAssign($teamId, $month, $overspending = 0, $fundedFr coalesce(sum(budgeted), 0) as budgeted, coalesce(sum(activity), 0) as budgetsActivity, coalesce(sum(payments), 0) as payments, + coalesce(sum(available), 0) as available, + sum(CASE WHEN available < 0 THEN available ELSE 0 END) as overspendingInMonth, coalesce(sum(funded_spending), 0) as funded_spending - ")->groupBy('month') + ") + ->groupBy('month') + // ->dd(); ->first(); $budgetMonth = BudgetMonth::where([ @@ -105,11 +115,31 @@ private function moveReadyToAssign($teamId, $month, $overspending = 0, $fundedFr $inflow = (new BudgetCategoryService($readyToAssignCategory))->getCategoryActivity($readyToAssignCategory, $month); $positiveAmount = $budgetMonth->left_from_last_month + $inflow + $results?->funded_spending; - $available = $positiveAmount - $results?->budgeted ; + $available = $positiveAmount - $results?->budgeted; $nextMonth = Carbon::createFromFormat("Y-m-d", $month)->addMonthsWithNoOverflow(1)->format('Y-m-d'); + $overspending = abs($results?->overspendingInMonth); + $leftover = $inflow - $results?->budgeted; + + echo "Leftover: ". $leftover . "overspending: " . $overspending . PHP_EOL; + + if ($overspending > 0 && $leftover > 0) { + $overspendingCopy = $overspending; + $overspending = $overspending > $leftover ? $overspending - $leftover : 0; + $leftover = $overspendingCopy >= $leftover ? 0 : $leftover - $overspendingCopy; + } + + + if ($leftover < 0) { + $overspending = $overspending > abs($leftover); + $leftover = 0; + + } // close current month + + $details = $this->team->balanceDetail(Carbon::createFromFormat("Y-m-d", $month)->endOfMonth()->format('Y-m-d'), $this->accounts); + BudgetMonth::updateOrCreate([ 'category_id' => $readyToAssignCategory->id, 'team_id' => $readyToAssignCategory->team_id, @@ -119,9 +149,11 @@ private function moveReadyToAssign($teamId, $month, $overspending = 0, $fundedFr 'user_id' => $readyToAssignCategory->user_id, 'budgeted' => $results?->budgeted, 'activity' => $inflow, - 'available' => $available, + 'available' => $results?->available, 'funded_spending' => $results?->funded_spending ?? 0, 'payments' => $results?->payments ?? 0, + "accounts_balance" => collect($details)->sum('balance'), + "meta_data" => $details ]); // set left over to the next month @@ -132,8 +164,8 @@ private function moveReadyToAssign($teamId, $month, $overspending = 0, $fundedFr 'name' => $nextMonth, ], [ 'user_id' => $readyToAssignCategory->user_id, - 'left_from_last_month' => $inflow - $results?->budgeted, - 'overspending_previous_month' => $available < 0 ? $available : $available, + 'left_from_last_month' => $leftover, + 'overspending_previous_month' => $overspending, ]); } @@ -157,6 +189,9 @@ private function reduceOverspent() { } public function startFrom($teamId, $yearMonth) { + $this->team = Team::find($teamId); + $this->accounts = Account::getByDetailTypes($teamId, AccountDetailType::ALL_CASH)->pluck('id'); + $categories = Category::where([ 'team_id' => $teamId, ]) @@ -171,6 +206,11 @@ public function startFrom($teamId, $yearMonth) { ->get() ->pluck('date'); + $monthsWithTransactions = [ + ...$monthsWithTransactions, + now()->format('Y-m'), + ]; + $total = count($monthsWithTransactions); $count = 0; foreach ($monthsWithTransactions as $month) { diff --git a/app/Domains/Transaction/Traits/TransactionTrait.php b/app/Domains/Transaction/Traits/TransactionTrait.php index 4ed16c34..e891d5c7 100644 --- a/app/Domains/Transaction/Traits/TransactionTrait.php +++ b/app/Domains/Transaction/Traits/TransactionTrait.php @@ -55,7 +55,7 @@ public function scopeExpenses($query) ->whereNotNull('category_id'); } - public function scopeBalance($query) + public function scopeBalance($query, $uptoDate = null, $accountIds = []) { $transactionsTotalSum = "ABS(sum(CASE WHEN transactions.direction = 'WITHDRAW' @@ -65,6 +65,8 @@ public function scopeBalance($query) return $query->where([ 'transactions.status' => 'verified', ]) + ->when($uptoDate, fn($q) => $q->whereRaw('transactions.date <= ?', [$uptoDate])) + ->when(count($accountIds), fn($q) => $q->whereIn('transactions.account_id', [$uptoDate])) ->whereNotNull('category_id') ->selectRaw($transactionsTotalSum); } diff --git a/app/Listeners/CreateBudgetMovement.php b/app/Listeners/CreateBudgetMovement.php index 2edd4c52..c28dff8b 100644 --- a/app/Listeners/CreateBudgetMovement.php +++ b/app/Listeners/CreateBudgetMovement.php @@ -3,10 +3,11 @@ namespace App\Listeners; use App\Events\BudgetAssigned; +use Illuminate\Contracts\Queue\ShouldQueue; use App\Domains\Budget\Services\BudgetCategoryService; use App\Domains\Budget\Services\BudgetRolloverService; -class CreateBudgetMovement +class CreateBudgetMovement implements ShouldQueue { protected $formData; diff --git a/app/Models/Team.php b/app/Models/Team.php index 6e3e4bad..d5693ef3 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -2,19 +2,21 @@ namespace App\Models; -use App\Domains\AppCore\Models\Category; +use Laravel\Paddle\Billable; use App\Domains\Meal\Models\Meal; -use App\Domains\Transaction\Models\Transaction; -use Illuminate\Database\Eloquent\Factories\HasFactory; -use Insane\Journal\Models\Core\Account; +use Illuminate\Support\Facades\DB; use Insane\Journal\Models\Core\Payee; +use Insane\Journal\Models\Core\Account; +use App\Domains\AppCore\Models\Category; +use Spatie\Onboard\Concerns\Onboardable; use Laravel\Jetstream\Events\TeamCreated; use Laravel\Jetstream\Events\TeamDeleted; use Laravel\Jetstream\Events\TeamUpdated; -use Laravel\Jetstream\Team as JetstreamTeam; -use Laravel\Paddle\Billable; use Spatie\Onboard\Concerns\GetsOnboarded; -use Spatie\Onboard\Concerns\Onboardable; +use Laravel\Jetstream\Team as JetstreamTeam; +use App\Domains\Transaction\Models\Transaction; +use App\Domains\Transaction\Models\TransactionLine; +use Illuminate\Database\Eloquent\Factories\HasFactory; class Team extends JetstreamTeam implements Onboardable { @@ -103,12 +105,28 @@ public function meals() * * @return string */ - public function balance() + public function balance($upToDate = null, $accountIds = []) { return (float) Transaction::byTeam($this->id) ->verified() - ->balance() + ->balance($upToDate, $accountIds) ->first() ->total; } + + public function balanceDetail($upToDate = null, $accountIds = []) + { + + return DB::table('transaction_lines')->where([ + 'transactions.status' => Transaction::STATUS_VERIFIED, + 'transactions.team_id' => $this->id, + ]) + ->when($upToDate, fn($q) => $q->whereRaw('transaction_lines.date <= ?', [$upToDate])) + ->when(count($accountIds), fn($q) => $q->whereIn('transaction_lines.account_id', $accountIds)) + ->join('transactions', 'transactions.id', 'transaction_lines.transaction_id') + ->join('accounts', 'accounts.id', 'transaction_lines.account_id') + ->selectRaw("sum(amount * transaction_lines.type) as balance, transaction_lines.account_id, accounts.name") + ->groupBy('accounts.id') + ->get(); + } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 0e6992b6..8c58a483 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,32 +2,32 @@ namespace App\Providers; -use App\Domains\Transaction\Listeners\UpdateOpenReconciliations; -use App\Events\AutomationEvent; use App\Events\BudgetAssigned; +use App\Events\AutomationEvent; use App\Events\Menu\AppCreated; use App\Events\OccurrenceCreated; +use App\Listeners\Menu\ShowInApp; +use App\Listeners\CheckOccurrence; use App\Listeners\AcceptInvitation; +use App\Listeners\TrashTeamSettings; use App\Listeners\AutomationListener; -use App\Listeners\CheckOccurrence; +use App\Listeners\CreateTeamSettings; +use Illuminate\Auth\Events\Registered; use App\Listeners\CreateBudgetCategory; use App\Listeners\CreateBudgetMovement; -use App\Listeners\CreateBudgetTransactionMovement; -use App\Listeners\CreateOccurrenceAutomation; use App\Listeners\CreateStartingBalance; -use App\Listeners\CreateTeamSettings; -use App\Listeners\HandleTransactionCreated; -use App\Listeners\Menu\ShowInApp; -use App\Listeners\TrashTeamSettings; -use Illuminate\Auth\Events\Registered; -use Illuminate\Auth\Listeners\SendEmailVerificationNotification; -use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Insane\Journal\Events\AccountCreated; use Insane\Journal\Events\AccountUpdated; -use Insane\Journal\Events\TransactionCreated; -use Insane\Journal\Listeners\CreateTeamAccounts; use Laravel\Jetstream\Events\TeamCreated; use Laravel\Jetstream\Events\TeamDeleted; +use App\Listeners\HandleTransactionCreated; +use App\Listeners\CreateOccurrenceAutomation; +use Insane\Journal\Events\TransactionCreated; +use Insane\Journal\Listeners\CreateTeamAccounts; +use App\Listeners\CreateBudgetTransactionMovement; +use App\Domains\Transaction\Listeners\UpdateOpenReconciliations; +use Illuminate\Auth\Listeners\SendEmailVerificationNotification; +use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider { @@ -54,21 +54,26 @@ class EventServiceProvider extends ServiceProvider AccountCreated::class => [ CreateBudgetCategory::class, CreateStartingBalance::class, + CreateBudgetMovement::class, ], AccountUpdated::class => [ CreateBudgetCategory::class, + CreateBudgetMovement::class, ], TransactionCreated::class => [ CreateBudgetTransactionMovement::class, HandleTransactionCreated::class, CheckOccurrence::class, UpdateOpenReconciliations::class, + CreateBudgetMovement::class, ], TransactionUpdated::class => [ CreateBudgetTransactionMovement::class, + CreateBudgetMovement::class, ], TransactionDeleted::class => [ CreateBudgetTransactionMovement::class, + CreateBudgetMovement::class, ], // App events AppCreated::class => [ diff --git a/config/pulse.php b/config/pulse.php new file mode 100644 index 00000000..ed66d707 --- /dev/null +++ b/config/pulse.php @@ -0,0 +1,227 @@ + env('PULSE_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Pulse Path + |-------------------------------------------------------------------------- + | + | This is the path which the Pulse dashboard will be accessible from. Feel + | free to change this path to anything you'd like. Note that this won't + | affect the path of the internal API that is never exposed to users. + | + */ + + 'path' => env('PULSE_PATH', 'pulse'), + + /* + |-------------------------------------------------------------------------- + | Pulse Master Switch + |-------------------------------------------------------------------------- + | + | This configuration option may be used to completely disable all Pulse + | data recorders regardless of their individual configurations. This + | provides a single option to quickly disable all Pulse recording. + | + */ + + 'enabled' => env('PULSE_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Pulse Storage Driver + |-------------------------------------------------------------------------- + | + | This configuration option determines which storage driver will be used + | while storing entries from Pulse's recorders. In addition, you also + | may provide any options to configure the selected storage driver. + | + */ + + 'storage' => [ + 'driver' => env('PULSE_STORAGE_DRIVER', 'database'), + + 'database' => [ + 'connection' => env('PULSE_DB_CONNECTION', null), + 'chunk' => 1000, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Pulse Ingest Driver + |-------------------------------------------------------------------------- + | + | This configuration options determines the ingest driver that will be used + | to capture entries from Pulse's recorders. Ingest drivers are great to + | free up your request workers quickly by offloading the data storage. + | + */ + + 'ingest' => [ + 'driver' => env('PULSE_INGEST_DRIVER', 'storage'), + + 'trim_lottery' => [1, 1_000], + + 'redis' => [ + 'connection' => env('PULSE_REDIS_CONNECTION'), + 'chunk' => 1000, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Pulse Cache Driver + |-------------------------------------------------------------------------- + | + | This configuration option determines the cache driver that will be used + | for various tasks, including caching dashboard results, establishing + | locks for events that should only occur on one server and signals. + | + */ + + 'cache' => env('PULSE_CACHE_DRIVER'), + + /* + |-------------------------------------------------------------------------- + | Pulse Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will be assigned to every Pulse route, giving you the + | chance to add your own middleware to this list or change any of the + | existing middleware. Of course, reasonable defaults are provided. + | + */ + + 'middleware' => [ + 'web', + Authorize::class, + ], + + /* + |-------------------------------------------------------------------------- + | Pulse Recorders + |-------------------------------------------------------------------------- + | + | The following array lists the "recorders" that will be registered with + | Pulse, along with their configuration. Recorders gather application + | event data from requests and tasks to pass to your ingest driver. + | + */ + + 'recorders' => [ + Recorders\CacheInteractions::class => [ + 'enabled' => env('PULSE_CACHE_INTERACTIONS_ENABLED', true), + 'sample_rate' => env('PULSE_CACHE_INTERACTIONS_SAMPLE_RATE', 1), + 'ignore' => [ + '/^laravel:pulse:/', // Internal Pulse keys... + '/^illuminate:/', // Internal Laravel keys... + '/^telescope:/', // Internal Telescope keys... + '/^nova/', // Internal Nova keys... + '/^.+@.+\|(?:(?:\d+\.\d+\.\d+\.\d+)|[0-9a-fA-F:]+)(?::timer)?$/', // Breeze / Jetstream authentication rate limiting... + '/^[a-zA-Z0-9]{40}$/', // Session IDs... + ], + 'groups' => [ + '/^job-exceptions:.*/' => 'job-exceptions:*', + // '/:\d+/' => ':*', + ], + ], + + Recorders\Exceptions::class => [ + 'enabled' => env('PULSE_EXCEPTIONS_ENABLED', true), + 'sample_rate' => env('PULSE_EXCEPTIONS_SAMPLE_RATE', 1), + 'location' => env('PULSE_EXCEPTIONS_LOCATION', true), + 'ignore' => [ + // '/^Package\\\\Exceptions\\\\/', + ], + ], + + Recorders\Queues::class => [ + 'enabled' => env('PULSE_QUEUES_ENABLED', true), + 'sample_rate' => env('PULSE_QUEUES_SAMPLE_RATE', 1), + 'ignore' => [ + // '/^Package\\\\Jobs\\\\/', + ], + ], + + Recorders\Servers::class => [ + 'server_name' => env('PULSE_SERVER_NAME', gethostname()), + 'directories' => explode(':', env('PULSE_SERVER_DIRECTORIES', '/')), + ], + + Recorders\SlowJobs::class => [ + 'enabled' => env('PULSE_SLOW_JOBS_ENABLED', true), + 'sample_rate' => env('PULSE_SLOW_JOBS_SAMPLE_RATE', 1), + 'threshold' => env('PULSE_SLOW_JOBS_THRESHOLD', 1000), + 'ignore' => [ + // '/^Package\\\\Jobs\\\\/', + ], + ], + + Recorders\SlowOutgoingRequests::class => [ + 'enabled' => env('PULSE_SLOW_OUTGOING_REQUESTS_ENABLED', true), + 'sample_rate' => env('PULSE_SLOW_OUTGOING_REQUESTS_SAMPLE_RATE', 1), + 'threshold' => env('PULSE_SLOW_OUTGOING_REQUESTS_THRESHOLD', 1000), + 'ignore' => [ + // '#^http://127\.0\.0\.1:13714#', // Inertia SSR... + ], + 'groups' => [ + // '#^https://api\.github\.com/repos/.*$#' => 'api.github.com/repos/*', + // '#^https?://([^/]*).*$#' => '\1', + // '#/\d+#' => '/*', + ], + ], + + Recorders\SlowQueries::class => [ + 'enabled' => env('PULSE_SLOW_QUERIES_ENABLED', true), + 'sample_rate' => env('PULSE_SLOW_QUERIES_SAMPLE_RATE', 1), + 'threshold' => env('PULSE_SLOW_QUERIES_THRESHOLD', 1000), + 'location' => env('PULSE_SLOW_QUERIES_LOCATION', true), + 'ignore' => [ + '/(["`])pulse_[\w]+?\1/', // Pulse tables... + ], + ], + + Recorders\SlowRequests::class => [ + 'enabled' => env('PULSE_SLOW_REQUESTS_ENABLED', true), + 'sample_rate' => env('PULSE_SLOW_REQUESTS_SAMPLE_RATE', 1), + 'threshold' => env('PULSE_SLOW_REQUESTS_THRESHOLD', 1000), + 'ignore' => [ + '#^/pulse$#', // Pulse dashboard... + ], + ], + + Recorders\UserJobs::class => [ + 'enabled' => env('PULSE_USER_JOBS_ENABLED', true), + 'sample_rate' => env('PULSE_USER_JOBS_SAMPLE_RATE', 1), + 'ignore' => [ + // '/^Package\\\\Jobs\\\\/', + ], + ], + + Recorders\UserRequests::class => [ + 'enabled' => env('PULSE_USER_REQUESTS_ENABLED', true), + 'sample_rate' => env('PULSE_USER_REQUESTS_SAMPLE_RATE', 1), + 'ignore' => [ + '#^/pulse$#', // Pulse dashboard... + ], + ], + ], +]; diff --git a/database/migrations/2023_10_12_204335_version1_stable.php b/database/migrations/2023_10_12_204335_version1_stable.php index d8a5b92a..cff64f5f 100644 --- a/database/migrations/2023_10_12_204335_version1_stable.php +++ b/database/migrations/2023_10_12_204335_version1_stable.php @@ -17,7 +17,7 @@ public function up(): void Schema::table('budget_months', function (Blueprint $table) { $table->decimal('left_from_last_month')->default(0)->after('payments'); - $table->decimal('funded_spending_previous_month')->default(0)->after('left_from_last_month'); + $table->decimal('overspending_previous_month')->default(0)->after('left_from_last_month'); }); } }; diff --git a/resources/js/Pages/Finance/Budget.vue b/resources/js/Pages/Finance/Budget.vue index 79b972b7..e6c04d18 100644 --- a/resources/js/Pages/Finance/Budget.vue +++ b/resources/js/Pages/Finance/Budget.vue @@ -27,7 +27,7 @@ import { useServerSearch } from "@/composables/useServerSearch"; import MessageBox from "@/Components/organisms/MessageBox.vue"; import BudgetCategories from "./Partials/BudgetCategories.vue"; -import { formatMoney, formatMonth } from "@/utils"; +import { MonthTypeFormat, formatMoney, formatMonth } from "@/utils"; const props = defineProps({ budgets: { @@ -66,7 +66,7 @@ const { state: pageState, executeSearchWithDelay } = useServerSearch( provide("pageState", pageState); const sectionTitle = computed(() => { - return `${formatMonth(pageState.dates.startDate, "MMMM yyyy")}`; + return pageState.dates.startDate ? `${formatMonth(pageState.dates.startDate, MonthTypeFormat.monthYear)}` : '--'; }); const { budgets } = toRefs(props); @@ -76,9 +76,9 @@ const { filterGroups, filters, visibleFilters, + selectedBudget, toggleFilter, setSelectedBudget, - selectedBudget, } = useBudget(); const panelSize = computed(() => { @@ -121,7 +121,10 @@ const goToday = () => { const budgetAccountsTotal = computed(() => { return props.accounts.reduce((total, account) => { - return exactMath.add(total, account?.balance) + console.log(account); + return account.balance_type == 'CREDIT' + ? total + : exactMath.add(total, account?.balance) }, 0) }) @@ -209,8 +212,8 @@ const budgetAccountsTotal = computed(() => {
-

Available for funds:

-

Funded:

-

funded spending:

-

Total:

+

from last month:

+

inflow:

+

Total:

Budgeted:

-

payments:

-

Total:

-

Assigned in month:

+

last month overspending:

+

Funded:

Balance:

diff --git a/resources/js/domains/budget/components/BudgetDetailForm.vue b/resources/js/domains/budget/components/BudgetDetailForm.vue index 54930a0a..8016e90e 100644 --- a/resources/js/domains/budget/components/BudgetDetailForm.vue +++ b/resources/js/domains/budget/components/BudgetDetailForm.vue @@ -17,14 +17,15 @@ import { ICategory } from '@/Modules/transactions/models/transactions'; const { hideDetails = true , category , item , editable = true } = defineProps<{ - parentId: number; - full: boolean; category: ICategory, item: Record, - hideDetails: boolean, - editable: boolean; + parentId?: number; + full?: boolean; + hideDetails?: boolean, + editable?: boolean; }>(); + const emit = defineEmits(['update:category']) const form = useForm({ diff --git a/resources/js/domains/budget/components/BudgetItem.vue b/resources/js/domains/budget/components/BudgetItem.vue index 9f4adefe..4a083006 100644 --- a/resources/js/domains/budget/components/BudgetItem.vue +++ b/resources/js/domains/budget/components/BudgetItem.vue @@ -102,10 +102,11 @@ const onAssignBudget = () => { budgeted: Number(budgeted.value), date: format(new Date(), 'yyyy-MM-dd') }, { - preserveState: false, + preserveState: true, preserveScroll: true }); } + console.log("here?"); isEditing.value = false; }) } diff --git a/resources/js/domains/budget/models/budget.ts b/resources/js/domains/budget/models/budget.ts index fac6aa03..1deb4a46 100644 --- a/resources/js/domains/budget/models/budget.ts +++ b/resources/js/domains/budget/models/budget.ts @@ -1,3 +1,5 @@ +import { ICategory } from "@/domains/transactions/models"; + export interface BudgetTarget { amount: number category_id: number; @@ -72,3 +74,11 @@ export interface BudgetCategory { export interface BudgetItem { } + +export interface IBudgetCategory extends ICategory { + hasOverspent: boolean; + hasFunded: boolean; + hasUnderFunded: boolean; + overAssigned: boolean; + subCategories: any[] +} diff --git a/resources/js/domains/budget/useBudget.ts b/resources/js/domains/budget/useBudget.ts index b5f6288a..fde06ab3 100644 --- a/resources/js/domains/budget/useBudget.ts +++ b/resources/js/domains/budget/useBudget.ts @@ -1,10 +1,8 @@ -import { isCurrentMonth } from './../../utils/index'; -import { budgetCols } from './budgetCols'; + import { cloneDeep } from "lodash"; import { computed, watch, reactive, toRefs, Ref } from "vue"; import { getCategoriesTotals, getGroupTotals } from './index'; import { ICategory } from "../transactions/models"; -import { format } from 'date-fns'; export const BudgetState = reactive({ data: [], @@ -57,12 +55,13 @@ export const BudgetState = reactive({ const availableForFunding = parseFloat(category.activity ?? 0); const fundedSpending = parseFloat(category?.funded_spending ?? 0); const assigned = budgetTotals.budgeted; - const leftOVer = parseFloat(category.left_from_last_month ?? 0) - const balance = (leftOVer + availableForFunding) - (parseFloat(category.overspending_previous_month ?? 0) + parseFloat(budgetTotals.budgeted ?? 0)); + const leftOver = parseFloat(category.left_from_last_month ?? 0) + const balance = (leftOver + availableForFunding) - (parseFloat(category.overspending_previous_month ?? 0) + parseFloat(budgetTotals.budgeted ?? 0)); return { - assigned, availableForFunding, + leftOver, + assigned, balance, creditCardFunded, fundedSpending, diff --git a/resources/js/utils/index.ts b/resources/js/utils/index.ts index 7827ab9f..cfde0988 100644 --- a/resources/js/utils/index.ts +++ b/resources/js/utils/index.ts @@ -71,7 +71,8 @@ export const generateRandomColor = () => `#${Math.floor(Math.random() * 16777215 export enum MonthTypeFormat { short = 'MMM', - long = 'MMMM' + long = 'MMMM', + monthYear = 'MMMM yyyy' } export const formatMonth = (dateString: string | Date, type: MonthTypeFormat = MonthTypeFormat.short ) => { try {