| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 | 
							- <?php
 - 
 - namespace App\Concerns;
 - 
 - use App\Enums\Accounting\JournalEntryType;
 - use App\Enums\Accounting\TransactionType;
 - use App\Filament\Forms\Components\CustomTableRepeater;
 - use App\Models\Accounting\JournalEntry;
 - use App\Models\Accounting\Transaction;
 - use App\Models\Banking\BankAccount;
 - use App\Services\CompanySettingsService;
 - use App\Utilities\Currency\CurrencyAccessor;
 - use App\Utilities\Currency\CurrencyConverter;
 - use Awcodes\TableRepeater\Header;
 - use Closure;
 - use Filament\Forms;
 - use Filament\Forms\Components\Actions\Action as FormAction;
 - use Filament\Forms\Form;
 - use Illuminate\Contracts\View\View;
 - use Illuminate\Support\Str;
 - 
 - trait HasTransactionAction
 - {
 -     use HasJournalEntryActions;
 - 
 -     protected TransactionType | Closure | null $transactionType = null;
 - 
 -     public function type(TransactionType | Closure | null $type = null): static
 -     {
 -         $this->transactionType = $type;
 - 
 -         return $this;
 -     }
 - 
 -     public function getTransactionType(): ?TransactionType
 -     {
 -         return $this->evaluate($this->transactionType);
 -     }
 - 
 -     protected function getFormDefaultsForType(TransactionType $type): array
 -     {
 -         $commonDefaults = [
 -             'posted_at' => now(),
 -         ];
 - 
 -         return match ($type) {
 -             TransactionType::Deposit, TransactionType::Withdrawal, TransactionType::Transfer => array_merge($commonDefaults, $this->transactionDefaults($type)),
 -             TransactionType::Journal => array_merge($commonDefaults, $this->journalEntryDefaults()),
 -         };
 -     }
 - 
 -     protected function journalEntryDefaults(): array
 -     {
 -         return [
 -             'journalEntries' => [
 -                 $this->defaultEntry(JournalEntryType::Debit),
 -                 $this->defaultEntry(JournalEntryType::Credit),
 -             ],
 -         ];
 -     }
 - 
 -     protected function defaultEntry(JournalEntryType $journalEntryType): array
 -     {
 -         return [
 -             'type' => $journalEntryType,
 -             'account_id' => Transaction::getUncategorizedAccountByType($journalEntryType->isDebit() ? TransactionType::Withdrawal : TransactionType::Deposit)?->id,
 -             'amount' => '0.00',
 -         ];
 -     }
 - 
 -     protected function transactionDefaults(TransactionType $type): array
 -     {
 -         return [
 -             'type' => $type,
 -             'bank_account_id' => BankAccount::where('enabled', true)->first()?->id,
 -             'amount' => '0.00',
 -             'account_id' => ! $type->isTransfer() ? Transaction::getUncategorizedAccountByType($type)->id : null,
 -         ];
 -     }
 - 
 -     public function transactionForm(Form $form): Form
 -     {
 -         return $form
 -             ->schema([
 -                 Forms\Components\DatePicker::make('posted_at')
 -                     ->label('Date')
 -                     ->timezone(CompanySettingsService::getDefaultTimezone())
 -                     ->required(),
 -                 Forms\Components\TextInput::make('description')
 -                     ->label('Description'),
 -                 Forms\Components\Select::make('bank_account_id')
 -                     ->label('Account')
 -                     ->options(fn (?Transaction $transaction) => Transaction::getBankAccountOptions(currentBankAccountId: $transaction?->bank_account_id))
 -                     ->live()
 -                     ->searchable()
 -                     ->required(),
 -                 Forms\Components\Select::make('type')
 -                     ->label('Type')
 -                     ->live()
 -                     ->options([
 -                         TransactionType::Deposit->value => TransactionType::Deposit->getLabel(),
 -                         TransactionType::Withdrawal->value => TransactionType::Withdrawal->getLabel(),
 -                     ])
 -                     ->required()
 -                     ->afterStateUpdated(static fn (Forms\Set $set, $state) => $set('account_id', Transaction::getUncategorizedAccountByType(TransactionType::parse($state))?->id)),
 -                 Forms\Components\TextInput::make('amount')
 -                     ->label('Amount')
 -                     ->money(static fn (Forms\Get $get) => BankAccount::find($get('bank_account_id'))?->account?->currency_code ?? CurrencyAccessor::getDefaultCurrency())
 -                     ->required(),
 -                 Forms\Components\Select::make('account_id')
 -                     ->label('Category')
 -                     ->options(fn (Forms\Get $get, ?Transaction $transaction) => Transaction::getTransactionAccountOptions(type: TransactionType::parse($get('type')), currentAccountId: $transaction?->account_id))
 -                     ->searchable()
 -                     ->required(),
 -                 Forms\Components\Textarea::make('notes')
 -                     ->label('Notes')
 -                     ->autosize()
 -                     ->rows(10)
 -                     ->columnSpanFull(),
 -             ])
 -             ->columns();
 -     }
 - 
 -     public function transferForm(Form $form): Form
 -     {
 -         return $form
 -             ->schema([
 -                 Forms\Components\DatePicker::make('posted_at')
 -                     ->label('Date')
 -                     ->timezone(CompanySettingsService::getDefaultTimezone())
 -                     ->required(),
 -                 Forms\Components\TextInput::make('description')
 -                     ->label('Description'),
 -                 Forms\Components\Select::make('bank_account_id')
 -                     ->label('From account')
 -                     ->options(fn (Forms\Get $get, ?Transaction $transaction) => Transaction::getBankAccountOptions(excludedAccountId: $get('account_id'), currentBankAccountId: $transaction?->bank_account_id))
 -                     ->live()
 -                     ->searchable()
 -                     ->required(),
 -                 Forms\Components\Select::make('type')
 -                     ->label('Type')
 -                     ->options([
 -                         TransactionType::Transfer->value => TransactionType::Transfer->getLabel(),
 -                     ])
 -                     ->disabled()
 -                     ->dehydrated()
 -                     ->required(),
 -                 Forms\Components\TextInput::make('amount')
 -                     ->label('Amount')
 -                     ->money(static fn (Forms\Get $get) => BankAccount::find($get('bank_account_id'))?->account?->currency_code ?? CurrencyAccessor::getDefaultCurrency())
 -                     ->required(),
 -                 Forms\Components\Select::make('account_id')
 -                     ->label('To account')
 -                     ->live()
 -                     ->options(fn (Forms\Get $get, ?Transaction $transaction) => Transaction::getBankAccountAccountOptions(excludedBankAccountId: $get('bank_account_id'), currentAccountId: $transaction?->account_id))
 -                     ->searchable()
 -                     ->required(),
 -                 Forms\Components\Textarea::make('notes')
 -                     ->label('Notes')
 -                     ->autosize()
 -                     ->rows(10)
 -                     ->columnSpanFull(),
 -             ])
 -             ->columns();
 -     }
 - 
 -     public function journalTransactionForm(Form $form): Form
 -     {
 -         return $form
 -             ->schema([
 -                 Forms\Components\Tabs::make('Tabs')
 -                     ->contained(false)
 -                     ->tabs([
 -                         $this->getJournalTransactionFormEditTab(),
 -                         $this->getJournalTransactionFormNotesTab(),
 -                     ]),
 -             ])
 -             ->columns(1);
 -     }
 - 
 -     protected function getJournalTransactionFormEditTab(): Forms\Components\Tabs\Tab
 -     {
 -         return Forms\Components\Tabs\Tab::make('Edit')
 -             ->label('Edit')
 -             ->icon('heroicon-o-pencil-square')
 -             ->schema([
 -                 $this->getTransactionDetailsGrid(),
 -                 $this->getJournalEntriesTableRepeater(),
 -             ]);
 -     }
 - 
 -     protected function getJournalTransactionFormNotesTab(): Forms\Components\Tabs\Tab
 -     {
 -         return Forms\Components\Tabs\Tab::make('Notes')
 -             ->label('Notes')
 -             ->icon('heroicon-o-clipboard')
 -             ->id('notes')
 -             ->schema([
 -                 $this->getTransactionDetailsGrid(),
 -                 Forms\Components\Textarea::make('notes')
 -                     ->label('Notes')
 -                     ->rows(10)
 -                     ->autosize(),
 -             ]);
 -     }
 - 
 -     protected function getTransactionDetailsGrid(): Forms\Components\Grid
 -     {
 -         return Forms\Components\Grid::make(6)
 -             ->schema([
 -                 Forms\Components\DatePicker::make('posted_at')
 -                     ->label('Date')
 -                     ->timezone(CompanySettingsService::getDefaultTimezone())
 -                     ->softRequired(),
 -                 Forms\Components\TextInput::make('description')
 -                     ->label('Description')
 -                     ->columnSpan(2),
 -             ]);
 -     }
 - 
 -     protected function getJournalEntriesTableRepeater(): CustomTableRepeater
 -     {
 -         return CustomTableRepeater::make('journalEntries')
 -             ->relationship('journalEntries')
 -             ->hiddenLabel()
 -             ->columns(4)
 -             ->headers($this->getJournalEntriesTableRepeaterHeaders())
 -             ->schema($this->getJournalEntriesTableRepeaterSchema())
 -             ->deletable(fn (CustomTableRepeater $repeater) => $repeater->getItemsCount() > 2)
 -             ->deleteAction(function (Forms\Components\Actions\Action $action) {
 -                 return $action
 -                     ->action(function (array $arguments, CustomTableRepeater $component): void {
 -                         $items = $component->getState();
 - 
 -                         $amount = $items[$arguments['item']]['amount'];
 -                         $type = $items[$arguments['item']]['type'];
 - 
 -                         $this->updateJournalEntryAmount(JournalEntryType::parse($type), '0.00', $amount);
 - 
 -                         unset($items[$arguments['item']]);
 - 
 -                         $component->state($items);
 - 
 -                         $component->callAfterStateUpdated();
 -                     });
 -             })
 -             ->rules([
 -                 function () {
 -                     return function (string $attribute, $value, \Closure $fail) {
 -                         if (empty($value) || ! is_array($value)) {
 -                             $fail('Journal entries are required.');
 - 
 -                             return;
 -                         }
 - 
 -                         $hasDebit = false;
 -                         $hasCredit = false;
 -                         $totalDebits = 0;
 -                         $totalCredits = 0;
 - 
 -                         foreach ($value as $entry) {
 -                             if (! isset($entry['type']) || ! isset($entry['amount'])) {
 -                                 continue;
 -                             }
 - 
 -                             $entryType = JournalEntryType::parse($entry['type']);
 -                             $amount = CurrencyConverter::convertToCents($entry['amount'], 'USD');
 - 
 -                             if ($entryType->isDebit()) {
 -                                 $hasDebit = true;
 -                                 $totalDebits += $amount;
 -                             } elseif ($entryType->isCredit()) {
 -                                 $hasCredit = true;
 -                                 $totalCredits += $amount;
 -                             }
 -                         }
 - 
 -                         if (! $hasDebit) {
 -                             $fail('At least one debit entry is required.');
 -                         }
 - 
 -                         if (! $hasCredit) {
 -                             $fail('At least one credit entry is required.');
 -                         }
 - 
 -                         if ($totalDebits !== $totalCredits) {
 -                             $debitFormatted = CurrencyConverter::formatCentsToMoney($totalDebits, CurrencyAccessor::getDefaultCurrency());
 -                             $creditFormatted = CurrencyConverter::formatCentsToMoney($totalCredits, CurrencyAccessor::getDefaultCurrency());
 -                             $fail("Total debits ({$debitFormatted}) must equal total credits ({$creditFormatted}).");
 -                         }
 -                     };
 -                 },
 -             ])
 -             ->minItems(2)
 -             ->defaultItems(2)
 -             ->addable(false)
 -             ->footerItem(fn (): View => $this->getJournalTransactionModalFooter())
 -             ->extraActions([
 -                 $this->buildAddJournalEntryAction(JournalEntryType::Debit),
 -                 $this->buildAddJournalEntryAction(JournalEntryType::Credit),
 -             ]);
 -     }
 - 
 -     protected function getJournalEntriesTableRepeaterHeaders(): array
 -     {
 -         return [
 -             Header::make('type')
 -                 ->width('150px')
 -                 ->label('Type'),
 -             Header::make('description')
 -                 ->width('320px')
 -                 ->label('Description'),
 -             Header::make('account_id')
 -                 ->width('320px')
 -                 ->label('Account'),
 -             Header::make('amount')
 -                 ->width('192px')
 -                 ->label('Amount'),
 -         ];
 -     }
 - 
 -     protected function getJournalEntriesTableRepeaterSchema(): array
 -     {
 -         return [
 -             Forms\Components\Select::make('type')
 -                 ->label('Type')
 -                 ->options(JournalEntryType::class)
 -                 ->live()
 -                 ->afterStateUpdated(function (Forms\Get $get, Forms\Set $set, $state, $old) {
 -                     $this->adjustJournalEntryAmountsForTypeChange(JournalEntryType::parse($state), JournalEntryType::parse($old), $get('amount'));
 -                 })
 -                 ->softRequired(),
 -             Forms\Components\TextInput::make('description')
 -                 ->label('Description'),
 -             Forms\Components\Select::make('account_id')
 -                 ->label('Account')
 -                 ->options(fn (?JournalEntry $journalEntry): array => Transaction::getJournalAccountOptions(currentAccountId: $journalEntry?->account_id))
 -                 ->softRequired()
 -                 ->searchable(),
 -             Forms\Components\TextInput::make('amount')
 -                 ->label('Amount')
 -                 ->live(onBlur: true)
 -                 ->money()
 -                 ->afterStateUpdated(function (Forms\Get $get, Forms\Set $set, ?string $state, ?string $old) {
 -                     $this->updateJournalEntryAmount(JournalEntryType::parse($get('type')), $state, $old);
 -                 })
 -                 ->softRequired(),
 -         ];
 -     }
 - 
 -     protected function buildAddJournalEntryAction(JournalEntryType $type): FormAction
 -     {
 -         $typeLabel = $type->getLabel();
 - 
 -         return FormAction::make("add{$typeLabel}Entry")
 -             ->button()
 -             ->outlined()
 -             ->color($type->isDebit() ? 'primary' : 'gray')
 -             ->action(function (CustomTableRepeater $component) use ($type) {
 -                 $state = $component->getState();
 -                 $newUuid = (string) Str::uuid();
 -                 $state[$newUuid] = $this->defaultEntry($type);
 - 
 -                 $component->state($state);
 -             });
 -     }
 - 
 -     public function getJournalTransactionModalFooter(): View
 -     {
 -         return view(
 -             'filament.company.components.actions.journal-entry-footer',
 -             [
 -                 'debitAmount' => $this->getFormattedDebitAmount(),
 -                 'creditAmount' => $this->getFormattedCreditAmount(),
 -                 'difference' => $this->getFormattedBalanceDifference(),
 -                 'isJournalBalanced' => $this->isJournalEntryBalanced(),
 -             ],
 -         );
 -     }
 - }
 
 
  |