| 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);
                            }
                        }
                    }),
            ]);
    }
}
 |