13 Commits

Author SHA1 Message Date
Jp
fc672e4f4a feat(TransmittalResource): enhance search functionality across related models
Implement wildcard search on transmittal series, notes, and files to improve user experience when filtering records. Also extend search capabilities to client company and branch code fields for more comprehensive filtering.
2026-02-12 16:12:35 +08:00
Jp
a77e95d2a5 feat: implement FilamentUser interface for User model
Add FilamentUser interface to allow access control within Filament admin panel. The canAccessPanel method currently returns true for all users, providing a foundation for future permission-based access restrictions.
2026-02-11 05:44:49 +08:00
Jp
ee65bdfb31 fix: force HTTPS scheme in all environments
Previously HTTPS was only enforced in production, which could lead to insecure URLs in other environments like staging. This change ensures consistent URL generation across all environments.
2026-02-11 05:41:22 +08:00
Jp
7fa8b75b29 fix: enforce HTTPS in production environment
Add URL::forceScheme('https') in AppServiceProvider to ensure all generated URLs use HTTPS when the application is in production. This improves security by enforcing secure connections.
2026-02-11 05:37:54 +08:00
Jp
a8ad07676a Merge branch 'main' of https://git.jpaleviado.site/kingjaypee12/MKM 2026-02-11 05:27:40 +08:00
Jp
6f04a60e43 Merge branch 'main' of https://github.com/kingjaypee12/MKM-App 2026-02-11 05:25:34 +08:00
Jp
76a52d7e82 fix(database): update foreign key constraints to cascade on delete
Update foreign key constraints on accounts and transmittals tables to cascade deletions, ensuring data integrity when referenced clients or branches are removed. The down migrations revert to the previous behavior.
2026-02-11 05:24:44 +08:00
5715ab10f2 Merge pull request 'update/ledger' (#1) from update/ledger into main
Reviewed-on: #1
2026-02-10 07:07:10 +00:00
138740648c Merge pull request #2 from kingjaypee12/update/ledger
refactor: streamline sales and expenses management in client resource
2026-02-10 15:06:57 +08:00
Jp
0131193b8d refactor: streamline sales and expenses management in client resource
- Move create/edit logic from relation managers to dedicated resource pages
- Add transaction handling with proper rollback in sale create/update
- Fix expense transaction creation by using correct array access
- Set default client from query parameter in sale/expense forms
- Exclude 'type' field from balance creation to prevent errors
2026-02-10 15:05:36 +08:00
13a0f69ce3 Merge pull request #1 from kingjaypee12/update/ledger
fix: cascade delete related transactions and ledgers for sales and ex…
2026-02-09 22:26:19 +08:00
Jp
0e4da559d6 fix: cascade delete related transactions and ledgers for sales and expenses
Ensure data integrity by cleaning up dependent records when deleting Sale or Expense models. This prevents orphaned transactions and ledgers in the database.
2026-02-09 22:25:08 +08:00
Jp
1548e178bc feat(client): add journals relation and improve transaction ledger handling
- Add journals relation to Client model via Branch
- Update branch selection in JournalsRelationManager to use code instead of name
- Improve tax, withholding, and cash account ledger queries by adding client_id filter and amount checks
- Add missing import for GenerateVoucher command in ExpensesRelationManager
- Label 'Normal Balance' column in AccountsRelationManager
2026-02-09 22:16:10 +08:00
19 changed files with 302 additions and 445 deletions

View File

@@ -15,7 +15,7 @@ class CreateBalanceAction extends BaseAction
public function __invoke(Data $payload, Closure $next)
{
$this->createBalanceCommand->execute($payload->balanceDTO->except('amount')->toArray());
$this->createBalanceCommand->execute($payload->balanceDTO->except('amount', 'type')->toArray());
return $next($payload);
}

View File

@@ -71,15 +71,21 @@ class CreateTransactionAction extends BaseAction
{
$accountName = $isExpense ? 'Input Tax' : 'Output Tax';
$type = $isExpense ? 'debit' : 'credit';
$clientId = $payload->transactionable->branch->client_id;
$taxAccount = Account::query()->where('account', $accountName)->whereHas('balances', function ($balance) use ($payload) {
return $balance->where('branch_id', $payload->transactionable->branch_id);
})->first();
$taxAccount = Account::query()
->where('account', $accountName)
->where('client_id', $clientId)
->first();
if ($taxAccount) {
$amount = $isExpense
? ($payload->transactionable->input_tax ?? 0.00)
: ($payload->transactionable->output_tax ?? 0.00);
if ($taxAccount && $amount > 0) {
$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?
amount: $amount,
transaction: $payload->transaction,
account: $taxAccount,
type: $type,
@@ -93,15 +99,19 @@ class CreateTransactionAction extends BaseAction
$isExpense = $payload->transactionable instanceof \App\Models\Expense;
$accountName = $isExpense ? 'Payable Withholding Tax' : 'Creditable Withholding Tax';
$type = $isExpense ? 'credit' : 'debit';
$clientId = $payload->transactionable->branch->client_id;
$withholdingAccount = Account::query()->where('account', $accountName)->whereHas('balances', function ($balance) use ($payload) {
return $balance->where('branch_id', $payload->transactionable->branch_id);
})->first();
$withholdingAccount = Account::query()
->where('account', $accountName)
->where('client_id', $clientId)
->first();
if ($withholdingAccount) {
$amount = $payload->transaction->payable_withholding_tax ?? 0.00;
if ($withholdingAccount && $amount > 0) {
$ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id,
amount: $payload->transaction->payable_withholding_tax ?? 0.00,
amount: $amount,
transaction: $payload->transaction,
account: $withholdingAccount,
type: $type,
@@ -116,12 +126,14 @@ class CreateTransactionAction extends BaseAction
$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;
$clientId = $payload->transactionable->branch->client_id;
$cashAccount = Account::query()->where('account', 'Cash')->whereHas('balances', function ($balance) use ($payload) {
return $balance->where('branch_id', $payload->transactionable->branch_id);
})->first();
$cashAccount = Account::query()
->where('account', 'Cash')
->where('client_id', $clientId)
->first();
if ($cashAccount) {
if ($cashAccount && $amount > 0) {
$ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id,
amount: $amount,

View File

@@ -37,7 +37,7 @@ class AccountsRelationManager extends RelationManager
->columns([
Tables\Columns\TextColumn::make('account')->description(fn (Account $record): string => $record->description ?? ''),
Tables\Columns\TextColumn::make('accountType.type'),
Tables\Columns\TextColumn::make('accountType.normal_balance'),
Tables\Columns\TextColumn::make('accountType.normal_balance')->label('Normal Balance'),
])
->filters([
//

View File

@@ -2,30 +2,13 @@
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 App\Filament\Resources\ExpenseResource;
use App\Models\Expense;
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
{
@@ -36,155 +19,7 @@ class ExpensesRelationManager extends RelationManager
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))
->afterStateUpdated(function ($set, $get) {
$set('voucher_number', static::getVoucherNumber($get));
$set('transactions.*.branch_id', $get('branch_id'));
})
->required()
->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, '.', ''));
->schema([]);
}
public function table(Table $table): Table
@@ -203,40 +38,11 @@ class ExpensesRelationManager extends RelationManager
])
->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;
}),
->url(fn () => ExpenseResource::getUrl('create', ['client_id' => $this->getOwnerRecord()->id])),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\EditAction::make()
->url(fn (Expense $record) => ExpenseResource::getUrl('edit', ['record' => $record])),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([

View File

@@ -35,7 +35,7 @@ class JournalsRelationManager extends RelationManager
->schema([
Select::make('branch_id')
->label('Branch')
->options(fn () => Branch::where('client_id', $this->getOwnerRecord()->id)->pluck('name', 'id'))
->options(fn () => Branch::where('client_id', $this->getOwnerRecord()->id)->pluck('code', 'id'))
->required()
->default(fn () => Branch::where('client_id', $this->getOwnerRecord()->id)->first()?->id),

View File

@@ -2,31 +2,14 @@
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 App\Filament\Resources\SaleResource;
use App\Models\Sale;
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
{
@@ -36,178 +19,7 @@ class SalesRelationManager extends RelationManager
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, '.', ''));
return $form->schema([]);
}
public function table(Table $table): Table
@@ -231,39 +43,11 @@ class SalesRelationManager extends RelationManager
])
->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;
}),
->url(fn () => SaleResource::getUrl('create', ['client_id' => $this->getOwnerRecord()->id])),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\EditAction::make()
->url(fn (Sale $record) => SaleResource::getUrl('edit', ['record' => $record])),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([

View File

@@ -43,6 +43,7 @@ class ExpenseResource extends Resource
{
return [
Select::make('client')
->default(request()->query('client_id'))
->options(Client::query()->get()->pluck('company', 'id'))
->afterStateUpdated(function ($set, $get) {
$set('branch_id', '');

View File

@@ -28,7 +28,7 @@ class CreateExpense extends CreateRecord
protected function afterCreate(): void
{
$transactions = Arr::only($this->form->getState(), ['transactions']);
$transactions = $this->form->getState()['transactions'] ?? [];
try {
$branch = $this->getRecord()->branch;
@@ -38,7 +38,7 @@ class CreateExpense extends CreateRecord
$data = [
'branch_id' => $branch->id,
'happened_on' => $this->getRecord()->happened_on,
...Arr::first($transaction),
...$transaction,
];
$payload = new CreateTransactionDTO(data: $data, transactionable: $this->getRecord());

View File

@@ -35,6 +35,7 @@ class SaleResource extends Resource
return $form
->schema([
Select::make('client')
->default(request()->query('client_id'))
->options(Client::query()->get()->pluck('company', 'id'))
->afterStateUpdated(function ($set, $get) {
$set('branch_id', '');

View File

@@ -2,10 +2,16 @@
namespace App\Filament\Resources\SaleResource\Pages;
use App\Actions\Transactions\CreateTransactionAction;
use App\DataObjects\CreateTransactionDTO;
use App\Filament\Resources\SaleResource;
use App\Models\Branch;
use App\Models\Sale;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Pipeline;
class CreateSale extends CreateRecord
{
@@ -16,11 +22,48 @@ class CreateSale extends CreateRecord
return $this->getFormDataMutation($data);
}
protected function handleRecordCreation(array $data): Model
{
$transactions = $this->data['transactions'] ?? [];
return $this->processCreate($data, $transactions);
}
public function getFormDataMutation(array $data): array
{
return Arr::except($data, ['client', 'transactions', 'with_discount']);
}
public function processCreate(array $data, array $transactions): Model
{
try {
DB::beginTransaction();
$record = Sale::create($data);
$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();
}
DB::commit();
} catch (\Exception $exception) {
DB::rollBack();
throw new \Exception('Failed to save transactions : '.$exception->getMessage());
}
return $record;
}
protected function afterCreate(): void
{
$branch = Branch::find($this->data['branch_id']);

View File

@@ -2,9 +2,15 @@
namespace App\Filament\Resources\SaleResource\Pages;
use App\Actions\Transactions\CreateTransactionAction;
use App\DataObjects\CreateTransactionDTO;
use App\Filament\Resources\SaleResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Pipeline;
class EditSale extends EditRecord
{
@@ -16,4 +22,61 @@ class EditSale extends EditRecord
Actions\DeleteAction::make(),
];
}
protected function mutateFormDataBeforeSave(array $data): array
{
return $this->getFormDataMutation($data);
}
protected function handleRecordUpdate(Model $record, array $data): Model
{
$transactions = $this->data['transactions'] ?? [];
return $this->processUpdate($record, $data, $transactions);
}
public function getFormDataMutation(array $data): array
{
return Arr::except($data, ['client', 'transactions', 'with_discount']);
}
public function processUpdate(Model $record, array $data, array $transactions): Model
{
try {
DB::beginTransaction();
$record->update($data);
$branch = $record->branch;
// Delete existing transactions and their related ledgers/balances
$record->transactions->each(function ($transaction) {
$transaction->ledgers->each(function ($ledger) {
$ledger->balances()->delete();
$ledger->delete();
});
$transaction->delete();
});
// Create new transactions
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();
}
DB::commit();
} catch (\Exception $exception) {
DB::rollBack();
throw new \Exception('Failed to save transactions : '.$exception->getMessage());
}
return $record;
}
}

View File

@@ -45,16 +45,36 @@ class TransmittalResource extends Resource
->columns([
Tables\Columns\Layout\Split::make([
Tables\Columns\TextColumn::make('series')
->searchable()
->searchable(query: function (Builder $query, string $search): Builder {
$wildcardSearch = '%' . str_replace(' ', '%', $search) . '%';
return $query->where(function (Builder $query) use ($wildcardSearch) {
$query->where('series', 'like', $wildcardSearch)
->orWhereHas('notes', function (Builder $query) use ($wildcardSearch) {
$query->where('comment', 'like', $wildcardSearch);
})
->orWhereHas('files', function (Builder $query) use ($wildcardSearch) {
$query->where('description', 'like', $wildcardSearch);
});
});
})
->label('Series')
->weight(FontWeight::Bold)
->columnSpan(2),
Tables\Columns\Layout\Stack::make([
Tables\Columns\TextColumn::make('client.company')
->searchable()
->searchable(query: function (Builder $query, string $search): Builder {
return $query->whereHas('client', function (Builder $query) use ($search) {
$query->where('company', 'like', '%' . str_replace(' ', '%', $search) . '%');
});
})
->weight(FontWeight::SemiBold)->label('Client'),
Tables\Columns\TextColumn::make('branch.code')
->searchable()
->searchable(query: function (Builder $query, string $search): Builder {
return $query->whereHas('branch', function (Builder $query) use ($search) {
$query->where('code', 'like', '%' . str_replace(' ', '%', $search) . '%');
});
})
->label('Branch'),
]),
]),

View File

@@ -73,4 +73,9 @@ class Client extends Model
{
return $this->hasManyThrough(Expense::class, Branch::class);
}
public function journals(): HasManyThrough
{
return $this->hasManyThrough(Journal::class, Branch::class);
}
}

View File

@@ -25,6 +25,20 @@ class Expense extends Model
return $this->morphMany(Transaction::class, 'transactionable');
}
protected static function booted()
{
static::deleting(function ($expense) {
$expense->transactions->each(function ($transaction) {
// Delete associated ledgers first to trigger any ledger deletion logic if exists
$transaction->ledgers->each(function ($ledger) {
$ledger->balances()->delete(); // Delete balances associated with ledger
$ledger->delete();
});
$transaction->delete();
});
});
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class);

View File

@@ -25,6 +25,20 @@ class Sale extends Model
return $this->morphMany(Transaction::class, 'transactionable');
}
protected static function booted()
{
static::deleting(function ($sale) {
$sale->transactions->each(function ($transaction) {
// Delete associated ledgers first to trigger any ledger deletion logic if exists
$transaction->ledgers->each(function ($ledger) {
$ledger->balances()->delete(); // Delete balances associated with ledger
$ledger->delete();
});
$transaction->delete();
});
});
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class);

View File

@@ -3,16 +3,23 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasPermissions;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
class User extends Authenticatable implements FilamentUser
{
use HasFactory, HasPermissions, HasRoles, Notifiable;
public function canAccessPanel(Panel $panel): bool
{
return true;
}
/**
* The attributes that are mass assignable.
*

View File

@@ -4,6 +4,7 @@ namespace App\Providers;
use App\Policies\RolePolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Spatie\Permission\Models\Role;
@@ -22,6 +23,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
URL::forceScheme('https');
Gate::before(function ($user, $ability) {
return $user->hasRole('super_admin') ? true : null;
});

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('accounts', function (Blueprint $table) {
$table->dropForeign(['client_id']);
$table->foreign('client_id')
->references('id')
->on('clients')
->cascadeOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('accounts', function (Blueprint $table) {
$table->dropForeign(['client_id']);
$table->foreign('client_id')
->references('id')
->on('clients')
->nullOnDelete(); // Since it is nullable
});
}
};

View File

@@ -0,0 +1,48 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('transmittals', function (Blueprint $table) {
$table->dropForeign(['client_id']);
$table->dropForeign(['branch_id']);
$table->foreign('client_id')
->references('id')
->on('clients')
->cascadeOnDelete();
$table->foreign('branch_id')
->references('id')
->on('branches')
->cascadeOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('transmittals', function (Blueprint $table) {
$table->dropForeign(['client_id']);
$table->dropForeign(['branch_id']);
$table->foreign('client_id')
->references('id')
->on('clients');
$table->foreign('branch_id')
->references('id')
->on('branches');
});
}
};