123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- <?php
-
- namespace App\Filament\Company\Resources\Accounting;
-
- use App\Enums\Accounting\TransactionType;
- use App\Filament\Company\Resources\Accounting\TransactionResource\Pages;
- use App\Filament\Forms\Components\DateRangeSelect;
- use App\Filament\Tables\Actions\EditTransactionAction;
- use App\Filament\Tables\Actions\ReplicateBulkAction;
- use App\Models\Accounting\JournalEntry;
- use App\Models\Accounting\Transaction;
- use App\Models\Common\Client;
- use App\Models\Common\Vendor;
- use Exception;
- use Filament\Forms\Components\DatePicker;
- use Filament\Forms\Components\Grid;
- use Filament\Forms\Form;
- use Filament\Forms\Set;
- use Filament\Notifications\Notification;
- use Filament\Resources\Resource;
- use Filament\Support\Colors\Color;
- use Filament\Support\Enums\FontWeight;
- use Filament\Support\Enums\MaxWidth;
- use Filament\Tables;
- use Filament\Tables\Table;
- use Illuminate\Database\Eloquent\Builder;
- use Illuminate\Database\Eloquent\Collection;
- use Illuminate\Support\Carbon;
-
- class TransactionResource extends Resource
- {
- protected static ?string $model = Transaction::class;
-
- protected static ?string $recordTitleAttribute = 'description';
-
- public static function form(Form $form): Form
- {
- return $form
- ->schema([]);
- }
-
- public static function table(Table $table): Table
- {
- return $table
- ->modifyQueryUsing(function (Builder $query) {
- $query->with([
- 'account',
- 'bankAccount.account',
- 'journalEntries.account',
- 'payeeable',
- ])
- ->where(function (Builder $query) {
- $query->whereNull('transactionable_id')
- ->orWhere('is_payment', true);
- });
- })
- ->columns([
- Tables\Columns\TextColumn::make('posted_at')
- ->label('Date')
- ->sortable()
- ->defaultDateFormat(),
- Tables\Columns\TextColumn::make('type')
- ->label('Type')
- ->sortable()
- ->toggleable(isToggledHiddenByDefault: true),
- Tables\Columns\TextColumn::make('description')
- ->label('Description')
- ->limit(50)
- ->searchable()
- ->toggleable(),
- Tables\Columns\TextColumn::make('payeeable.name')
- ->label('Payee')
- ->searchable()
- ->toggleable(isToggledHiddenByDefault: true),
- Tables\Columns\TextColumn::make('bankAccount.account.name')
- ->label('Account')
- ->searchable()
- ->toggleable(),
- Tables\Columns\TextColumn::make('account.name')
- ->label('Category')
- ->prefix(static fn (Transaction $transaction) => $transaction->type->isTransfer() ? 'Transfer to ' : null)
- ->searchable()
- ->toggleable()
- ->state(static fn (Transaction $transaction) => $transaction->account->name ?? 'Journal Entry'),
- Tables\Columns\TextColumn::make('amount')
- ->label('Amount')
- ->weight(static fn (Transaction $transaction) => $transaction->reviewed ? null : FontWeight::SemiBold)
- ->color(
- static fn (Transaction $transaction) => match ($transaction->type) {
- TransactionType::Deposit => Color::rgb('rgb(' . Color::Green[700] . ')'),
- TransactionType::Journal => 'primary',
- default => null,
- }
- )
- ->sortable()
- ->currency(static fn (Transaction $transaction) => $transaction->bankAccount?->account->currency_code),
- ])
- ->defaultSort('posted_at', 'desc')
- ->filters([
- Tables\Filters\SelectFilter::make('bank_account_id')
- ->label('Account')
- ->searchable()
- ->options(static fn () => Transaction::getBankAccountOptions(excludeArchived: false)),
- Tables\Filters\SelectFilter::make('account_id')
- ->label('Category')
- ->multiple()
- ->options(static fn () => Transaction::getChartAccountOptions()),
- Tables\Filters\TernaryFilter::make('reviewed')
- ->label('Status')
- ->trueLabel('Reviewed')
- ->falseLabel('Not Reviewed'),
- Tables\Filters\SelectFilter::make('type')
- ->label('Type')
- ->options(TransactionType::class),
- Tables\Filters\TernaryFilter::make('is_payment')
- ->label('Payment')
- ->default(false),
- Tables\Filters\SelectFilter::make('payee')
- ->label('Payee')
- ->options(static fn () => Transaction::getPayeeOptions())
- ->searchable()
- ->query(function (Builder $query, array $data): Builder {
- if (empty($data['value'])) {
- return $query;
- }
-
- $id = (int) $data['value'];
-
- if ($id < 0) {
- return $query->where('payeeable_type', Vendor::class)
- ->where('payeeable_id', abs($id));
- } else {
- return $query->where('payeeable_type', Client::class)
- ->where('payeeable_id', $id);
- }
- }),
- static::buildDateRangeFilter('posted_at', 'Posted', true),
- static::buildDateRangeFilter('updated_at', 'Last modified'),
- ])
- ->filtersFormSchema(fn (array $filters): array => [
- Grid::make()
- ->schema([
- $filters['bank_account_id'],
- $filters['account_id'],
- $filters['reviewed'],
- $filters['type'],
- $filters['is_payment'],
- $filters['payee'],
- ])
- ->columnSpanFull()
- ->extraAttributes(['class' => 'border-b border-gray-200 dark:border-white/10 pb-8']),
- $filters['posted_at'],
- $filters['updated_at'],
- ])
- ->filtersFormWidth(MaxWidth::ThreeExtraLarge)
- ->actions([
- Tables\Actions\Action::make('markAsReviewed')
- ->label('Mark as reviewed')
- ->view('filament.company.components.tables.actions.mark-as-reviewed')
- ->icon(static fn (Transaction $transaction) => $transaction->reviewed ? 'heroicon-s-check-circle' : 'heroicon-o-check-circle')
- ->color(static fn (Transaction $transaction, Tables\Actions\Action $action) => match (static::determineTransactionState($transaction, $action)) {
- 'reviewed' => 'primary',
- 'unreviewed' => Color::rgb('rgb(' . Color::Gray[600] . ')'),
- 'uncategorized' => 'gray',
- })
- ->tooltip(static fn (Transaction $transaction, Tables\Actions\Action $action) => match (static::determineTransactionState($transaction, $action)) {
- 'reviewed' => 'Reviewed',
- 'unreviewed' => 'Mark as reviewed',
- 'uncategorized' => 'Categorize first to mark as reviewed',
- })
- ->disabled(fn (Transaction $transaction): bool => $transaction->isUncategorized())
- ->action(fn (Transaction $transaction) => $transaction->update(['reviewed' => ! $transaction->reviewed])),
- Tables\Actions\ActionGroup::make([
- Tables\Actions\ActionGroup::make([
- EditTransactionAction::make(),
- Tables\Actions\ReplicateAction::make()
- ->excludeAttributes(['created_by', 'updated_by', 'created_at', 'updated_at'])
- ->modal(false)
- ->beforeReplicaSaved(static function (Transaction $replica) {
- $replica->description = '(Copy of) ' . $replica->description;
- })
- ->hidden(static fn (Transaction $transaction) => $transaction->transactionable_id)
- ->after(static function (Transaction $original, Transaction $replica) {
- $original->journalEntries->each(function (JournalEntry $entry) use ($replica) {
- $entry->replicate([
- 'transaction_id',
- ])->fill([
- 'transaction_id' => $replica->id,
- ])->save();
- });
- }),
- ])->dropdown(false),
- Tables\Actions\DeleteAction::make(),
- ]),
- ])
- ->bulkActions([
- Tables\Actions\BulkActionGroup::make([
- Tables\Actions\DeleteBulkAction::make(),
- ReplicateBulkAction::make()
- ->label('Replicate')
- ->modalWidth(MaxWidth::Large)
- ->modalDescription('Replicating transactions will also replicate their journal entries. Are you sure you want to proceed?')
- ->successNotificationTitle('Transactions replicated successfully')
- ->failureNotificationTitle('Failed to replicate transactions')
- ->deselectRecordsAfterCompletion()
- ->excludeAttributes(['created_by', 'updated_by', 'created_at', 'updated_at'])
- ->beforeReplicaSaved(static function (Transaction $replica) {
- $replica->description = '(Copy of) ' . $replica->description;
- })
- ->before(function (Collection $records, ReplicateBulkAction $action) {
- $isInvalid = $records->contains(fn (Transaction $record) => $record->transactionable_id);
-
- if ($isInvalid) {
- Notification::make()
- ->title('Cannot replicate transactions')
- ->body('You cannot replicate transactions associated with bills or invoices')
- ->persistent()
- ->danger()
- ->send();
-
- $action->cancel(true);
- }
- })
- ->withReplicatedRelationships(['journalEntries']),
- ]),
- ]);
- }
-
- public static function getRelations(): array
- {
- return [
- //
- ];
- }
-
- public static function getPages(): array
- {
- return [
- 'index' => Pages\ListTransactions::route('/'),
- 'view' => Pages\ViewTransaction::route('/{record}'),
- ];
- }
-
- /**
- * @throws Exception
- */
- public static function buildDateRangeFilter(string $fieldPrefix, string $label, bool $hasBottomBorder = false): Tables\Filters\Filter
- {
- return Tables\Filters\Filter::make($fieldPrefix)
- ->columnSpanFull()
- ->form([
- Grid::make()
- ->live()
- ->schema([
- DateRangeSelect::make("{$fieldPrefix}_date_range")
- ->label($label)
- ->selectablePlaceholder(false)
- ->placeholder('Select a date range')
- ->startDateField("{$fieldPrefix}_start_date")
- ->endDateField("{$fieldPrefix}_end_date"),
- DatePicker::make("{$fieldPrefix}_start_date")
- ->label("{$label} from")
- ->columnStart(1)
- ->afterStateUpdated(static function (Set $set) use ($fieldPrefix) {
- $set("{$fieldPrefix}_date_range", 'Custom');
- }),
- DatePicker::make("{$fieldPrefix}_end_date")
- ->label("{$label} to")
- ->afterStateUpdated(static function (Set $set) use ($fieldPrefix) {
- $set("{$fieldPrefix}_date_range", 'Custom');
- }),
- ])
- ->extraAttributes($hasBottomBorder ? ['class' => 'border-b border-gray-200 dark:border-white/10 pb-8'] : []),
- ])
- ->query(function (Builder $query, array $data) use ($fieldPrefix): Builder {
- $query
- ->when($data["{$fieldPrefix}_start_date"], fn (Builder $query, $startDate) => $query->whereDate($fieldPrefix, '>=', $startDate))
- ->when($data["{$fieldPrefix}_end_date"], fn (Builder $query, $endDate) => $query->whereDate($fieldPrefix, '<=', $endDate));
-
- return $query;
- })
- ->indicateUsing(function (array $data) use ($fieldPrefix, $label): array {
- $indicators = [];
-
- static::addIndicatorForDateRange($data, "{$fieldPrefix}_start_date", "{$fieldPrefix}_end_date", $label, $indicators);
-
- return $indicators;
- });
-
- }
-
- public static function addIndicatorForDateRange($data, $startKey, $endKey, $labelPrefix, &$indicators): void
- {
- $formattedStartDate = filled($data[$startKey]) ? Carbon::parse($data[$startKey])->toFormattedDateString() : null;
- $formattedEndDate = filled($data[$endKey]) ? Carbon::parse($data[$endKey])->toFormattedDateString() : null;
- if ($formattedStartDate && $formattedEndDate) {
- // If both start and end dates are set, show the combined date range as the indicator, no specific field needs to be removed since the entire filter will be removed
- $indicators[] = Tables\Filters\Indicator::make("{$labelPrefix}: {$formattedStartDate} - {$formattedEndDate}");
- } else {
- if ($formattedStartDate) {
- $indicators[] = Tables\Filters\Indicator::make("{$labelPrefix} After: {$formattedStartDate}")
- ->removeField($startKey);
- }
-
- if ($formattedEndDate) {
- $indicators[] = Tables\Filters\Indicator::make("{$labelPrefix} Before: {$formattedEndDate}")
- ->removeField($endKey);
- }
- }
- }
-
- protected static function determineTransactionState(Transaction $transaction, Tables\Actions\Action $action): string
- {
- if ($transaction->reviewed) {
- return 'reviewed';
- }
-
- if ($transaction->reviewed === false && $action->isEnabled()) {
- return 'unreviewed';
- }
-
- return 'uncategorized';
- }
- }
|