25 Commits

Author SHA1 Message Date
1d6238e9cb Merge pull request #9 from kingjaypee12/jp/updates-on-sales
Jp/updates on sales
2026-02-18 22:58:34 +08:00
Jp
d8077f200a feat: replace Excel export with PDF export for transmittals
- Add new TransmittalPDFExportJob to generate PDFs using dompdf
- Remove old Excel export implementation (TransmittalsExport)
- Update ExportCompleteJob to use new PDF job instead of Excel
- Add TestQueueJob for queue testing with new route
- Update notification label from "Download File" to "Download PDF File"
- Fix auth() helper usage by importing Auth facade consistently
2026-02-18 22:57:34 +08:00
Jp
7899ed75ea feat(sales): add discount support with ledger accounting
- Add discount_type column to transactions table via migration
- Update Sale model to use fillable instead of guarded for better security
- Implement discount account ledger creation when discount is applied
- Fix net amount calculation to include discount in CreateSaleAction
- Remove unused "Exempt" column from sale transaction table
- Make discount_type required when discount is enabled in form
- Update form data mutation to properly handle discount calculations
2026-02-18 21:40:39 +08:00
Jp
5d427cdea4 feat: add discount management and PDF export for transmittals
- Create Discount model, migration, and Filament resource with relation to Client
- Add PDF export functionality for transmittals using DomPDF
- Include discount type selection in sales transactions
- Fix account filtering logic in expense resource
- Update export job to generate PDF instead of Excel
2026-02-18 01:42:44 +08:00
2644be0505 Merge pull request #8 from kingjaypee12/jp/sales-clean-up
feat(SaleResource): redirect to client after sale creation
2026-02-16 02:08:01 +08:00
Jp
9ddb71f03d feat(SaleResource): redirect to client after sale creation
When a client is associated with the sale, redirect to the client's view page with the sales relation manager active instead of the default list page. This improves the user workflow by keeping the context focused on the client.
2026-02-16 02:06:59 +08:00
eadcc0b3d7 Merge pull request #7 from kingjaypee12/jp/sales-clean-up
Jp/sales clean up
2026-02-16 02:03:02 +08:00
Jp
8c6fa6cb08 refactor(client): extract base account generation to command
Move the logic for generating base accounts for a new client from the ClientObserver into a dedicated command class (GenerateBaseAccountCommand). This improves code organization and reusability.

- The command is now used in the ClientObserver::created method.
- The command is also made available as a manual action in the AccountsRelationManager table header, allowing admins to generate base accounts for existing clients that lack them.
- Added necessary imports to the CreateSale page, though the command is not directly used there in this diff, suggesting preparatory work for future integration.
2026-02-16 02:02:26 +08:00
Jp
7bcfaff311 refactor: extract transaction creation and account syncing to actions
- Introduce CreateRecordTransactionsAction to handle transaction creation for any model
- Introduce SyncAccountsAction to encapsulate account synchronization logic
- Refactor CreateSaleAction to use new actions and handle full sale creation flow
- Simplify CreateExpense and CreateSale pages by delegating to actions
- Ensure proper transaction handling with database rollback on failure
2026-02-16 01:48:25 +08:00
Jp
e04689acca refactor(sales): replace service with command and action pattern
- Replace SaleService with CreateSaleCommand and CreateSaleAction for better separation of concerns
- Move sale creation logic into dedicated command class following command pattern
- Update CreateSale.php to use new action instead of direct service call
- Wrap sale creation in database transaction for data consistency
2026-02-16 01:22:00 +08:00
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
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
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
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
42 changed files with 1674 additions and 468 deletions

11
.devdbrc Normal file
View File

@@ -0,0 +1,11 @@
[
{
"name": "My test MySQL database",
"type": "mysql",
"host": "192.168.100.105",
"port": "3306",
"username": "root",
"password": "root",
"database": "mkm_admin"
}
]

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Actions\Sales;
use App\Actions\Transactions\CreateRecordTransactionsAction;
use App\Commands\Sales\CreateSaleCommand;
use App\Commands\Series\CreateSeriesCommand;
use App\Models\Sale;
use Illuminate\Support\Facades\DB;
class CreateSaleAction
{
/**
* Create a new class instance.
*/
public function __construct(
private CreateSaleCommand $createSaleCommand,
private CreateSeriesCommand $createSeriesCommand,
){ }
public function __invoke(array $data, array $transactions): Sale
{
$record = $this->createSaleCommand->execute($data);
try {
DB::beginTransaction();
//create transactions for the sale
app(CreateRecordTransactionsAction::class)($record, $transactions);
$accountIds = collect($transactions)
->pluck('account_id')
->filter()
->unique()
->values()
->all();
//sync accounts to sale
app(SyncAccountsAction::class)($record, $accountIds);
//increment current_series
$this->createSeriesCommand->execute([
'branch_id' => $record->branch_id,
'series' => $record->reference_number,
]);
DB::commit();
} catch (\Exception $exception) {
DB::rollBack();
throw new \Exception('Failed to save transactions : '.$exception->getMessage());
}
return $record;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Actions\Sales;
use App\Models\Sale;
use Illuminate\Support\Facades\DB;
class SyncAccountsAction
{
public function __invoke(Sale $sale, array $accounts): void
{
DB::transaction(function () use ($sale, $accounts) {
$sale->accounts()->sync($accounts);
}, attempts: 2);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Actions\Transactions;
use App\DataObjects\CreateTransactionDTO;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Pipeline;
class CreateRecordTransactionsAction
{
public function __invoke(Model $record, array $transactions): void
{
foreach ($transactions as $transaction) {
$tData = [
'branch_id' => $record->branch_id,
'happened_on' => $record->happened_on,
...$transaction,
];
$payload = new CreateTransactionDTO(data: $tData, transactionable: $record);
Pipeline::send(passable: $payload)->through(
[
CreateTransactionAction::class,
]
)->thenReturn();
}
}
}

View File

@@ -24,6 +24,10 @@ class CreateTransactionAction extends BaseAction
$this->cashAccountLedger($payload);
if ($payload->transaction->discount !== 0) {
$this->discountAccountLedger($payload);
}
return $next($payload);
}
@@ -33,11 +37,13 @@ class CreateTransactionAction extends BaseAction
$isExpense = $payload->transactionable instanceof \App\Models\Expense;
$type = $isExpense ? 'debit' : 'credit';
$discount = $payload->transaction->discount ?? 0.00;
if ($branch->isClientVatable) {
//create transaction account ledger
$ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id,
amount: $payload->transaction->net_amount ?? 0.00,
amount: $payload->transaction->net_amount + $discount ?? 0.00,
transaction: $payload->transaction,
account: $payload->transaction->account,
type: $type,
@@ -144,4 +150,28 @@ class CreateTransactionAction extends BaseAction
$this->ledgerPipe($ledgerPayload);
}
}
public function discountAccountLedger($payload): void
{
$isExpense = $payload->transactionable instanceof \App\Models\Expense;
$type = $isExpense ? 'credit' : 'debit';
$amount = $payload->transaction->discount ?? 0.00;
$clientId = $payload->transactionable->branch->client_id;
$discountAccount = Account::query()
->where('account', 'Sales Discount')
->where('client_id', $clientId)
->first();
if ($discountAccount && $amount > 0) {
$ledgerPayload = new CreateLedgerDTO(
branch_id: $payload->transactionable->branch_id,
amount: $amount,
transaction: $payload->transaction,
account: $discountAccount,
type: $type,
);
$this->ledgerPipe($ledgerPayload);
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Commands\Clients;
use App\Commands\Command;
use Illuminate\Support\Facades\DB;
class GenerateBaseAccountCommand
{
/**
* Create a new class instance.
*/
public function __construct()
{
//
}
/**
* Execute the command.
*/
public function execute($client): void
{
DB::transaction(function () use ($client) {
$client->accounts()->createMany([
[
'account_type_id' => 1,
'account' => 'Cash',
'normal_balance' => 'debit',
],
[
'account_type_id' => 1,
'account' => 'Input Tax',
'normal_balance' => 'debit',
],
[
'account_type_id' => 1,
'account' => 'Creditable Withholding Tax',
'normal_balance' => 'debit',
],
[
'account_type_id' => 2,
'account' => 'Output Tax',
'normal_balance' => 'credit',
],
[
'account_type_id' => 2,
'account' => 'Payable Withholding Tax',
'normal_balance' => 'credit',
],
[
'account_type_id' => 5,
'account' => 'Vat Exempt Revenue',
'normal_balance' => 'credit',
],
[
'account_type_id' => 4,
'account' => 'Sales Discount',
'normal_balance' => 'debit',
],
]);
});
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Commands\Sales;
use App\Commands\Command;
use App\DataObjects\CreateSaleDTO;
use App\Models\Sale;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class CreateSaleCommand implements Command
{
public function execute(array $data): Sale
{
return DB::transaction(function () use ($data, &$sale) {
$tData = new CreateSaleDTO(
reference_number: $data['current_series'],
happened_on: \Carbon\Carbon::parse($data['happened_on']),
branch_id: $data['branch_id'],
user_id: Auth::user()->id,
);
return Sale::create($tData->toArray());
});
}
}

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

@@ -1,144 +0,0 @@
<?php
namespace App\Exports;
use App\Models\Transmittal;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\View;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithDefaultStyles;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Style;
class TransmittalsExport implements FromCollection, ShouldAutoSize, ShouldQueue, WithDefaultStyles, WithHeadings, WithMapping
{
use Exportable, Queueable, SerializesModels;
public function __construct(
private readonly array $id
) {}
public function view(): \Illuminate\Contracts\View\View
{
$transmittals = Transmittal::query()->with(['client', 'branch', 'files.notes', 'files.remarks'])->whereIn('id', Arr::flatten($this->id))->get();
return View::make('transmittal.export.transmittal-export-table')->with(['transmittals' => $transmittals]);
}
/**
* @throws Exception
*/
public function defaultStyles(Style $defaultStyle)
{
return $defaultStyle->applyFromArray([
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
'vertical' => Alignment::VERTICAL_CENTER,
],
]);
}
public function headings(): array
{
return [
'series',
'files',
'notes',
'remarks',
];
}
public function collection()
{
$transmittals = Transmittal::query()->with(['client', 'branch', 'files.notes', 'files.remarks'])
->withCount(['files', 'notes', 'remarks'])->with(['files' => function ($files) {
$files->withCount(['notes', 'remarks']);
}])
->whereIn('id', Arr::flatten($this->id))->get();
return $transmittals;
}
public function map($transmittal): array
{
$data = [];
$firstFile = $transmittal->files->first();
$data[] = [
$transmittal->series,
$firstFile?->description,
$firstFile->notes->first()?->comment,
$firstFile->remarks->first()?->remark,
];
//iterate comments and remarks for first file
$notes = $firstFile->notes->pluck('comment');
$remarks = $firstFile->remarks->pluck('remark');
$fileNoteCount = count($notes);
$fileRemarkCount = count($remarks);
$fileRowCount = $fileNoteCount;
if ($fileRemarkCount > $fileNoteCount) {
$fileRowCount = $fileRemarkCount;
}
for ($i = 1; $i < $fileRowCount; $i++) {
$data[] = [
'',
'',
$notes[$i] ?? '',
$remarks[$i] ?? '',
];
}
//file iteration except for first file
$fileRowCounter = 0;
foreach ($transmittal->files as $file) {
$notes = $file->notes->pluck('comment');
$remarks = $file->remarks->pluck('remark');
$fileNoteCount = count($notes);
$fileRemarkCount = count($remarks);
$fileRowCount = $fileNoteCount;
if ($fileRemarkCount > $fileNoteCount) {
$fileRowCount = $fileRemarkCount;
}
if ($fileRowCounter != 0) {
$data[] = [
'',
$file->description,
$file->notes->first()?->comment ?? '',
$file->remarks->first()?->remark ?? '',
];
//iterate for remaining notes and remarks
for ($i = 1; $i < $fileRowCount; $i++) {
$data[] = [
'',
'',
$notes[$i] ?? '',
$remarks[$i] ?? '',
];
}
}
$fileRowCounter++;
}
return $data;
}
}

View File

@@ -10,6 +10,7 @@ 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\DiscountRelationManager;
use App\Filament\Resources\ClientResource\RelationManagers\ExpensesRelationManager;
use App\Filament\Resources\ClientResource\RelationManagers\JournalsRelationManager;
use App\Filament\Resources\ClientResource\RelationManagers\SalesRelationManager;
@@ -109,6 +110,7 @@ class ClientResource extends Resource
SalesRelationManager::class,
ExpensesRelationManager::class,
JournalsRelationManager::class,
DiscountRelationManager::class,
];
}

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Resources\ClientResource\RelationManagers;
use App\Commands\Clients\GenerateBaseAccountCommand;
use App\Filament\Exports\ClientAccountsExporter;
use App\Models\Account;
use Filament\Actions\Exports\Enums\ExportFormat;
@@ -43,6 +44,20 @@ class AccountsRelationManager extends RelationManager
//
])
->headerActions([
Tables\Actions\Action::make('generate-base-accounts')
->requiresConfirmation()
->label('Generate Base Accounts')
->action(function () {
$client = $this->getOwnerRecord();
if (! $client ) {
return;
}
if($client->accounts()->count() > 0) {
return;
}
app(GenerateBaseAccountCommand::class)->execute($client);
}),
Tables\Actions\ExportAction::make('Export Accounts')->exporter(ClientAccountsExporter::class)->formats([ExportFormat::Csv]),
Tables\Actions\CreateAction::make()->label('New Account')->icon('heroicon-o-plus')->slideOver(),
])

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Filament\Resources\ClientResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class DiscountRelationManager extends RelationManager
{
protected static string $relationship = 'discounts';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('discount')
->required()
->maxLength(255),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('discount')
->columns([
Tables\Columns\TextColumn::make('discount'),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -24,31 +24,13 @@ class ExpensesRelationManager extends RelationManager
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('supplier')
->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(),
]),
return ExpenseResource::table($table)->headerActions([
Tables\Actions\Action::make('New Expense')->action('openCreateForm'),
]);
}
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
{
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()
->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(),
]),
return SaleResource::table($table)->headerActions([
Tables\Actions\Action::make('New Sale')->action('openCreateForm'),
]);
}
public function openCreateForm()
{
return redirect()->route('filament.admin.resources.sales.create', ['client_id' => $this->getOwnerRecord()->id]);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\DiscountResource\Pages;
use App\Filament\Resources\DiscountResource\RelationManagers;
use App\Models\Discount;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class DiscountResource extends Resource
{
protected static ?string $model = Discount::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static bool $shouldRegisterNavigation = false;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('discount')
->label('Discount')
->required(),
Forms\Components\Hidden::make('client_id')
->default(fn () => request()->client_id),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('discount')
->label('Discount')
->searchable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListDiscounts::route('/'),
'create' => Pages\CreateDiscount::route('/create'),
'edit' => Pages\EditDiscount::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Filament\Resources\DiscountResource\Pages;
use App\Filament\Resources\DiscountResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateDiscount extends CreateRecord
{
protected static string $resource = DiscountResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DiscountResource\Pages;
use App\Filament\Resources\DiscountResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditDiscount extends EditRecord
{
protected static string $resource = DiscountResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DiscountResource\Pages;
use App\Filament\Resources\DiscountResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListDiscounts extends ListRecords
{
protected static string $resource = DiscountResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -195,11 +195,11 @@ class ExpenseResource extends Resource
'client_id' => $get('../../client'),
]);
if ($get('../../branch_id')) {
$query->whereHas('balances', function ($query) use ($get) {
return $query->where('branch_id', $get('../../branch_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');
@@ -208,7 +208,6 @@ class ExpenseResource extends Resource
return $query->get()->pluck('account', 'id');
}
#[NoReturn]
public static function setDefaultFormValues(Get $get, Set $set, ?string $old, ?string $state): void
{
@@ -273,6 +272,7 @@ class ExpenseResource extends Resource
Tables\Columns\TextColumn::make('branch.client.company'),
Tables\Columns\TextColumn::make('branch.code'),
Tables\Columns\TextColumn::make('happened_on'),
Tables\Columns\TextColumn::make('accounts_list')->label('Accounts'),
];
}

View File

@@ -2,19 +2,52 @@
namespace App\Filament\Resources\ExpenseResource\Pages;
use App\Actions\Transactions\CreateTransactionAction;
use App\DataObjects\CreateTransactionDTO;
use App\Actions\Transactions\CreateRecordTransactionsAction;
use App\Filament\Resources\ClientResource;
use App\Filament\Resources\ExpenseResource;
use App\Models\Client;
use Exception;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Pipeline;
use Symfony\Component\Console\Exception\LogicException;
class CreateExpense extends CreateRecord
{
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
{
@@ -23,6 +56,16 @@ class CreateExpense extends CreateRecord
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']);
}
@@ -31,24 +74,16 @@ class CreateExpense extends CreateRecord
$transactions = $this->form->getState()['transactions'] ?? [];
try {
$branch = $this->getRecord()->branch;
app(CreateRecordTransactionsAction::class)($this->getRecord(), $transactions);
foreach ($transactions as $transaction) {
$accountIds = collect($transactions)
->pluck('account_id')
->filter()
->unique()
->values()
->all();
$data = [
'branch_id' => $branch->id,
'happened_on' => $this->getRecord()->happened_on,
...$transaction,
];
$payload = new CreateTransactionDTO(data: $data, transactionable: $this->getRecord());
Pipeline::send(passable: $payload)->through(
[
CreateTransactionAction::class,
]
)->thenReturn();
}
$this->getRecord()->accounts()->sync($accountIds);
$this->commitDatabaseTransaction();
} catch (Exception $exception) {

View File

@@ -6,6 +6,7 @@ use App\Filament\Resources\SaleResource\Pages;
use App\Models\Account;
use App\Models\Branch;
use App\Models\Client;
use App\Models\Discount;
use App\Models\Sale;
use Awcodes\TableRepeater\Components\TableRepeater;
use Awcodes\TableRepeater\Header;
@@ -30,12 +31,13 @@ class SaleResource extends Resource
protected static bool $shouldRegisterNavigation = false;
public static function form(Form $form): Form
{
return $form
->schema([
Select::make('client')
->default(request()->query('client_id'))
->default(fn () => request()->integer('client_id'))
->options(Client::query()->get()->pluck('company', 'id'))
->afterStateUpdated(function ($set, $get) {
$set('branch_id', '');
@@ -53,7 +55,7 @@ class SaleResource extends Resource
->live(),
TextInput::make('current_series')
->label('Series')
->disabled(),
->readOnly(),
DatePicker::make('happened_on')->label('Date')
->required()
->afterStateUpdated(function ($set, $get) {
@@ -71,6 +73,8 @@ class SaleResource extends Resource
]);
}
public static function getSeries(Get $get): string
{
$branch = Branch::find($get('branch_id'));
@@ -92,11 +96,12 @@ class SaleResource extends Resource
Header::make('Charge Account'),
Header::make('Description'),
Header::make('Gross Amount'),
Header::make('Exempt'),
// Header::make('Exempt'),
Header::make('Vatable Amount'),
Header::make('Output Tax'),
Header::make('Withholding Tax'),
Header::make('Discount'),
Header::make('Discount Type'),
Header::make('Net Amount'),
];
}
@@ -105,7 +110,7 @@ class SaleResource extends Resource
Header::make('Charge Account'),
Header::make('Description'),
Header::make('Gross Amount'),
Header::make('Exempt'),
// Header::make('Exempt'),
Header::make('Vatable Amount'),
Header::make('Output Tax'),
Header::make('Withholding Tax'),
@@ -128,6 +133,7 @@ class SaleResource extends Resource
TextInput::make('exempt')
->numeric()
->live()
->hidden()
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
static::setDefaultFormValues($get, $set, $old, $state);
})->default(0),
@@ -135,7 +141,7 @@ class SaleResource extends Resource
->numeric()
->nullable()
->live()
->readOnly()
->readOnly(fn (Get $get) => $get('exempt') == 0)
->default(0),
Hidden::make('happened_on')->default(fn (Get $get) => $get('../../happened_on')),
Hidden::make('with_discount')->default(fn (Get $get) => $get('../../with_discount')),
@@ -150,14 +156,17 @@ class SaleResource extends Resource
->numeric()
->live()
->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
static::setDefaultFormValues($get, $set, $old, $state);
})->default(0),
TextInput::make('discount')
->numeric()
->readOnly()
// ->readOnly()
->visible(fn (Get $get) => $get('../../with_discount'))
->live(),
Select::make('discount_type')
->options(fn (Get $get) => static::getDiscountOptions($get))
->required(fn (Get $get) => $get('../../with_discount'))
->visible(fn (Get $get) => $get('../../with_discount')),
TextInput::make('net_amount')->numeric()->default(0),
];
}
@@ -170,11 +179,11 @@ class SaleResource extends Resource
'client_id' => $get('../../client'),
]);
if ($get('../../branch_id')) {
$query->whereHas('balances', function ($query) use ($get) {
return $query->where('branch_id', $get('../../branch_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');
@@ -183,6 +192,13 @@ class SaleResource extends Resource
return $query->get()->pluck('account', 'id');
}
private static function getDiscountOptions(Get $get)
{
$query = Discount::query()->where('client_id', $get('../../client'));
return $query->pluck('discount', 'id');
}
private static function setDefaultFormValues(Get $get, Set $set, ?string $old, ?string $state)
{
$exempt = (float) $get('exempt');
@@ -210,7 +226,7 @@ class SaleResource extends Resource
}
$set('output_tax', number_format($outputTax, 2, '.', ''));
$set('discount', number_format($discount, 2, '.', ''));
// $set('discount', number_format($discount, 2, '.', ''));
$set('vatable_amount', number_format($vatableAmount, 2, '.', ''));
$set('net_amount', number_format($netAmount, 2, '.', ''));
}
@@ -219,17 +235,10 @@ 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('branch.code')->label('Branch')->sortable(),
TextColumn::make('reference_number')->label('Reference Number')->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(),
TextColumn::make('user.name')->label('Created By')->sortable(),
])
->filters([
//

View File

@@ -2,21 +2,60 @@
namespace App\Filament\Resources\SaleResource\Pages;
use App\Actions\Transactions\CreateTransactionAction;
use App\DataObjects\CreateTransactionDTO;
use App\Actions\Sales\CreateSaleAction;
use App\Actions\Sales\SyncAccountsAction;
use App\Actions\Transactions\CreateRecordTransactionsAction;
use App\Commands\Clients\GenerateBaseAccountCommand;
use App\Filament\Resources\ClientResource;
use App\Filament\Resources\SaleResource;
use App\Models\Branch;
use App\Models\Client;
use App\Models\Sale;
use App\Services\Sales\SaleService;
use Filament\Actions\Action;
use Filament\Forms\Components\Actions;
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
{
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
{
return $this->getFormDataMutation($data);
@@ -30,42 +69,33 @@ class CreateSale extends CreateRecord
public function getFormDataMutation(array $data): array
{
return Arr::except($data, ['client', 'transactions', 'with_discount']);
$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));
$discount = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['discount'] ?? 0));
$data['discount'] = $discount;
$data['net_amount'] = collect($transactions)->sum(fn (array $transaction) => (float) ($transaction['net_amount'] ?? 0)) + $discount;
return Arr::except($data, ['client', 'transactions']);
}
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());
}
$record = app(CreateSaleAction::class)($this->getFormDataMutation($data), $transactions);
return $record;
}
protected function afterCreate(): void
protected function getRedirectUrl(): string
{
$branch = Branch::find($this->data['branch_id']);
$client = $this->getClient();
if (! $client) {
return parent::getRedirectUrl();
}
return ClientResource::getUrl('view', ['record' => $client->id]).'?activeRelationManager=3';
}
}

View File

@@ -36,6 +36,16 @@ class EditSale extends EditRecord
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']);
}
@@ -71,6 +81,16 @@ class EditSale extends EditRecord
]
)->thenReturn();
}
$accountIds = collect($transactions)
->pluck('account_id')
->filter()
->unique()
->values()
->all();
$record->accounts()->sync($accountIds);
DB::commit();
} catch (\Exception $exception) {
DB::rollBack();

View File

@@ -4,9 +4,9 @@ namespace App\Filament\Resources;
use App\Commands\Transmittal\GenerateTransmittalSeries;
use App\Commands\Transmittal\StoreTransmittalCommand;
use App\Exports\TransmittalsExport;
use App\Filament\Resources\TransmittalResource\Pages;
use App\Jobs\ExportCompleteJob;
use App\Jobs\TransmittalPDFExportJob;
use App\Models\Branch;
use App\Models\Client;
use App\Models\Transmittal;
@@ -26,6 +26,7 @@ use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Malzariey\FilamentDaterangepickerFilter\Filters\DateRangeFilter;
class TransmittalResource extends Resource
@@ -112,7 +113,7 @@ class TransmittalResource extends Resource
public static function getTableActions(): array
{
return [
Tables\Actions\Action::make('Export')->action(fn ($record) => static::exportTransmittal([$record->id])),
Tables\Actions\Action::make('Export')->label('Export as PDF')->action(fn ($record) => static::exportTransmittal([$record->id])),
Tables\Actions\ViewAction::make(),
Tables\Actions\Action::make('Update Status')
->fillForm(function ($record) {
@@ -139,7 +140,7 @@ class TransmittalResource extends Resource
})
->icon('heroicon-o-pencil-square')
->slideOver()
->hidden(! auth()->user()->can('update_transmittal')),
->hidden(! Auth::user()->can('update_transmittal')),
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
];
@@ -147,13 +148,11 @@ class TransmittalResource extends Resource
public static function exportTransmittal(array $id): void
{
$recipient = auth()->user();
$recipient = Auth::user();
static::generateExportNotification();
(new TransmittalsExport([$id]))->store('public/transmittal-export.xlsx')->chain([
app(ExportCompleteJob::class, ['user' => $recipient]),
]);
dispatch(new TransmittalPDFExportJob($recipient, $id));
}
public static function generateExportNotification(): Notification

View File

@@ -2,6 +2,8 @@
namespace App\Jobs;
use App\Models\Transmittal;
use Barryvdh\DomPDF\Facade\Pdf;
use Filament\Notifications\Actions\Action as NotificationAction;
use Filament\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -13,30 +15,37 @@ use Illuminate\Support\Facades\Storage;
class ExportCompleteJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(private $user)
public function __construct(private $user, private array $ids)
{
}
/**
* Execute the job.
*/
public function handle(): void
{
Notification::make()->success()
$transmittals = Transmittal::query()
->with(['client', 'branch', 'files.notes', 'files.remarks'])
->whereIn('id', $this->ids)
->get();
$pdf = Pdf::loadView('transmittal.export.transmittal-export-pdf', [
'transmittals' => $transmittals,
]);
Storage::disk('public')->put('transmittal-export.pdf', $pdf->output());
Notification::make()
->success()
->title('Export Completed')
->actions([
NotificationAction::make('download_transmittal-export.xlsx')
->label('Download File')
->url(url: Storage::url('public/transmittal-export.xlsx') ,shouldOpenInNewTab: true)
NotificationAction::make('download_transmittal-export.pdf')
->label('Download PDF File')
->url(Storage::url('transmittal-export.pdf'), true)
->markAsRead(),
]
)
])
->sendToDatabase($this->user);
}
}

28
app/Jobs/TestQueueJob.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class TestQueueJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public function __construct()
{
}
public function handle(): void
{
Log::info('TestQueueJob executed');
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Jobs;
use App\Models\Transmittal;
use Barryvdh\DomPDF\Facade\Pdf;
use Filament\Forms\Components\Actions;
use Filament\Notifications\Livewire\Notifications;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class TransmittalPDFExportJob implements ShouldQueue
{
use Queueable, Dispatchable, InteractsWithQueue, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(private $user, private array $ids)
{}
/**
* Execute the job.
*/
public function handle(): void
{
$transmittals = Transmittal::query()
->with(['client', 'branch', 'files.notes', 'files.remarks'])
->whereIn('id', $this->ids)
->get();
$pdf = Pdf::loadView('transmittal.export.transmittal-export-pdf', [
'transmittals' => $transmittals,
]);
Storage::disk('public')->put('transmittal-export.pdf', $pdf->output());
Notifications::make()
->success()
->title('Export Completed')
->actions([
Actions\Action::make('download_transmittal-export.pdf')
->label('Download PDF File')
->url(Storage::url('transmittal-export.pdf'), true)
->markAsRead(),
])
->sendToDatabase($this->user);
}
}

View File

@@ -78,4 +78,9 @@ class Client extends Model
{
return $this->hasManyThrough(Journal::class, Branch::class);
}
public function discounts(): HasMany
{
return $this->hasMany(Discount::class);
}
}

10
app/Models/Discount.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Discount extends Model
{
protected $fillable = ['discount'];
}

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Expense extends Model
@@ -43,4 +44,14 @@ class Expense extends Model
{
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,13 +5,21 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Sale extends Model
{
use HasFactory;
protected $guarded = [];
protected $fillable = [
'branch_id',
'user_id',
'client_id',
'happened_on',
'reference_number',
'buyer'
];
protected $casts = [
'happened_on' => 'date:Y-m-d',
@@ -43,4 +51,19 @@ class Sale extends Model
{
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

@@ -2,6 +2,7 @@
namespace App\Observers;
use App\Commands\Clients\GenerateBaseAccountCommand;
use App\Models\Client;
use Illuminate\Support\Facades\DB;
@@ -12,45 +13,7 @@ class ClientObserver
*/
public function created(Client $client): void
{
DB::transaction(function () use ($client) {
$client->accounts()->createMany([
[
'account_type_id' => 1,
'account' => 'Cash',
'normal_balance' => 'debit',
],
[
'account_type_id' => 1,
'account' => 'Input Tax',
'normal_balance' => 'debit',
],
[
'account_type_id' => 1,
'account' => 'Creditable Withholding Tax',
'normal_balance' => 'debit',
],
[
'account_type_id' => 2,
'account' => 'Output Tax',
'normal_balance' => 'credit',
],
[
'account_type_id' => 2,
'account' => 'Payable Withholding Tax',
'normal_balance' => 'credit',
],
[
'account_type_id' => 5,
'account' => 'Vat Exempt Revenue',
'normal_balance' => 'credit',
],
[
'account_type_id' => 4,
'account' => 'Sales Discount',
'normal_balance' => 'debit',
],
]);
});
app(GenerateBaseAccountCommand::class)->execute($client);
}
/**

View File

@@ -3,6 +3,7 @@
namespace App\Providers\Filament;
use BezhanSalleh\FilamentShield\FilamentShieldPlugin;
use Filament\Navigation\NavigationItem;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
@@ -17,6 +18,7 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider
@@ -39,6 +41,13 @@ class AdminPanelProvider extends PanelProvider
->pages([
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()
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\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

@@ -10,9 +10,11 @@
"require": {
"php": "^8.2",
"awcodes/filament-table-repeater": "^3.0",
"barryvdh/laravel-dompdf": "^2.0",
"bezhansalleh/filament-shield": "^3.2",
"filament/filament": "^3.2",
"laravel/framework": "^11.9",
"laravel/horizon": "^5.44",
"laravel/tinker": "^2.9",
"livewire/livewire": "^3.4",
"livewire/volt": "^1.0",

617
composer.lock generated

File diff suppressed because it is too large Load Diff

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

View File

@@ -0,0 +1,29 @@
<?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::create('discounts', function (Blueprint $table) {
$table->id();
$table->text('discount');
$table->foreignId('client_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('discounts');
}
};

View File

@@ -0,0 +1,28 @@
<?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('transactions', function (Blueprint $table) {
$table->string('discount_type')->after('with_discount')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('transactions', function (Blueprint $table) {
$table->dropColumn('discount_type');
});
}
};

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Transmittal Report</title>
<style>
body {
font-family: DejaVu Sans, sans-serif;
font-size: 12px;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header-logo {
height: 60px;
margin-bottom: 10px;
}
.header-title {
font-size: 20px;
font-weight: bold;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #000;
padding: 4px;
}
th {
text-transform: capitalize;
text-align: center;
}
</style>
</head>
<body>
<div class="header">
<img src="{{ public_path('images/logo.png') }}" alt="Logo" class="header-logo">
<div class="header-title">Transmittal Report</div>
</div>
@include('transmittal.export.transmittal-export-table', ['transmittals' => $transmittals])
</body>
</html>

View File

@@ -1,5 +1,6 @@
<?php
use App\Jobs\TestQueueJob;
use App\Models\Transmittal;
use Illuminate\Support\Facades\Route;
@@ -13,10 +14,16 @@ Route::view('profile', 'profile')
->middleware(['auth'])
->name('profile');
Route::get('preview-transmittal', function () {
return view('transmittal.export.transmittal-export-table')->with(['transmittals' => Transmittal::withCount(['files', 'notes', 'remarks'])->with(['files' => function ($files) {
$files->withCount(['notes', 'remarks']);
}])->get()]);
// Route::get('preview-transmittal', function () {
// return view('transmittal.export.transmittal-export-table')->with(['transmittals' => Transmittal::withCount(['files', 'notes', 'remarks'])->with(['files' => function ($files) {
// $files->withCount(['notes', 'remarks']);
// }])->get()]);
// });
Route::get('queue-test', function () {
TestQueueJob::dispatch();
return 'TestQueueJob dispatched';
});
require __DIR__.'/auth.php';