25 Commits

Author SHA1 Message Date
3cf5f6db6a Merge pull request #6 from kingjaypee12/fix/sales-related-table
feat: add account linking and improve sales table
2026-02-15 18:57:49 +08:00
Jp
f5c8ec04ad feat: add account linking and improve sales table
- Add many-to-many relationships between Sale/Expense and Account models
- Create pivot tables for account_sale and account_expense with migrations
- Implement account syncing during sale/expense creation and editing
- Add accounts_list attribute to display comma-separated account names
- Introduce SaleService with DTO for sale creation logic
- Simplify sales table columns to show branch, reference, date, and creator
- Calculate and store aggregated financial fields from transactions
- Make series field read-only instead of disabled in sale form
2026-02-15 18:57:18 +08:00
7bbe6e2d2a Merge pull request #5 from kingjaypee12/jp/fix-sales-breadcrumbs
feat(client): add client context to sales and expenses creation
2026-02-15 17:44:32 +08:00
Jp
fbc01bf1a4 feat(client): add client context to sales and expenses creation
- Disable branch selection in sale form when creating from client context
- Pass client ID to create pages via URL parameter
- Update breadcrumbs to reflect client navigation path
- Simplify relation managers by reusing resource tables and adding custom create actions
2026-02-15 17:43:33 +08:00
Jp
ff07f6f810 fix: remove debug statement from account options query
The dd() statement was accidentally left in production code, causing debug output to be displayed. This removes it to ensure proper functionality.
2026-02-14 14:14:33 +08:00
Jp
2bd8e99f64 fix: remove branch filter and add debug output for revenue accounts
The branch filter was incorrectly limiting available revenue accounts in certain scenarios. Temporarily added debug output to investigate account query results.
2026-02-14 14:13:24 +08:00
Jp
950e5613e6 Merge https://git.jpaleviado.site/kingjaypee12/MKM 2026-02-12 16:30:41 +08:00
Jp
fc118b8a6c Merge branch 'main' of https://github.com/kingjaypee12/MKM-App 2026-02-12 16:29:36 +08:00
Jp
a80e9a7b1c feat: add Laravel Horizon for queue monitoring and management
- Install Horizon package and configure with default settings
- Add HorizonServiceProvider with access gate for super_admin role only
- Integrate Horizon dashboard link into Filament admin panel navigation
- Update composer dependencies including Horizon and related package updates
2026-02-12 16:29:30 +08:00
d793abec9e Merge pull request #4 from kingjaypee12/fix/error-on-production
feat(TransmittalResource): enhance search functionality across relate…
2026-02-12 16:29:16 +08:00
fcac27b34d Merge pull request 'feat(TransmittalResource): enhance search functionality across related models' (#5) from fix/error-on-production into main
Reviewed-on: #5
2026-02-12 08:15:26 +00:00
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
22ea384d52 Merge pull request #3 from kingjaypee12/fix/error-on-production
Fix/error on production
2026-02-11 05:52:54 +08:00
7174bd6c90 Merge pull request 'feat: implement FilamentUser interface for User model' (#4) from fix/error-on-production into main
Reviewed-on: #4
2026-02-10 21:45:13 +00: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
4f9ec9ebfb Merge pull request 'fix: force HTTPS scheme in all environments' (#3) from fix/error-on-production into main
Reviewed-on: #3
2026-02-10 21:42:11 +00: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
f63be7fa5e Merge pull request 'fix: enforce HTTPS in production environment' (#2) from fix/error-on-production into main
Reviewed-on: #2
2026-02-10 21:38:32 +00: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
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
22 changed files with 714 additions and 93 deletions

View File

@@ -0,0 +1,29 @@
<?php
namespace App\DataObjects;
use Carbon\Carbon;
readonly class CreateSaleDTO
{
/**
* Create a new class instance.
*/
public function __construct(
protected string $reference_number,
protected Carbon $happened_on,
protected int $branch_id,
protected int $user_id,
)
{}
public function toArray(): array
{
return [
'reference_number' => $this->reference_number,
'happened_on' => $this->happened_on,
'branch_id' => $this->branch_id,
'user_id' => $this->user_id,
];
}
}

View File

@@ -24,31 +24,13 @@ class ExpensesRelationManager extends RelationManager
public function table(Table $table): Table public function table(Table $table): Table
{ {
return $table return ExpenseResource::table($table)->headerActions([
->recordTitleAttribute('supplier') Tables\Actions\Action::make('New Expense')->action('openCreateForm'),
->columns([
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()
->url(fn () => ExpenseResource::getUrl('create', ['client_id' => $this->getOwnerRecord()->id])),
])
->actions([
Tables\Actions\EditAction::make()
->url(fn (Expense $record) => ExpenseResource::getUrl('edit', ['record' => $record])),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]); ]);
} }
public function openCreateForm()
{
return redirect()->route('filament.admin.resources.expenses.create', ['client_id' => $this->getOwnerRecord()->id]);
}
} }

View File

@@ -24,36 +24,13 @@ class SalesRelationManager extends RelationManager
public function table(Table $table): Table public function table(Table $table): Table
{ {
return $table return SaleResource::table($table)->headerActions([
->recordTitleAttribute('title') Tables\Actions\Action::make('New Sale')->action('openCreateForm'),
->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()
->url(fn () => SaleResource::getUrl('create', ['client_id' => $this->getOwnerRecord()->id])),
])
->actions([
Tables\Actions\EditAction::make()
->url(fn (Sale $record) => SaleResource::getUrl('edit', ['record' => $record])),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]); ]);
} }
public function openCreateForm()
{
return redirect()->route('filament.admin.resources.sales.create', ['client_id' => $this->getOwnerRecord()->id]);
}
} }

View File

@@ -273,6 +273,7 @@ class ExpenseResource extends Resource
Tables\Columns\TextColumn::make('branch.client.company'), Tables\Columns\TextColumn::make('branch.client.company'),
Tables\Columns\TextColumn::make('branch.code'), Tables\Columns\TextColumn::make('branch.code'),
Tables\Columns\TextColumn::make('happened_on'), Tables\Columns\TextColumn::make('happened_on'),
Tables\Columns\TextColumn::make('accounts_list')->label('Accounts'),
]; ];
} }

View File

@@ -4,7 +4,9 @@ namespace App\Filament\Resources\ExpenseResource\Pages;
use App\Actions\Transactions\CreateTransactionAction; use App\Actions\Transactions\CreateTransactionAction;
use App\DataObjects\CreateTransactionDTO; use App\DataObjects\CreateTransactionDTO;
use App\Filament\Resources\ClientResource;
use App\Filament\Resources\ExpenseResource; use App\Filament\Resources\ExpenseResource;
use App\Models\Client;
use Exception; use Exception;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@@ -15,6 +17,39 @@ class CreateExpense extends CreateRecord
{ {
protected static string $resource = ExpenseResource::class; protected static string $resource = ExpenseResource::class;
public ?int $clientId = null;
public function mount(): void
{
parent::mount();
$this->clientId = request()->integer('client_id');
}
public function getBreadcrumbs(): array
{
$client = $this->getClient();
if (! $client) {
return parent::getBreadcrumbs();
}
return [
ClientResource::getUrl('view', ['record' => $client->id]) => $client->company,
ClientResource::getUrl('view', ['record' => $client->id]).'?activeRelationManager=4' => 'Expenses',
$this->getResource()::getUrl('create', ['client_id' => $client->id]) => 'Create',
];
}
protected function getClient(): Client|null
{
if (! $this->clientId) {
return null;
}
return Client::find($this->clientId);
}
protected function mutateFormDataBeforeCreate(array $data): array protected function mutateFormDataBeforeCreate(array $data): array
{ {
@@ -23,6 +58,16 @@ class CreateExpense extends CreateRecord
public function getFormDataMutation(array $data): array public function getFormDataMutation(array $data): array
{ {
$transactions = $data['transactions'] ?? [];
$data['gross_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['gross_amount'] ?? 0));
$data['exempt'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['exempt'] ?? 0));
$data['zero_rated'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['zero_rated'] ?? 0));
$data['vatable_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['vatable_amount'] ?? 0));
$data['input_tax'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['input_tax'] ?? 0));
$data['payable_withholding_tax'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['payable_withholding_tax'] ?? 0));
$data['net_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['net_amount'] ?? 0));
return Arr::except($data, ['client', 'transactions']); return Arr::except($data, ['client', 'transactions']);
} }
@@ -50,6 +95,15 @@ class CreateExpense extends CreateRecord
)->thenReturn(); )->thenReturn();
} }
$accountIds = collect($transactions)
->pluck('account_id')
->filter()
->unique()
->values()
->all();
$this->getRecord()->accounts()->sync($accountIds);
$this->commitDatabaseTransaction(); $this->commitDatabaseTransaction();
} catch (Exception $exception) { } catch (Exception $exception) {
$this->rollBackDatabaseTransaction(); $this->rollBackDatabaseTransaction();

View File

@@ -40,6 +40,7 @@ class SaleResource extends Resource
->afterStateUpdated(function ($set, $get) { ->afterStateUpdated(function ($set, $get) {
$set('branch_id', ''); $set('branch_id', '');
}) })
->disabled()
->required() ->required()
->live(), ->live(),
Select::make('branch_id') Select::make('branch_id')
@@ -53,7 +54,7 @@ class SaleResource extends Resource
->live(), ->live(),
TextInput::make('current_series') TextInput::make('current_series')
->label('Series') ->label('Series')
->disabled(), ->readOnly(),
DatePicker::make('happened_on')->label('Date') DatePicker::make('happened_on')->label('Date')
->required() ->required()
->afterStateUpdated(function ($set, $get) { ->afterStateUpdated(function ($set, $get) {
@@ -170,11 +171,11 @@ class SaleResource extends Resource
'client_id' => $get('../../client'), 'client_id' => $get('../../client'),
]); ]);
if ($get('../../branch_id')) { // if ($get('../../branch_id')) {
$query->whereHas('balances', function ($query) use ($get) { // $query->whereHas('balances', function ($query) use ($get) {
return $query->where('branch_id', $get('../../branch_id')); // return $query->where('branch_id', $get('../../branch_id'));
}); // });
} // }
$query->whereHas('accountType', function ($query) { $query->whereHas('accountType', function ($query) {
return $query->where('type', 'Revenue'); return $query->where('type', 'Revenue');
@@ -219,17 +220,10 @@ class SaleResource extends Resource
{ {
return $table return $table
->columns([ ->columns([
TextColumn::make('id')->label('ID')->sortable(), TextColumn::make('branch.code')->label('Branch')->sortable(),
TextColumn::make('client.name')->label('Client')->sortable(), TextColumn::make('reference_number')->label('Reference Number')->sortable(),
TextColumn::make('branch.name')->label('Branch')->sortable(),
TextColumn::make('happened_on')->label('Date')->date()->sortable(), TextColumn::make('happened_on')->label('Date')->date()->sortable(),
TextColumn::make('gross_amount')->label('Gross Amount')->numeric()->sortable(), TextColumn::make('user.name')->label('Created By')->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([ ->filters([
// //

View File

@@ -4,9 +4,12 @@ namespace App\Filament\Resources\SaleResource\Pages;
use App\Actions\Transactions\CreateTransactionAction; use App\Actions\Transactions\CreateTransactionAction;
use App\DataObjects\CreateTransactionDTO; use App\DataObjects\CreateTransactionDTO;
use App\Filament\Resources\ClientResource;
use App\Filament\Resources\SaleResource; use App\Filament\Resources\SaleResource;
use App\Models\Branch; use App\Models\Branch;
use App\Models\Client;
use App\Models\Sale; use App\Models\Sale;
use App\Services\Sales\SaleService;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@@ -17,6 +20,39 @@ class CreateSale extends CreateRecord
{ {
protected static string $resource = SaleResource::class; protected static string $resource = SaleResource::class;
public ?int $clientId = null;
public function mount(): void
{
parent::mount();
$this->clientId = request()->integer('client_id');
}
public function getBreadcrumbs(): array
{
$client = $this->getClient();
if (! $client) {
return parent::getBreadcrumbs();
}
return [
ClientResource::getUrl('view', ['record' => $client->id]) => $client->company,
ClientResource::getUrl('view', ['record' => $client->id]).'?activeRelationManager=3' => 'Sales',
$this->getResource()::getUrl('create', ['client_id' => $client->id]) => 'Create',
];
}
protected function getClient(): Client|null
{
if (! $this->clientId) {
return null;
}
return Client::find($this->clientId);
}
protected function mutateFormDataBeforeCreate(array $data): array protected function mutateFormDataBeforeCreate(array $data): array
{ {
return $this->getFormDataMutation($data); return $this->getFormDataMutation($data);
@@ -30,6 +66,16 @@ class CreateSale extends CreateRecord
public function getFormDataMutation(array $data): array public function getFormDataMutation(array $data): array
{ {
$transactions = $data['transactions'] ?? [];
$data['gross_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['gross_amount'] ?? 0));
$data['exempt'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['exempt'] ?? 0));
$data['vatable_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['vatable_amount'] ?? 0));
$data['output_tax'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['output_tax'] ?? 0));
$data['payable_withholding_tax'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['payable_withholding_tax'] ?? 0));
$data['discount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['discount'] ?? 0));
$data['net_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['net_amount'] ?? 0));
return Arr::except($data, ['client', 'transactions', 'with_discount']); return Arr::except($data, ['client', 'transactions', 'with_discount']);
} }
@@ -37,7 +83,7 @@ class CreateSale extends CreateRecord
{ {
try { try {
DB::beginTransaction(); DB::beginTransaction();
$record = Sale::create($data); $record = app(SaleService::class)->create($this->getFormDataMutation($data));
$branch = $record->branch; $branch = $record->branch;
foreach ($transactions as $transaction) { foreach ($transactions as $transaction) {
@@ -55,6 +101,16 @@ class CreateSale extends CreateRecord
] ]
)->thenReturn(); )->thenReturn();
} }
$accountIds = collect($transactions)
->pluck('account_id')
->filter()
->unique()
->values()
->all();
$record->accounts()->sync($accountIds);
DB::commit(); DB::commit();
} catch (\Exception $exception) { } catch (\Exception $exception) {
DB::rollBack(); DB::rollBack();

View File

@@ -36,6 +36,16 @@ class EditSale extends EditRecord
public function getFormDataMutation(array $data): array public function getFormDataMutation(array $data): array
{ {
$transactions = $data['transactions'] ?? [];
$data['gross_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['gross_amount'] ?? 0));
$data['exempt'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['exempt'] ?? 0));
$data['vatable_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['vatable_amount'] ?? 0));
$data['output_tax'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['output_tax'] ?? 0));
$data['payable_withholding_tax'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['payable_withholding_tax'] ?? 0));
$data['discount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['discount'] ?? 0));
$data['net_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['net_amount'] ?? 0));
return Arr::except($data, ['client', 'transactions', 'with_discount']); return Arr::except($data, ['client', 'transactions', 'with_discount']);
} }
@@ -71,6 +81,16 @@ class EditSale extends EditRecord
] ]
)->thenReturn(); )->thenReturn();
} }
$accountIds = collect($transactions)
->pluck('account_id')
->filter()
->unique()
->values()
->all();
$record->accounts()->sync($accountIds);
DB::commit(); DB::commit();
} catch (\Exception $exception) { } catch (\Exception $exception) {
DB::rollBack(); DB::rollBack();

View File

@@ -45,16 +45,36 @@ class TransmittalResource extends Resource
->columns([ ->columns([
Tables\Columns\Layout\Split::make([ Tables\Columns\Layout\Split::make([
Tables\Columns\TextColumn::make('series') 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') ->label('Series')
->weight(FontWeight::Bold) ->weight(FontWeight::Bold)
->columnSpan(2), ->columnSpan(2),
Tables\Columns\Layout\Stack::make([ Tables\Columns\Layout\Stack::make([
Tables\Columns\TextColumn::make('client.company') 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'), ->weight(FontWeight::SemiBold)->label('Client'),
Tables\Columns\TextColumn::make('branch.code') 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'), ->label('Branch'),
]), ]),
]), ]),

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphMany;
class Expense extends Model class Expense extends Model
@@ -43,4 +44,14 @@ class Expense extends Model
{ {
return $this->belongsTo(Branch::class); return $this->belongsTo(Branch::class);
} }
public function accounts(): BelongsToMany
{
return $this->belongsToMany(Account::class, 'account_expense')->withTimestamps();
}
public function getAccountsListAttribute(): string
{
return $this->accounts->pluck('account')->implode(', ');
}
} }

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphMany;
class Sale extends Model class Sale extends Model
@@ -43,4 +44,19 @@ class Sale extends Model
{ {
return $this->belongsTo(Branch::class); return $this->belongsTo(Branch::class);
} }
public function accounts(): BelongsToMany
{
return $this->belongsToMany(Account::class, 'account_sale')->withTimestamps();
}
public function getAccountsListAttribute(): string
{
return $this->accounts->pluck('account')->implode(', ');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
} }

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
namespace App\Providers\Filament; namespace App\Providers\Filament;
use BezhanSalleh\FilamentShield\FilamentShieldPlugin; use BezhanSalleh\FilamentShield\FilamentShieldPlugin;
use Filament\Navigation\NavigationItem;
use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Http\Middleware\DispatchServingFilamentEvent;
@@ -39,6 +40,13 @@ class AdminPanelProvider extends PanelProvider
->pages([ ->pages([
Pages\Dashboard::class, Pages\Dashboard::class,
]) ])
->navigationItems([
NavigationItem::make('Horizon')
->url(fn (): string => url(config('horizon.path')))
->icon('heroicon-o-queue-list')
->group('System')
->visible(fn (): bool => auth()->user()?->hasRole('super_admin') ?? false),
])
->databaseNotifications() ->databaseNotifications()
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([ ->widgets([

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Laravel\Horizon\Horizon;
use Laravel\Horizon\HorizonApplicationServiceProvider;
class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
parent::boot();
// Horizon::routeSmsNotificationsTo('15556667777');
// Horizon::routeMailNotificationsTo('example@example.com');
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
}
/**
* Register the Horizon gate.
*
* This gate determines who can access Horizon in non-local environments.
*/
protected function gate(): void
{
Gate::define('viewHorizon', function ($user) {
return $user->hasRole('super_admin');
});
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Services\Sales;
use App\DataObjects\CreateSaleDTO;
use App\Models\Sale;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
class SaleService
{
/**
* Create a new class instance.
*/
public function __construct()
{
//
}
public function create(array $data): Sale
{
$tData = new CreateSaleDTO(
reference_number: $data['current_series'],
happened_on: Carbon::parse($data['happened_on']),
branch_id: $data['branch_id'],
user_id: Auth::user()->id,
);
return Sale::create($tData->toArray());
}
}

View File

@@ -13,6 +13,7 @@
"bezhansalleh/filament-shield": "^3.2", "bezhansalleh/filament-shield": "^3.2",
"filament/filament": "^3.2", "filament/filament": "^3.2",
"laravel/framework": "^11.9", "laravel/framework": "^11.9",
"laravel/horizon": "^5.44",
"laravel/tinker": "^2.9", "laravel/tinker": "^2.9",
"livewire/livewire": "^3.4", "livewire/livewire": "^3.4",
"livewire/volt": "^1.0", "livewire/volt": "^1.0",

109
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "68d9041cb27c73080bced01f909a4567", "content-hash": "60653f82a7ab603cb6e06b89358e7eec",
"packages": [ "packages": [
{ {
"name": "anourvalar/eloquent-serialize", "name": "anourvalar/eloquent-serialize",
@@ -386,16 +386,16 @@
}, },
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.14.7", "version": "0.14.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/brick/math.git", "url": "https://github.com/brick/math.git",
"reference": "07ff363b16ef8aca9692bba3be9e73fe63f34e50" "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/07ff363b16ef8aca9692bba3be9e73fe63f34e50", "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629",
"reference": "07ff363b16ef8aca9692bba3be9e73fe63f34e50", "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -434,7 +434,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/brick/math/issues", "issues": "https://github.com/brick/math/issues",
"source": "https://github.com/brick/math/tree/0.14.7" "source": "https://github.com/brick/math/tree/0.14.8"
}, },
"funding": [ "funding": [
{ {
@@ -442,7 +442,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2026-02-07T10:57:35+00:00" "time": "2026-02-10T14:33:43+00:00"
}, },
{ {
"name": "carbonphp/carbon-doctrine-types", "name": "carbonphp/carbon-doctrine-types",
@@ -2617,17 +2617,96 @@
"time": "2026-01-20T15:26:20+00:00" "time": "2026-01-20T15:26:20+00:00"
}, },
{ {
"name": "laravel/prompts", "name": "laravel/horizon",
"version": "v0.3.12", "version": "v5.44.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/prompts.git", "url": "https://github.com/laravel/horizon.git",
"reference": "4861ded9003b7f8a158176a0b7666f74ee761be8" "reference": "00c21e4e768112cce3f4fe576d75956dfc423de2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/prompts/zipball/4861ded9003b7f8a158176a0b7666f74ee761be8", "url": "https://api.github.com/repos/laravel/horizon/zipball/00c21e4e768112cce3f4fe576d75956dfc423de2",
"reference": "4861ded9003b7f8a158176a0b7666f74ee761be8", "reference": "00c21e4e768112cce3f4fe576d75956dfc423de2",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-pcntl": "*",
"ext-posix": "*",
"illuminate/contracts": "^9.21|^10.0|^11.0|^12.0|^13.0",
"illuminate/queue": "^9.21|^10.0|^11.0|^12.0|^13.0",
"illuminate/support": "^9.21|^10.0|^11.0|^12.0|^13.0",
"nesbot/carbon": "^2.17|^3.0",
"php": "^8.0",
"ramsey/uuid": "^4.0",
"symfony/console": "^6.0|^7.0|^8.0",
"symfony/error-handler": "^6.0|^7.0|^8.0",
"symfony/polyfill-php83": "^1.28",
"symfony/process": "^6.0|^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"orchestra/testbench": "^7.55|^8.36|^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.10|^2.0",
"predis/predis": "^1.1|^2.0|^3.0"
},
"suggest": {
"ext-redis": "Required to use the Redis PHP driver.",
"predis/predis": "Required when not using the Redis PHP driver (^1.1|^2.0|^3.0)."
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Horizon": "Laravel\\Horizon\\Horizon"
},
"providers": [
"Laravel\\Horizon\\HorizonServiceProvider"
]
},
"branch-alias": {
"dev-master": "6.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\Horizon\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Dashboard and code-driven configuration for Laravel queues.",
"keywords": [
"laravel",
"queue"
],
"support": {
"issues": "https://github.com/laravel/horizon/issues",
"source": "https://github.com/laravel/horizon/tree/v5.44.0"
},
"time": "2026-02-10T18:18:08+00:00"
},
{
"name": "laravel/prompts",
"version": "v0.3.13",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
"reference": "ed8c466571b37e977532fb2fd3c272c784d7050d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/prompts/zipball/ed8c466571b37e977532fb2fd3c272c784d7050d",
"reference": "ed8c466571b37e977532fb2fd3c272c784d7050d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2671,9 +2750,9 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.", "description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": { "support": {
"issues": "https://github.com/laravel/prompts/issues", "issues": "https://github.com/laravel/prompts/issues",
"source": "https://github.com/laravel/prompts/tree/v0.3.12" "source": "https://github.com/laravel/prompts/tree/v0.3.13"
}, },
"time": "2026-02-03T06:57:26+00:00" "time": "2026-02-06T12:17:10+00:00"
}, },
{ {
"name": "laravel/serializable-closure", "name": "laravel/serializable-closure",

178
config/horizon.php Normal file
View File

@@ -0,0 +1,178 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Horizon Domain
|--------------------------------------------------------------------------
|
| This value is the prefix that will be used by Horizon to generate its
| routes. You are free to change this value to anything you like,
| such as "admin/horizon" or anything else you'd like it to be.
|
*/
'domain' => env('HORIZON_DOMAIN'),
/*
|--------------------------------------------------------------------------
| Horizon Path
|--------------------------------------------------------------------------
|
| This is the URI path where Horizon will be accessible from. Feel free
| to change this path to anything you like. Note that the path will be
| applied after the "domain" value defined above.
|
*/
'path' => env('HORIZON_PATH', 'horizon'),
/*
|--------------------------------------------------------------------------
| Horizon Redis Connection
|--------------------------------------------------------------------------
|
| This is the name of the Redis connection where Horizon will store the
| information about your jobs. This connection should be defined in
| your "database" configuration file.
|
*/
'use' => 'default',
/*
|--------------------------------------------------------------------------
| Horizon Redis Prefix
|--------------------------------------------------------------------------
|
| This prefix will be used when storing all Horizon data in Redis. You
| may modify the prefix if you are running multiple Horizon instances
| on the same server to avoid any collision between instances.
|
*/
'prefix' => env(
'HORIZON_PREFIX',
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'
),
/*
|--------------------------------------------------------------------------
| Horizon Route Middleware
|--------------------------------------------------------------------------
|
| These middleware will get attached onto each Horizon route, giving you
| the chance to add your own middleware to this list or change any of
| the existing middleware. Or, you can simply stick with this list.
|
*/
'middleware' => ['web', 'auth'],
/*
|--------------------------------------------------------------------------
| Queue Wait Time Thresholds
|--------------------------------------------------------------------------
|
| This option allows you to configure when the queue "wait time" is
| considered "long", helping you identify which queues are being
| backed up and need more workers to handle the incoming load.
|
*/
'waits' => [
'redis:default' => 60,
],
/*
|--------------------------------------------------------------------------
| Job Trimming Times
|--------------------------------------------------------------------------
|
| These options determine how many minutes Horizon will keep different
| types of jobs in its database. This helps keep your database small
| and fast, while still giving you some history for debugging.
|
*/
'trim' => [
'recent' => 60,
'recent_failed' => 10080,
'failed' => 10080,
'monitored' => 10080,
],
/*
|--------------------------------------------------------------------------
| Metrics Configuration
|--------------------------------------------------------------------------
|
| Here you may configure how many minutes Horizon will keep metrics.
| These metrics include things like throughput and average wait time
| for each of your queues, giving you insight into your performance.
|
*/
'metrics' => [
'trim_snapshots' => [
'job' => 24,
'queue' => 24,
],
],
/*
|--------------------------------------------------------------------------
| Fast Termination
|--------------------------------------------------------------------------
|
| When this option is enabled, Horizon's "terminate" command will not
| wait for all jobs to finish before terminating the workers. This
| can be useful when you need to quickly restart your workers.
|
*/
'fast_termination' => false,
/*
|--------------------------------------------------------------------------
| Memory Limit (MB)
|--------------------------------------------------------------------------
|
| This value describes the maximum amount of memory the Horizon master
| process may consume before it is terminated and restarted. This
| helps prevent any memory leaks from affecting the entire server.
|
*/
'memory_limit' => 64,
/*
|--------------------------------------------------------------------------
| Queue Worker Configuration
|--------------------------------------------------------------------------
|
| Here you may define the queue worker settings used by your application
| in all environments. These settings determine how many workers will
| be used for each environment's respective queues.
|
*/
'environments' => [
'production' => [
'supervisor-1' => [
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
],
'local' => [
'supervisor-1' => [
'maxProcesses' => 3,
],
],
],
];

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');
});
}
};

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
{
public function up(): void
{
Schema::create('account_sale', function (Blueprint $table) {
$table->id();
$table->foreignId('sale_id')->constrained()->onDelete('cascade');
$table->foreignId('account_id')->constrained()->onDelete('cascade');
$table->timestamps();
$table->unique(['sale_id', 'account_id']);
});
Schema::create('account_expense', function (Blueprint $table) {
$table->id();
$table->foreignId('expense_id')->constrained()->onDelete('cascade');
$table->foreignId('account_id')->constrained()->onDelete('cascade');
$table->timestamps();
$table->unique(['expense_id', 'account_id']);
});
}
public function down(): void
{
Schema::dropIfExists('account_expense');
Schema::dropIfExists('account_sale');
}
};