feat(client): add financial reports and ledger management

- Add trial balance and general ledger pages to client resource with interactive tables
- Implement sales and expenses relation managers for client-specific transactions
- Enhance transaction handling with proper tax and withholding calculations
- Add date casting to Transaction model and define client relationships
- Configure super admin role bypass in AppServiceProvider
- Update Filament components and fix JavaScript formatting issues
This commit is contained in:
Jp
2026-02-09 16:20:55 +08:00
parent 91eb1fbe63
commit 207f4c1609
43 changed files with 3412 additions and 2967 deletions

View File

@@ -0,0 +1,161 @@
<?php
namespace App\Filament\Resources\ClientResource\RelationManagers;
use App\Actions\Balances\CreateBalanceAction;
use App\Actions\Ledgers\CreateLedgerAction;
use App\DataObjects\CreateLedgerDTO;
use App\Models\Branch;
use App\Models\Journal;
use Awcodes\TableRepeater\Components\TableRepeater;
use Awcodes\TableRepeater\Header;
use Filament\Forms;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Pipeline;
class JournalsRelationManager extends RelationManager
{
protected static string $relationship = 'journals';
protected static ?string $title = 'Journal Entries (Adjustments)';
public function form(Form $form): Form
{
return $form
->schema([
Select::make('branch_id')
->label('Branch')
->options(fn () => Branch::where('client_id', $this->getOwnerRecord()->id)->pluck('name', 'id'))
->required()
->default(fn () => Branch::where('client_id', $this->getOwnerRecord()->id)->first()?->id),
DatePicker::make('happened_on')
->label('Date')
->required()
->default(now()),
TextInput::make('series')
->label('Reference/Series #')
->required(),
Textarea::make('description')
->label('Description')
->columnSpanFull(),
TableRepeater::make('ledgers')
->relationship('ledgers')
->headers([
Header::make('Account'),
Header::make('Description'),
Header::make('Debit'),
Header::make('Credit'),
])
->schema([
Select::make('account_id')
->relationship('account', 'account', fn (Builder $query) => $query->where('client_id', $this->getOwnerRecord()->id))
->searchable()
->preload()
->required(),
TextInput::make('description'),
TextInput::make('debit_amount')
->numeric()
->default(0),
TextInput::make('credit_amount')
->numeric()
->default(0),
])
->columnSpanFull()
->minItems(2)
->live()
->rules([
fn (): \Closure => function (string $attribute, $value, \Closure $fail) {
$debit = collect($value)->sum('debit_amount');
$credit = collect($value)->sum('credit_amount');
if (abs($debit - $credit) > 0.01) {
$fail("Total Debit (" . number_format($debit, 2) . ") must equal Total Credit (" . number_format($credit, 2) . ").");
}
},
])
->afterStateUpdated(function ($state, $component) {
// Optional: Validation logic
}),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('description')
->columns([
Tables\Columns\TextColumn::make('happened_on')
->date()
->sortable(),
Tables\Columns\TextColumn::make('series')
->searchable(),
Tables\Columns\TextColumn::make('description')
->limit(50),
Tables\Columns\TextColumn::make('total_debit')
->label('Total Debit')
->state(fn (Journal $record) => $record->ledgers->sum('debit_amount'))
->money('PHP'),
Tables\Columns\TextColumn::make('total_credit')
->label('Total Credit')
->state(fn (Journal $record) => $record->ledgers->sum('credit_amount'))
->money('PHP'),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make()
->label('Add Adjustment Entry')
->using(function (array $data, string $model) {
return DB::transaction(function () use ($data, $model) {
$ledgersData = $data['ledgers'] ?? [];
$journalData = Arr::except($data, ['ledgers']);
$journalData['client_id'] = $this->getOwnerRecord()->id;
$journal = $model::create($journalData);
foreach ($ledgersData as $ledger) {
$ledgerPayload = new CreateLedgerDTO(
branch_id: $journal->branch_id,
amount: ($ledger['debit_amount'] > 0) ? $ledger['debit_amount'] : $ledger['credit_amount'],
ledger: null, // Will be created
transaction: null, // No transaction
journal: $journal,
account: \App\Models\Account::find($ledger['account_id']),
type: ($ledger['debit_amount'] > 0) ? 'debit' : 'credit'
);
Pipeline::send(passable: $ledgerPayload)->through(
[
CreateLedgerAction::class,
]
)->thenReturn();
}
return $journal;
});
}),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
}