123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- <?php
-
- namespace App\Filament\Company\Resources\Accounting\BudgetResource\RelationManagers;
-
- use App\Filament\Tables\Columns\CustomTextInputColumn;
- use App\Models\Accounting\Budget;
- use App\Models\Accounting\BudgetAllocation;
- use App\Models\Accounting\BudgetItem;
- use App\Utilities\Currency\CurrencyConverter;
- use Filament\Notifications\Notification;
- use Filament\Resources\RelationManagers\RelationManager;
- use Filament\Support\RawJs;
- use Filament\Tables\Actions\Action;
- use Filament\Tables\Actions\BulkAction;
- use Filament\Tables\Columns\IconColumn;
- use Filament\Tables\Columns\Summarizers\Summarizer;
- use Filament\Tables\Columns\TextColumn;
- use Filament\Tables\Grouping\Group;
- use Filament\Tables\Table;
- use Illuminate\Database\Eloquent\Builder;
- use Illuminate\Database\Eloquent\Collection;
- use Illuminate\Support\Carbon;
- use Illuminate\Support\Facades\DB;
- use stdClass;
-
- class BudgetItemsRelationManager extends RelationManager
- {
- protected static string $relationship = 'budgetItems';
-
- protected static bool $isLazy = false;
-
- protected const TOTAL_COLUMN = 'total';
-
- public array $batchChanges = [];
-
- /**
- * Generate a consistent key for the budget item and period
- */
- protected static function generatePeriodKey(int $recordId, string | Carbon $startDate): string
- {
- $formattedDate = $startDate instanceof Carbon
- ? $startDate->format('Y_m_d')
- : Carbon::parse($startDate)->format('Y_m_d');
-
- return "{$recordId}.{$formattedDate}";
- }
-
- /**
- * Generate a consistent key for the budget item's total
- */
- protected static function generateTotalKey(int $recordId): string
- {
- return "{$recordId}." . self::TOTAL_COLUMN;
- }
-
- public function handleBatchColumnChanged($data): void
- {
- $key = "{$data['recordKey']}.{$data['name']}";
- $this->batchChanges[$key] = $data['value'];
- }
-
- public function saveBatchChanges(): void
- {
- foreach ($this->batchChanges as $key => $value) {
- [$recordKey, $column] = explode('.', $key, 2);
-
- try {
- $startDate = Carbon::createFromFormat('Y_m_d', $column);
- } catch (\Exception) {
- continue;
- }
-
- $record = BudgetItem::find($recordKey);
- if (! $record) {
- continue;
- }
-
- $allocation = $record->allocations()
- ->whereDate('start_date', $startDate)
- ->first();
-
- $allocation?->update(['amount' => $value]);
- }
-
- $this->batchChanges = [];
-
- Notification::make()
- ->title('Budget allocations updated')
- ->success()
- ->send();
- }
-
- protected function calculatePeriodSum(array $budgetItemIds, string | Carbon $startDate): int
- {
- $allocations = DB::table('budget_allocations')
- ->whereIn('budget_item_id', $budgetItemIds)
- ->whereDate('start_date', $startDate)
- ->pluck('amount', 'budget_item_id');
-
- $dbTotal = $allocations->sum();
-
- $batchTotal = 0;
-
- foreach ($budgetItemIds as $itemId) {
- $key = self::generatePeriodKey($itemId, $startDate);
-
- if (isset($this->batchChanges[$key])) {
- $batchValue = CurrencyConverter::convertToCents($this->batchChanges[$key]);
- $existingAmount = $allocations[$itemId] ?? 0;
-
- $batchTotal += ($batchValue - $existingAmount);
- }
- }
-
- return $dbTotal + $batchTotal;
- }
-
- public function table(Table $table): Table
- {
- /** @var Budget $budget */
- $budget = $this->getOwnerRecord();
- $allocationPeriods = $budget->getPeriods();
-
- return $table
- ->recordTitleAttribute('account_id')
- ->paginated(false)
- ->heading(null)
- ->modifyQueryUsing(function (Builder $query) use ($allocationPeriods) {
- $query->select('budget_items.*')
- ->leftJoin('budget_allocations', 'budget_allocations.budget_item_id', '=', 'budget_items.id');
-
- foreach ($allocationPeriods as $period) {
- $alias = $period->start_date->format('Y_m_d');
- $query->selectRaw(
- "SUM(CASE WHEN budget_allocations.start_date = ? THEN budget_allocations.amount ELSE 0 END) as {$alias}",
- [$period->start_date->toDateString()]
- );
- }
-
- return $query->groupBy('budget_items.id');
- })
- ->groups([
- Group::make('account.category')
- ->titlePrefixedWithLabel(false)
- ->collapsible(),
- ])
- ->recordClasses(['is-spreadsheet'])
- ->defaultGroup('account.category')
- ->headerActions([
- Action::make('saveBatchChanges')
- ->label('Save all changes')
- ->action('saveBatchChanges')
- ->color('primary'),
- ])
- ->columns([
- TextColumn::make('account.name')
- ->label('Account')
- ->limit(30)
- ->searchable(),
- CustomTextInputColumn::make(self::TOTAL_COLUMN)
- ->label('Total')
- ->alignRight()
- ->mask(RawJs::make('$money($input)'))
- ->getStateUsing(function (BudgetItem $record) {
- $key = self::generateTotalKey($record->getKey());
- if (isset($this->batchChanges[$key])) {
- return $this->batchChanges[$key];
- }
-
- $total = $record->allocations->sum(
- fn (BudgetAllocation $allocation) => $allocation->getRawOriginal('amount')
- );
-
- return CurrencyConverter::convertCentsToFormatSimple($total);
- })
- ->deferred()
- ->navigable()
- ->summarize(
- Summarizer::make()
- ->using(function (\Illuminate\Database\Query\Builder $query) {
- $allocations = $query
- ->leftJoin('budget_allocations', 'budget_allocations.budget_item_id', '=', 'budget_items.id')
- ->select('budget_allocations.budget_item_id', 'budget_allocations.start_date', 'budget_allocations.amount')
- ->get();
-
- $allocationsByDate = $allocations->groupBy('start_date');
-
- $total = 0;
-
- /** @var \Illuminate\Support\Collection<string, \Illuminate\Support\Collection<int, stdClass>> $allocationsByDate */
- foreach ($allocationsByDate as $startDate => $group) {
- $dbTotal = $group->sum('amount');
- $amounts = $group->pluck('amount', 'budget_item_id');
- $batchTotal = 0;
-
- foreach ($amounts as $itemId => $existingAmount) {
- $key = self::generatePeriodKey($itemId, $startDate);
-
- if (isset($this->batchChanges[$key])) {
- $batchValue = CurrencyConverter::convertToCents($this->batchChanges[$key]);
- $batchTotal += ($batchValue - $existingAmount);
- }
- }
-
- $total += $dbTotal + $batchTotal;
- }
-
- return CurrencyConverter::convertCentsToFormatSimple($total);
- })
- ),
- IconColumn::make('disperseAction')
- ->icon('heroicon-m-chevron-double-right')
- ->color('primary')
- ->label('')
- ->default('')
- ->tooltip('Disperse total across periods')
- ->action(
- Action::make('disperse')
- ->label('Disperse')
- ->action(function (BudgetItem $record) use ($allocationPeriods) {
- if (empty($allocationPeriods)) {
- return;
- }
-
- $totalKey = self::generateTotalKey($record->getKey());
- $totalAmount = $this->batchChanges[$totalKey] ?? null;
-
- if (isset($totalAmount)) {
- $totalCents = CurrencyConverter::convertToCents($totalAmount);
- } else {
- $totalCents = $record->allocations->sum(function (BudgetAllocation $budgetAllocation) {
- return $budgetAllocation->getRawOriginal('amount');
- });
- }
-
- if ($totalCents <= 0) {
- foreach ($allocationPeriods as $period) {
- $periodKey = self::generatePeriodKey($record->getKey(), $period->start_date);
- $this->batchChanges[$periodKey] = CurrencyConverter::convertCentsToFormatSimple(0);
- }
-
- return;
- }
-
- $numPeriods = count($allocationPeriods);
-
- $baseAmount = floor($totalCents / $numPeriods);
- $remainder = $totalCents - ($baseAmount * $numPeriods);
-
- foreach ($allocationPeriods as $index => $period) {
- $amount = $baseAmount + ($index === 0 ? $remainder : 0);
- $formattedAmount = CurrencyConverter::convertCentsToFormatSimple($amount);
-
- $periodKey = self::generatePeriodKey($record->getKey(), $period->start_date);
- $this->batchChanges[$periodKey] = $formattedAmount;
- }
- }),
- ),
- ...$allocationPeriods->map(function (BudgetAllocation $period) {
- $alias = $period->start_date->format('Y_m_d');
-
- return CustomTextInputColumn::make($alias)
- ->label($period->period)
- ->alignRight()
- ->deferred()
- ->navigable()
- ->mask(RawJs::make('$money($input)'))
- ->getStateUsing(function ($record) use ($alias) {
- $key = "{$record->getKey()}.{$alias}";
-
- return $this->batchChanges[$key] ?? CurrencyConverter::convertCentsToFormatSimple($record->{$alias} ?? 0);
- })
- ->summarize(
- Summarizer::make()
- ->using(function (\Illuminate\Database\Query\Builder $query) use ($period) {
- $budgetItemIds = $query->pluck('id')->toArray();
- $total = $this->calculatePeriodSum($budgetItemIds, $period->start_date);
-
- return CurrencyConverter::convertCentsToFormatSimple($total);
- })
- );
- })->toArray(),
- ])
- ->bulkActions([
- BulkAction::make('clearAllocations')
- ->label('Clear Allocations')
- ->icon('heroicon-o-trash')
- ->color('danger')
- ->requiresConfirmation()
- ->deselectRecordsAfterCompletion()
- ->action(function (Collection $records) use ($allocationPeriods) {
- foreach ($records as $record) {
- foreach ($allocationPeriods as $period) {
- $periodKey = self::generatePeriodKey($record->getKey(), $period->start_date);
- $this->batchChanges[$periodKey] = CurrencyConverter::convertCentsToFormatSimple(0);
- }
- }
- }),
- ]);
- }
- }
|