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

@@ -25,6 +25,7 @@ class CreateLedgerAction extends BaseAction
ledger_id: $ledger->id,
account_id: $ledger->account_id,
branch_id: $ledger->branch_id,
type: $payload->type ?? 'debit',
);
return $next($payload);

View File

@@ -30,6 +30,8 @@ class CreateTransactionAction extends BaseAction
public function transactionAccountLedger($payload): void
{
$branch = $payload->transaction->branch;
$isExpense = $payload->transactionable instanceof \App\Models\Expense;
$type = $isExpense ? 'debit' : 'credit';
if ($branch->isClientVatable) {
//create transaction account ledger
@@ -38,10 +40,11 @@ class CreateTransactionAction extends BaseAction
amount: $payload->transaction->net_amount ?? 0.00,
transaction: $payload->transaction,
account: $payload->transaction->account,
type: $type,
);
$this->ledgerPipe($ledgerPayload);
$this->inputTaxAccountLedger($payload);
$this->taxAccountLedger($payload, $isExpense);
} else {
//create transaction account ledger
$ledgerPayload = new CreateLedgerDTO(
@@ -49,6 +52,7 @@ class CreateTransactionAction extends BaseAction
amount: $payload->transaction->gross_amount ?? 0.00,
transaction: $payload->transaction,
account: $payload->transaction->account,
type: $type,
);
$this->ledgerPipe($ledgerPayload);
}
@@ -63,50 +67,69 @@ class CreateTransactionAction extends BaseAction
])->thenReturn();
}
public function inputTaxAccountLedger($payload): void
public function taxAccountLedger($payload, bool $isExpense): void
{
$inputTax = Account::query()->where('account', 'Input Tax')->whereHas('balances', function ($balance) use ($payload) {
$accountName = $isExpense ? 'Input Tax' : 'Output Tax';
$type = $isExpense ? 'debit' : 'credit';
$taxAccount = Account::query()->where('account', $accountName)->whereHas('balances', function ($balance) use ($payload) {
return $balance->where('branch_id', $payload->transactionable->branch_id);
})->first();
$ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id,
amount: $payload->transactionable->input_tax ?? 0.00,
transaction: $payload->transaction,
account: $inputTax,
);
$this->ledgerPipe($ledgerPayload);
if ($taxAccount) {
$ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id,
amount: $payload->transactionable->input_tax ?? 0.00, // Assuming input_tax holds the tax amount for both? Or output_tax?
transaction: $payload->transaction,
account: $taxAccount,
type: $type,
);
$this->ledgerPipe($ledgerPayload);
}
}
public function withHoldingAccountLedger($payload): void
{
$withholdingAccount = Account::query()->where('account', 'Payable Withholding Tax')->whereHas('balances', function ($balance) use ($payload) {
$isExpense = $payload->transactionable instanceof \App\Models\Expense;
$accountName = $isExpense ? 'Payable Withholding Tax' : 'Creditable Withholding Tax';
$type = $isExpense ? 'credit' : 'debit';
$withholdingAccount = Account::query()->where('account', $accountName)->whereHas('balances', function ($balance) use ($payload) {
return $balance->where('branch_id', $payload->transactionable->branch_id);
})->first();
$ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id,
amount: $payload->transaction->payable_withholding_tax ?? 0.00,
transaction: $payload->transaction,
account: $withholdingAccount,
);
$this->ledgerPipe($ledgerPayload);
if ($withholdingAccount) {
$ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id,
amount: $payload->transaction->payable_withholding_tax ?? 0.00,
transaction: $payload->transaction,
account: $withholdingAccount,
type: $type,
);
$this->ledgerPipe($ledgerPayload);
}
}
public function cashAccountLedger($payload): void
{
$isExpense = $payload->transactionable instanceof \App\Models\Expense;
$type = $isExpense ? 'credit' : 'debit';
$wht = $isExpense ? ($payload->transaction->payable_withholding_tax ?? 0) : ($payload->transaction->creditable_withholding_tax ?? 0);
$amount = ($payload->transaction->gross_amount ?? 0) - $wht;
$cashAccount = Account::query()->where('account', 'Cash')->whereHas('balances', function ($balance) use ($payload) {
return $balance->where('branch_id', $payload->transactionable->branch_id);
})->first();
$amount = $payload->transaction->gross_amount - $payload->transaction->creditable_withholding_tax;
$ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id,
amount: $amount ?? 0.00,
transaction: $payload->transaction,
account: $cashAccount,
);
$this->ledgerPipe($ledgerPayload);
if ($cashAccount) {
$ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id,
amount: $amount,
transaction: $payload->transaction,
account: $cashAccount,
type: $type,
);
$this->ledgerPipe($ledgerPayload);
}
}
}

View File

@@ -15,16 +15,27 @@ class CreateBalanceDTO extends Data
public ?int $id = null,
public ?int $ledger_id = null,
public ?int $account_id = null,
public ?int $branch_id = null
public ?int $branch_id = null,
public string $type = 'debit'
) {
$account = Account::query()->where('id', $this->account_id)->first();
$account = Account::with('accountType')->where('id', $this->account_id)->first();
$currentBalance = $account ? $account->current_balance : 0;
$normalBalance = strtolower($account->accountType->normal_balance ?? 'debit');
$transactionType = strtolower($this->type);
if ($account->normal_balance == 'credit') {
$this->balance = $currentBalance - $this->amount;
} else {
$this->balance = $currentBalance + $this->amount;
if ($transactionType === 'debit') {
if ($normalBalance === 'debit') {
$this->balance = $currentBalance + $this->amount;
} else {
$this->balance = $currentBalance - $this->amount;
}
} else { // credit
if ($normalBalance === 'credit') {
$this->balance = $currentBalance + $this->amount;
} else {
$this->balance = $currentBalance - $this->amount;
}
}
}
}

View File

@@ -13,7 +13,10 @@ class CreateLedgerDTO extends Data
public array $data;
#[Computed]
public int $transaction_id;
public ?int $transaction_id = null;
#[Computed]
public ?int $journal_id = null;
#[Computed]
public int $account_id;
@@ -35,16 +38,22 @@ class CreateLedgerDTO extends Data
public float $amount,
public ?Model $ledger = null,
public ?Model $transaction = null,
public ?Model $journal = null,
public ?Model $account = null,
public ?CreateBalanceDTO $balanceDTO = null,
public ?string $type = null, // 'debit' or 'credit'
) {
$this->transaction_id = $this->transaction->id;
$this->transaction_id = $this->transaction?->id;
$this->journal_id = $this->journal?->id;
$this->account_id = $this->account->id;
$this->client_id = $this->transaction->branch->client_id;
$this->credit_amount = $this->transaction->account_type == 'credit' ? $this->amount : 0.00;
$this->debit_amount = $this->transaction->account_type == 'debit' ? $this->amount : 0.00;
$this->description = $this->transaction->description;
$this->client_id = $this->transaction?->branch->client_id ?? $this->journal?->client_id;
$accountType = $this->type ? strtolower($this->type) : strtolower($this->transaction?->account_type);
$this->data = Arr::except($this->toArray(), ['transaction', 'ledger', 'account', 'amount']);
$this->credit_amount = $accountType == 'credit' ? $this->amount : 0.00;
$this->debit_amount = $accountType == 'debit' ? $this->amount : 0.00;
$this->description = $this->transaction?->description ?? $this->journal?->description;
$this->data = Arr::except($this->toArray(), ['transaction', 'journal', 'ledger', 'account', 'amount', 'type']);
}
}

View File

@@ -1,12 +0,0 @@
<?php
namespace App\Filament\Pages;
use Filament\Pages\Page;
class GeneralLedger extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static string $view = 'filament.pages.general-ledger';
}

View File

@@ -1,18 +0,0 @@
<?php
namespace App\Filament\Pages;
use Filament\Pages\Page;
class TrialBalance extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static string $view = 'filament.pages.trial-balance';
public function getViewData(): array
{
return [];
}
}

View File

@@ -6,8 +6,13 @@ use App\DataObjects\CreateBranchDTO;
use App\Filament\Resources\ClientResource\Pages\EditClient;
use App\Filament\Resources\ClientResource\Pages\ListClients;
use App\Filament\Resources\ClientResource\Pages\ViewClient;
use App\Filament\Resources\ClientResource\Pages\GeneralLedger;
use App\Filament\Resources\ClientResource\Pages\TrialBalance;
use App\Filament\Resources\ClientResource\RelationManagers\AccountsRelationManager;
use App\Filament\Resources\ClientResource\RelationManagers\BranchesRelationManager;
use App\Filament\Resources\ClientResource\RelationManagers\ExpensesRelationManager;
use App\Filament\Resources\ClientResource\RelationManagers\JournalsRelationManager;
use App\Filament\Resources\ClientResource\RelationManagers\SalesRelationManager;
use App\Filament\Resources\ClientResource\RelationManagers\TransmittalsRelationManager;
use App\Models\Branch;
use App\Models\Client;
@@ -19,6 +24,8 @@ use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Filament\Pages\SubNavigationPosition;
use Filament\Resources\Pages\Page;
class ClientResource extends Resource
{
@@ -26,11 +33,25 @@ class ClientResource extends Resource
protected static ?string $navigationIcon = 'heroicon-o-user';
protected static ?string $recordTitleAttribute = 'company';
protected static SubNavigationPosition $subNavigationPosition = SubNavigationPosition::Top;
public static function authorizeView(Model $record): void
{
parent::authorizeView($record);
}
public static function getRecordSubNavigation(Page $page): array
{
return $page->generateNavigationItems([
ViewClient::class,
EditClient::class,
GeneralLedger::class,
TrialBalance::class,
]);
}
public static function form(Form $form): Form
{
return $form
@@ -85,6 +106,9 @@ class ClientResource extends Resource
AccountsRelationManager::class,
BranchesRelationManager::class,
TransmittalsRelationManager::class,
SalesRelationManager::class,
ExpensesRelationManager::class,
JournalsRelationManager::class,
];
}
@@ -94,6 +118,8 @@ class ClientResource extends Resource
'view' => ViewClient::route('/{record}'),
'edit' => EditClient::route('/{record}/edit'),
'index' => ListClients::route('/'),
'general-ledger' => GeneralLedger::route('/{record}/general-ledger'),
'trial-balance' => TrialBalance::route('/{record}/trial-balance'),
];
}

View File

@@ -0,0 +1,142 @@
<?php
namespace App\Filament\Resources\ClientResource\Pages;
use App\Filament\Resources\ClientResource;
use App\Models\Ledger;
use Filament\Resources\Pages\Page;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\Filter;
use Filament\Forms\Components\DatePicker;
use Illuminate\Database\Eloquent\Builder;
use Filament\Resources\Pages\Concerns\InteractsWithRecord;
use pxlrbt\FilamentExcel\Actions\Tables\ExportAction;
use pxlrbt\FilamentExcel\Exports\ExcelExport;
use pxlrbt\FilamentExcel\Columns\Column;
class GeneralLedger extends Page implements HasTable
{
use InteractsWithTable;
use InteractsWithRecord;
protected static string $resource = ClientResource::class;
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static string $view = 'filament.resources.client-resource.pages.general-ledger';
public function mount(int | string $record): void
{
$this->record = $this->resolveRecord($record);
}
public function table(Table $table): Table
{
return $table
->query(
Ledger::query()
->where('client_id', $this->getRecord()->id)
->with(['account', 'transaction', 'journal'])
)
->columns([
TextColumn::make('date')
->label('Date')
->state(function (Ledger $record) {
return $record->transaction?->happened_on?->format('Y-m-d')
?? $record->journal?->happened_on?->format('Y-m-d')
?? $record->created_at->format('Y-m-d');
})
->sortable(),
TextColumn::make('account.account')
->label('Account')
->searchable()
->sortable(),
TextColumn::make('description')
->limit(50)
->tooltip(function (TextColumn $column): ?string {
$state = $column->getState();
if (strlen($state) <= $column->getCharacterLimit()) {
return null;
}
return $state;
}),
TextColumn::make('debit_amount')
->label('Debit')
->money('PHP')
->sortable(),
TextColumn::make('credit_amount')
->label('Credit')
->money('PHP')
->sortable(),
])
->filters([
SelectFilter::make('account')
->relationship('account', 'account', fn (Builder $query) => $query->where('client_id', $this->getRecord()->id))
->searchable()
->preload(),
Filter::make('date_range')
->form([
DatePicker::make('from'),
DatePicker::make('to'),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when(
$data['from'],
fn (Builder $query, $date): Builder => $query->where(function ($q) use ($date) {
$q->whereHas('transaction', fn ($q) => $q->whereDate('happened_on', '>=', $date))
->orWhereHas('journal', fn ($q) => $q->whereDate('happened_on', '>=', $date))
->orWhere(fn($q) => $q->whereDoesntHave('transaction')->whereDoesntHave('journal')->whereDate('created_at', '>=', $date));
})
)
->when(
$data['to'],
fn (Builder $query, $date): Builder => $query->where(function ($q) use ($date) {
$q->whereHas('transaction', fn ($q) => $q->whereDate('happened_on', '<=', $date))
->orWhereHas('journal', fn ($q) => $q->whereDate('happened_on', '<=', $date))
->orWhere(fn($q) => $q->whereDoesntHave('transaction')->whereDoesntHave('journal')->whereDate('created_at', '<=', $date));
})
);
})
])
->defaultSort('created_at', 'desc')
->groups([
'account.account',
])
->headerActions([
ExportAction::make()
->label('Export General Ledger')
->exports([
ExcelExport::make()
->fromTable()
->withFilename(fn () => $this->getRecord()->name . ' - General Ledger - ' . date('Y-m-d'))
->withColumns([
Column::make('date')
->heading('Date')
->formatStateUsing(fn ($record) => $record->transaction?->happened_on?->format('Y-m-d')
?? $record->journal?->happened_on?->format('Y-m-d')
?? $record->created_at->format('Y-m-d')),
Column::make('reference')
->heading('Reference')
->formatStateUsing(fn ($record) => match(true) {
$record->transaction && $record->transaction->transactionable =>
$record->transaction->transactionable->voucher_number
?? $record->transaction->transactionable->reference_number
?? 'TR-'.$record->transaction->id,
$record->transaction => 'TR-'.$record->transaction->id,
$record->journal => $record->journal->series ?? 'JV-'.$record->journal->id,
default => 'L-'.$record->id,
}),
Column::make('account.account')->heading('Account'),
Column::make('description')->heading('Description'),
Column::make('debit_amount')->heading('Debit'),
Column::make('credit_amount')->heading('Credit'),
])
])
]);
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Filament\Resources\ClientResource\Pages;
use App\Filament\Resources\ClientResource;
use App\Models\Account;
use Filament\Resources\Pages\Page;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\Summarizers\Summarizer;
use Filament\Tables\Columns\Summarizers\Sum;
use Filament\Resources\Pages\Concerns\InteractsWithRecord;
use Illuminate\Database\Eloquent\Builder;
class TrialBalance extends Page implements HasTable
{
use InteractsWithTable;
use InteractsWithRecord;
protected static string $resource = ClientResource::class;
protected static ?string $navigationIcon = 'heroicon-o-scale';
protected static string $view = 'filament.resources.client-resource.pages.trial-balance';
public function mount(int | string $record): void
{
$this->record = $this->resolveRecord($record);
}
public function table(Table $table): Table
{
return $table
->query(
Account::query()
->where('client_id', $this->getRecord()->id)
->with(['accountType', 'latestBalance'])
)
->columns([
TextColumn::make('account')
->label('Account Name')
->sortable()
->searchable(),
TextColumn::make('debit')
->label('Debit')
->money('PHP')
->state(function (Account $record) {
$balance = $record->latestBalance?->balance ?? 0;
$normal = strtolower($record->accountType->normal_balance ?? 'debit');
return ($normal == 'debit' && $balance >= 0) || ($normal == 'credit' && $balance < 0)
? abs($balance) : 0;
})
->summarize(Summarizer::make()
->label('Total Debit')
->money('PHP')
->using(function ($query) {
return Account::query()
->whereIn('id', $query->clone()->pluck('accounts.id'))
->with(['accountType', 'latestBalance'])
->get()
->sum(function ($record) {
$balance = $record->latestBalance?->balance ?? 0;
$normal = strtolower($record->accountType->normal_balance ?? 'debit');
return ($normal == 'debit' && $balance >= 0) || ($normal == 'credit' && $balance < 0)
? abs($balance) : 0;
});
})
),
TextColumn::make('credit')
->label('Credit')
->money('PHP')
->state(function (Account $record) {
$balance = $record->latestBalance?->balance ?? 0;
$normal = strtolower($record->accountType->normal_balance ?? 'debit');
return ($normal == 'credit' && $balance >= 0) || ($normal == 'debit' && $balance < 0)
? abs($balance) : 0;
})
->summarize(Summarizer::make()
->label('Total Credit')
->money('PHP')
->using(function ($query) {
return Account::query()
->whereIn('id', $query->clone()->pluck('accounts.id'))
->with(['accountType', 'latestBalance'])
->get()
->sum(function ($record) {
$balance = $record->latestBalance?->balance ?? 0;
$normal = strtolower($record->accountType->normal_balance ?? 'debit');
return ($normal == 'credit' && $balance >= 0) || ($normal == 'debit' && $balance < 0)
? abs($balance) : 0;
});
})
),
]);
}
}

View File

@@ -2,38 +2,238 @@
namespace App\Filament\Resources\ClientResource\RelationManagers;
use App\Actions\Transactions\CreateTransactionAction;
use App\DataObjects\CreateTransactionDTO;
use App\Models\Account;
use App\Models\Branch;
use App\Models\Client;
use Awcodes\TableRepeater\Components\TableRepeater;
use Awcodes\TableRepeater\Header;
use Filament\Forms;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Pipeline;
class ExpensesRelationManager extends RelationManager
{
protected static string $relationship = 'expenses';
protected static ?string $title = 'Expenses';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('client_id')
Select::make('branch_id')
->relationship('branch', 'code', fn (Builder $query) => $query->where('client_id', $this->getOwnerRecord()->id))
->afterStateUpdated(function ($set, $get) {
$set('voucher_number', static::getVoucherNumber($get));
$set('transactions.*.branch_id', $get('branch_id'));
})
->required()
->maxLength(255),
->live(),
TextInput::make('supplier')->label('Supplier Name')->required(),
TextInput::make('reference_number')->label('Reference Number'),
TextInput::make('voucher_number')->label('Voucher Number')
->default(fn ($get) => static::getVoucherNumber($get))
->readOnly(),
DatePicker::make('happened_on')->label('Date')
->required()
->afterStateUpdated(function ($set, $get) {
$set('transactions.*.happened_on', $get('happened_on'));
})
->live()
->native(false),
TableRepeater::make('transactions')
->headers(fn (Get $get): array => static::getTransactionTableHeader($get))
->relationship('transactions')
->schema(fn (Get $get): array => $this->getTransactionTableFormSchema($get))
->visible(fn (Get $get) => $get('branch_id') != null)
->columnSpan('full'),
]);
}
public static function getVoucherNumber(Get $get): string
{
$branch = Branch::find($get('branch_id'));
if ($branch) {
return GenerateVoucher::execute($branch);
}
return '';
}
private static function getTransactionTableHeader(Get $get): array
{
return [
Header::make('Charge Account'),
Header::make('Description'),
Header::make('Gross Amount'),
Header::make('Exempt'),
Header::make('Zero Rated'),
Header::make('Vatable Amount'),
Header::make('Input Tax'),
Header::make('Withholding Tax'),
Header::make('Net Amount'),
];
}
private function getTransactionTableFormSchema(Get $get): array
{
return [
Select::make('account_id')->options(fn ($get) => $this->getAccountOptions($get)),
TextInput::make('description')->label('Description'),
Hidden::make('branch_id')->default(fn (Get $get) => $get('../../branch_id')),
TextInput::make('gross_amount')
->numeric()
->live(false, 500)
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
$this->setDefaultFormValues($get, $set, $old, $state);
})->default(0),
TextInput::make('exempt')
->numeric()
->live()
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
$this->setDefaultFormValues($get, $set, $old, $state);
})->default(0),
TextInput::make('zero_rated')
->numeric()
->live()
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
$this->setDefaultFormValues($get, $set, $old, $state);
})->default(0),
TextInput::make('vatable_amount')
->numeric()
->nullable()
->live()
->readOnly()
->default(0),
Hidden::make('happened_on')->default(fn (Get $get) => $get('../../happened_on')),
TextInput::make('input_tax')
->numeric()
->live()
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
$this->setDefaultFormValues($get, $set, $old, $state);
})->default(0),
TextInput::make('payable_withholding_tax')
->numeric()
->live()
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
$this->setDefaultFormValues($get, $set, $old, $state);
})->default(0),
TextInput::make('net_amount')->numeric()->default(0),
];
}
private function getAccountOptions(Get $get): Collection
{
$query = Account::query();
$query->where([
'client_id' => $this->getOwnerRecord()->id,
]);
if ($get('../../branch_id')) {
$query->whereHas('balances', function ($query) use ($get) {
return $query->where('branch_id', $get('../../branch_id'));
});
}
$query->whereHas('accountType', function ($query) {
return $query->where('type', 'Expenses');
});
return $query->get()->pluck('account', 'id');
}
private function setDefaultFormValues(Get $get, Set $set, ?string $old, ?string $state): void
{
$exempt = (float) $get('exempt');
$withHoldingTax = (float) $get('payable_withholding_tax');
$vatableSales = $get('gross_amount');
$vatableAmount = 0;
if ($vatableSales) {
$vatableAmount = $vatableSales / 1.12;
}
$inputTax = $vatableAmount * 0.12;
$netAmount = (int) $vatableSales - $get('payable_withholding_tax');
if ($this->getOwnerRecord()->vatable) {
$netAmount = ($vatableAmount + $exempt) - $withHoldingTax;
}
$set('input_tax', number_format($inputTax, 2, '.', ''));
$set('vatable_amount', number_format($vatableAmount, 2, '.', ''));
$set('net_amount', number_format($netAmount, 2, '.', ''));
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('client_id')
->recordTitleAttribute('supplier')
->columns([
Tables\Columns\TextColumn::make('client_id'),
TextColumn::make('supplier'),
TextColumn::make('reference_number'),
TextColumn::make('voucher_number'),
TextColumn::make('branch.code'),
TextColumn::make('happened_on'),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
Tables\Actions\CreateAction::make()
->using(function (array $data, string $model) {
$transactions = $data['transactions'] ?? [];
$data = Arr::except($data, ['transactions']);
$record = $model::create($data);
try {
$branch = $record->branch;
foreach ($transactions as $transaction) {
$tData = [
'branch_id' => $branch->id,
'happened_on' => $record->happened_on,
...$transaction,
];
$payload = new CreateTransactionDTO(data: $tData, transactionable: $record);
Pipeline::send(passable: $payload)->through(
[
CreateTransactionAction::class,
]
)->thenReturn();
}
} catch (\Exception $exception) {
throw new \Exception('Failed to save transactions : '.$exception->getMessage());
}
return $record;
}),
])
->actions([
Tables\Actions\EditAction::make(),

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

View File

@@ -0,0 +1,275 @@
<?php
namespace App\Filament\Resources\ClientResource\RelationManagers;
use App\Actions\Transactions\CreateTransactionAction;
use App\DataObjects\CreateTransactionDTO;
use Illuminate\Support\Facades\Pipeline;
use App\Models\Account;
use App\Models\Branch;
use App\Models\Client;
use Awcodes\TableRepeater\Components\TableRepeater;
use Awcodes\TableRepeater\Header;
use Filament\Forms;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class SalesRelationManager extends RelationManager
{
protected static string $relationship = 'sales';
protected static ?string $title = 'Sales';
public function form(Form $form): Form
{
return $form
->schema([
Select::make('branch_id')
->relationship('branch', 'code', fn (Builder $query) => $query->where('client_id', $this->getOwnerRecord()->id))
->required()
->afterStateUpdated(function ($set, $get) {
$set('current_series', static::getSeries($get));
$set('transactions.*.branch_id', $get('branch_id'));
})
->live(),
TextInput::make('current_series')
->label('Series')
->disabled(),
DatePicker::make('happened_on')->label('Date')
->required()
->afterStateUpdated(function ($set, $get) {
$set('transactions.*.happened_on', $get('happened_on'));
})
->native(false),
Checkbox::make('with_discount')->label('With Discount?')->default(false)->live(),
TableRepeater::make('transactions')
->headers(fn (Get $get): array => static::getTransactionTableHeader($get))
->relationship('transactions')
->schema(fn (Get $get): array => $this->getTransactionTableFormSchema($get))
->visible(fn (Get $get) => $get('branch_id') != null)
->columnSpan('full'),
]);
}
public static function getSeries(Get $get): string
{
$branch = Branch::find($get('branch_id'));
if ($branch) {
$currentSeries = $branch->current_series;
return str_pad($currentSeries + 1, 6, '0', STR_PAD_LEFT);
}
return '';
}
private static function getTransactionTableHeader(Get $get): array
{
if ($get('with_discount')) {
return [
Header::make('Charge Account'),
Header::make('Description'),
Header::make('Gross Amount'),
Header::make('Exempt'),
Header::make('Vatable Amount'),
Header::make('Output Tax'),
Header::make('Withholding Tax'),
Header::make('Discount'),
Header::make('Net Amount'),
];
}
return [
Header::make('Charge Account'),
Header::make('Description'),
Header::make('Gross Amount'),
Header::make('Exempt'),
Header::make('Vatable Amount'),
Header::make('Output Tax'),
Header::make('Withholding Tax'),
Header::make('Net Amount'),
];
}
private function getTransactionTableFormSchema(Get $get): array
{
return [
Select::make('account_id')->options(fn ($get) => $this->getAccountOptions($get)),
TextInput::make('description')->label('Description'),
Hidden::make('branch_id')->default(fn (Get $get) => $get('../../branch_id')),
TextInput::make('gross_amount')
->numeric()
->live(false, 500)
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
$this->setDefaultFormValues($get, $set, $old, $state);
})->default(0),
TextInput::make('exempt')
->numeric()
->live()
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
$this->setDefaultFormValues($get, $set, $old, $state);
})->default(0),
TextInput::make('vatable_amount')
->numeric()
->nullable()
->live()
->readOnly()
->default(0),
Hidden::make('happened_on')->default(fn (Get $get) => $get('../../happened_on')),
Hidden::make('with_discount')->default(fn (Get $get) => $get('../../with_discount')),
TextInput::make('output_tax')
->numeric()
->live()
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
$this->setDefaultFormValues($get, $set, $old, $state);
})->default(0),
TextInput::make('payable_withholding_tax')
->numeric()
->live()
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
$this->setDefaultFormValues($get, $set, $old, $state);
})->default(0),
TextInput::make('discount')
->numeric()
->readOnly()
->visible(fn (Get $get) => $get('../../with_discount'))
->live(),
TextInput::make('net_amount')->numeric()->default(0),
];
}
private function getAccountOptions($get)
{
$query = Account::query();
$query->where([
'client_id' => $this->getOwnerRecord()->id,
]);
if ($get('../../branch_id')) {
$query->whereHas('balances', function ($query) use ($get) {
return $query->where('branch_id', $get('../../branch_id'));
});
}
$query->whereHas('accountType', function ($query) {
return $query->where('type', 'Revenue');
});
return $query->get()->pluck('account', 'id');
}
private function setDefaultFormValues(Get $get, Set $set, ?string $old, ?string $state)
{
$exempt = (float) $get('exempt');
$withHoldingTax = (float) $get('payable_withholding_tax');
$vatableSales = $get('gross_amount');
$vatableAmount = 0;
if ($vatableSales) {
$vatableAmount = $vatableSales / 1.12;
}
$discount = $exempt * .20;
$outputTax = $vatableAmount * 0.12;
//default net amount
$netAmount = (int) $vatableSales - $get('payable_withholding_tax');
//net amount if vatable
if ($this->getOwnerRecord()->vatable) {
$netAmount = ($vatableAmount + $exempt) - $withHoldingTax;
}
//if discounted
if ($get('../../with_discount')) {
$netAmount = $netAmount - $discount;
}
$set('output_tax', number_format($outputTax, 2, '.', ''));
$set('discount', number_format($discount, 2, '.', ''));
$set('vatable_amount', number_format($vatableAmount, 2, '.', ''));
$set('net_amount', number_format($netAmount, 2, '.', ''));
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('title')
->columns([
TextColumn::make('id')->label('ID')->sortable(),
TextColumn::make('branch.code')->label('Branch')->sortable(),
TextColumn::make('happened_on')->label('Date')->date()->sortable(),
TextColumn::make('gross_amount')->label('Gross Amount')->numeric()->sortable(),
TextColumn::make('exempt')->label('Exempt')->numeric()->sortable(),
TextColumn::make('vatable_amount')->label('Vatable Amount')->numeric()->sortable(),
TextColumn::make('output_tax')->label('Output Tax')->numeric()->sortable(),
TextColumn::make('payable_withholding_tax')->label('Payable Withholding Tax')->numeric()->sortable(),
TextColumn::make('discount')->label('Discount')->numeric()->sortable(),
TextColumn::make('net_amount')->label('Net Amount')->numeric()->sortable(),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make()
->using(function (array $data, string $model) {
$transactions = $data['transactions'] ?? [];
$data = Arr::except($data, ['transactions']);
$record = $model::create($data);
try {
$branch = $record->branch;
foreach ($transactions as $transaction) {
$tData = [
'branch_id' => $branch->id,
'happened_on' => $record->happened_on,
...$transaction,
];
$payload = new CreateTransactionDTO(data: $tData, transactionable: $record);
Pipeline::send(passable: $payload)->through(
[
CreateTransactionAction::class,
]
)->thenReturn();
}
} catch (\Exception $exception) {
throw new \Exception('Failed to save transactions : '.$exception->getMessage());
}
return $record;
}),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -29,7 +29,9 @@ class ExpenseResource extends Resource
protected static bool $isVatable;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationIcon = 'heroicon-o-banknotes';
protected static bool $shouldRegisterNavigation = false;
public static function form(Form $form): Form
{

View File

@@ -19,13 +19,16 @@ use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class SaleResource extends Resource
{
protected static ?string $model = Sale::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
protected static bool $shouldRegisterNavigation = false;
public static function form(Form $form): Form
{
@@ -215,7 +218,17 @@ class SaleResource extends Resource
{
return $table
->columns([
//
TextColumn::make('id')->label('ID')->sortable(),
TextColumn::make('client.name')->label('Client')->sortable(),
TextColumn::make('branch.name')->label('Branch')->sortable(),
TextColumn::make('happened_on')->label('Date')->date()->sortable(),
TextColumn::make('gross_amount')->label('Gross Amount')->numeric()->sortable(),
TextColumn::make('exempt')->label('Exempt')->numeric()->sortable(),
TextColumn::make('vatable_amount')->label('Vatable Amount')->numeric()->sortable(),
TextColumn::make('output_tax')->label('Output Tax')->numeric()->sortable(),
TextColumn::make('payable_withholding_tax')->label('Payable Withholding Tax')->numeric()->sortable(),
TextColumn::make('discount')->label('Discount')->numeric()->sortable(),
TextColumn::make('net_amount')->label('Net Amount')->numeric()->sortable(),
])
->filters([
//

View File

@@ -24,6 +24,5 @@ class CreateSale extends CreateRecord
protected function afterCreate(): void
{
$branch = Branch::find($this->data['branch_id']);
dd($branch);
}
}

View File

@@ -47,6 +47,16 @@ class Account extends Model
return $this->hasMany(Balance::class);
}
public function latestBalance(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(Balance::class)->latestOfMany();
}
public function ledgers(): HasMany
{
return $this->hasMany(Ledger::class);
}
public function getCurrentBalanceAttribute()
{
if ($this->balances()->exists()) {

View File

@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
#[ObservedBy([ClientObserver::class])]
class Client extends Model
@@ -62,4 +63,14 @@ class Client extends Model
{
return $this->hasMany(Transmittal::class);
}
public function sales(): HasManyThrough
{
return $this->hasManyThrough(Sale::class, Branch::class);
}
public function expenses(): HasManyThrough
{
return $this->hasManyThrough(Expense::class, Branch::class);
}
}

View File

@@ -7,7 +7,7 @@ use App\Traits\HasUser;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Journal extends Model
{
@@ -17,9 +17,9 @@ class Journal extends Model
protected $casts = ['happened_on' => 'date'];
public function ledger(): HasOne
public function ledgers(): HasMany
{
return $this->hasOne(Ledger::class);
return $this->hasMany(Ledger::class);
}
public function scopeDateCreatedFilter(Builder $query, $date) : Builder

View File

@@ -15,6 +15,10 @@ class Transaction extends Model
protected $guarded = [];
protected $casts = [
'happened_on' => 'date',
];
public function transactionable(): MorphTo
{
return $this->morphTo();

View File

@@ -22,6 +22,10 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
Gate::before(function ($user, $ability) {
return $user->hasRole('super_admin') ? true : null;
});
Gate::policy(Role::class, RolePolicy::class);
}
}

View File

@@ -25,6 +25,7 @@
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/boost": "^2.1",
"laravel/breeze": "^2.1",
"laravel/pint": "^1.13",
"laravel/sail": "^1.26",

4803
composer.lock generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{["x-on:blur"]:"createTag()",["x-model"]:"newTag",["x-on:keydown"](t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},["x-on:paste"](){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default};
function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{"x-on:blur":"createTag()","x-model":"newTag","x-on:keydown"(t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},"x-on:paste"(){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
function n(){return{checkboxClickController:null,collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){if(this.isLoading=!0,this.areRecordsSelected(this.getRecordsInGroupOnPage(e))){this.deselectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e));return}this.selectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e)),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){this.checkboxClickController&&this.checkboxClickController.abort(),this.checkboxClickController=new AbortController;let{signal:e}=this.checkboxClickController;this.$root?.addEventListener("click",t=>t.target?.matches(".fi-ta-record-checkbox")&&this.handleCheckboxClick(t,t.target),{signal:e})},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let o=s.indexOf(this.lastChecked),r=s.indexOf(t),l=[o,r].sort((i,d)=>i-d),c=[];for(let i=l[0];i<=l[1];i++)s[i].checked=t.checked,c.push(s[i].value);t.checked?this.selectRecords(c):this.deselectRecords(c)}this.lastChecked=t}}}export{n as default};
function d(){return{checkboxClickController:null,collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){this.isLoading=!0;let t=await this.$wire.getGroupedSelectableTableRecordKeys(e);this.areRecordsSelected(this.getRecordsInGroupOnPage(e))?this.deselectRecords(t):this.selectRecords(t),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){this.checkboxClickController&&this.checkboxClickController.abort(),this.checkboxClickController=new AbortController;let{signal:e}=this.checkboxClickController;this.$root?.addEventListener("click",t=>t.target?.matches(".fi-ta-record-checkbox")&&this.handleCheckboxClick(t,t.target),{signal:e})},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let l=s.indexOf(this.lastChecked),r=s.indexOf(t),o=[l,r].sort((c,n)=>c-n),i=[];for(let c=o[0];c<=o[1];c++)s[c].checked=t.checked,i.push(s[c].value);t.checked?this.selectRecords(i):this.deselectRecords(i)}this.lastChecked=t}}}export{d as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,69 +0,0 @@
<x-filament-panels::page>
<div class="relative overflow-x-auto">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">
Product name
</th>
<th scope="col" class="px-6 py-3">
Color
</th>
<th scope="col" class="px-6 py-3">
Category
</th>
<th scope="col" class="px-6 py-3">
Price
</th>
</tr>
</thead>
<tbody>
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
Apple MacBook Pro 17"
</th>
<td class="px-6 py-4">
Silver
</td>
<td class="px-6 py-4">
Laptop
</td>
<td class="px-6 py-4">
$2999
</td>
</tr>
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
Microsoft Surface Pro
</th>
<td class="px-6 py-4">
White
</td>
<td class="px-6 py-4">
Laptop PC
</td>
<td class="px-6 py-4">
$1999
</td>
</tr>
<tr class="bg-white dark:bg-gray-800">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
Magic Mouse 2
</th>
<td class="px-6 py-4">
Black
</td>
<td class="px-6 py-4">
Accessories
</td>
<td class="px-6 py-4">
$99
</td>
</tr>
</tbody>
</table>
</div>
</x-filament-panels::page>

View File

@@ -1,45 +0,0 @@
<x-filament-panels::page>
<div class="relative overflow-x-auto">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">
Account
</th>
<th scope="col" class="px-6 py-3">
Debit Amount
</th>
<th scope="col" class="px-6 py-3">
Credit Amount
</th>
</tr>
</thead>
<tbody>
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
Apple MacBook Pro 17"
</th>
<td class="px-6 py-4">
$2999
</td>
<td class="px-6 py-4">
$2999
</td>
</tr>
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" class="px-6 py-4 font-medium text-red-700 whitespace-nowrap dark:text-red-700">
Total
</th>
<td class="px-6 py-4">
$1999
</td>
<td class="px-6 py-4">
$1999
</td>
</tr>
</tbody>
</table>
</div>
</x-filament-panels::page>

View File

@@ -0,0 +1,3 @@
<x-filament-panels::page>
{{ $this->table }}
</x-filament-panels::page>

View File

@@ -0,0 +1,3 @@
<x-filament-panels::page>
{{ $this->table }}
</x-filament-panels::page>

62
trialbalance.md Normal file
View File

@@ -0,0 +1,62 @@
I need to add comprehensive trial balance functionality to my Filament application with adjustment capabilities:
1. Create models for:
- TrialBalance (main trial balance report)
- TrialBalanceAdjustment (for adjusting entries)
- SuspenseAccount (temporary holding for discrepancies)
2. TrialBalance fields:
- period_start_date, period_end_date
- account_code, account_name
- debit_balance, credit_balance
- account_type (asset, liability, equity, revenue, expense)
- is_adjusted (boolean)
- created_at, updated_at
3. TrialBalanceAdjustment fields:
- trial_balance_id (foreign key)
- adjustment_type (enum: accrual, deferral, depreciation, correction, suspense)
- account_code
- description
- debit_amount, credit_amount
- adjustment_date
- created_by (user_id)
- status (enum: draft, posted, reversed)
4. Filament Resource features:
- Main trial balance table showing all accounts with balances
- Summary footer showing:
* Total Debits
* Total Credits
* Difference (highlighted in red if non-zero)
- Action button "Add Adjustment Entry" that opens a modal form
- Adjustment form with:
* Account selection dropdown
* Adjustment type selector
* Debit/Credit amount inputs (with validation that one must be zero)
* Description field
* Auto-calculation of new balance
- "Create Suspense Entry" button for unexplained differences
- Table filter for: adjusted/unadjusted entries, date range, account type
- Relation manager showing all adjustments for each account
- Color coding: green when balanced, red when unbalanced
- "Post Adjustments" action to finalize entries
- "Adjusted Trial Balance" view showing balances after adjustments
5. Additional features:
- Audit trail for all adjustments
- Ability to reverse adjustments
- Automatic journal entry creation from adjustments
- Export to PDF/Excel with adjustments detailed
- Before/after comparison view
- Permission checks (only accountants can add adjustments)
6. Include these methods:
- calculateTrialBalance() - generates from ledger
- addAdjustment() - creates adjustment entry
- postAdjustments() - finalizes and posts to general ledger
- reverseAdjustment() - reverses a posted adjustment
- getAdjustedBalance() - calculates balance after adjustments
- checkBalance() - verifies debits equal credits
Please provide the complete code including models, migrations, Fila ment resources, and form components.