Browse Source

feat: add Accounting domain with models, DTOs, actions, enums, policies, and runtime relationship support

- Introduced `ChartOfAccount` model with parent-child relationships, casting, and policy integration.
- Defined DTOs and actions for registering, adjusting, and suspending accounts (`RegisterChartOfAccount`, `AdjustAccountClassification`, `SuspendAccountPostings`).
- Added enums for account classifications and balances.
- Created `AccountingServiceProvider` and `RelationshipServiceProvider` for domain initialization and runtime relationship binding.
- Migrated COA-related logic from Billing domain and removed redundant files.
- Updated DataTables, Livewire forms, and localization for COA and product management.
master
Syarif Ubaidillah 4 weeks ago
parent
commit
2eb23e8793
  1. 19
      app/Domains/Accounting/Actions/LedgerConfiguration/AdjustAccountClassification.php
  2. 20
      app/Domains/Accounting/Actions/LedgerConfiguration/RegisterChartOfAccount.php
  3. 16
      app/Domains/Accounting/Actions/LedgerConfiguration/SuspendAccountPostings.php
  4. 17
      app/Domains/Accounting/DTOs/LedgerConfiguration/AdjustAccountClassificationDTO.php
  5. 19
      app/Domains/Accounting/DTOs/LedgerConfiguration/RegisterChartOfAccountDTO.php
  6. 18
      app/Domains/Accounting/Enums/Ledger/AccountBalance.php
  7. 21
      app/Domains/Accounting/Enums/Ledger/AccountClassification.php
  8. 42
      app/Domains/Accounting/Models/ChartOfAccount.php
  9. 34
      app/Domains/Accounting/Policies/ChartOfAccountPolicy.php
  10. 23
      app/Domains/Accounting/Providers/AccountingServiceProvider.php
  11. 31
      app/Domains/Accounting/Providers/RelationshipServiceProvider.php
  12. 23
      app/Domains/Billing/Actions/Catalog/ModifyProduct.php
  13. 21
      app/Domains/Billing/Actions/Catalog/RegisterProduct.php
  14. 12
      app/Domains/Billing/DTOs/Catalog/ModifyProductDTO.php
  15. 12
      app/Domains/Billing/DTOs/Catalog/RegisterProductDTO.php
  16. 9
      app/Domains/Billing/Enums/Catalog/AccountBalance.php
  17. 12
      app/Domains/Billing/Enums/Catalog/AccountClassification.php
  18. 21
      app/Domains/Billing/Models/ChartOfAccount.php
  19. 2
      app/Domains/Billing/Models/InvoiceItem.php
  20. 4
      app/Domains/Billing/Models/Product.php
  21. 6
      app/Domains/Identity/Enums/RoleType.php
  22. 11
      app/Domains/Payment/Listeners/VirtualAccount/InitializeVirtualAccountForPublishedInvoice.php
  23. 32
      app/Http/Controllers/Web/Accounting/ChartOfAccountController.php
  24. 31
      app/Http/Controllers/Web/Billing/ProductController.php
  25. 130
      app/Http/DataTables/Accounting/ChartOfAccountDataTable.php
  26. 131
      app/Http/DataTables/Billing/ProductDataTable.php
  27. 30
      app/Livewire/Forms/Accounting/ChartOfAccountForm.php
  28. 18
      app/Livewire/Forms/Billing/ProductForm.php
  29. 26
      app/Livewire/Forms/Finance/ChartOfAccountForm.php
  30. 7
      bootstrap/providers.php
  31. 1
      database/migrations/2026_09_08_022815_create_chart_of_accounts_table.php
  32. 5
      lang/en/domains/account/enum.php
  33. 21
      lang/en/domains/accounting/enum.php
  34. 12
      lang/en/domains/accounting/field.php
  35. 9
      lang/en/domains/accounting/seo.php
  36. 9
      lang/en/domains/billing/field.php
  37. 9
      lang/en/domains/billing/seo.php
  38. 1
      lang/en/domains/channel/field.php
  39. 2
      lang/en/resources.php
  40. 5
      lang/en/ui/menu.php
  41. 5
      lang/id/domains/account/enum.php
  42. 19
      lang/id/domains/accounting/enum.php
  43. 12
      lang/id/domains/accounting/field.php
  44. 9
      lang/id/domains/accounting/seo.php
  45. 9
      lang/id/domains/billing/field.php
  46. 9
      lang/id/domains/billing/seo.php
  47. 1
      lang/id/domains/channel/field.php
  48. 8
      lang/id/domains/channel/messages.php
  49. 28
      lang/id/domains/finance/enum.php
  50. 2
      lang/id/resources.php
  51. 5
      lang/id/ui/menu.php
  52. 4
      resources/scss/plugin/datatables.scss
  53. 4
      resources/views/components/button.blade.php
  54. 16
      resources/views/components/layouts/nav/sidebar.blade.php
  55. 20
      resources/views/pages/accounting/chartofaccounts/index.blade.php
  56. 56
      resources/views/pages/accounting/chartofaccounts/⚡form-modal/form-modal.blade.php
  57. 115
      resources/views/pages/accounting/chartofaccounts/⚡form-modal/form-modal.php
  58. 17
      resources/views/pages/billing/products/index.blade.php
  59. 27
      resources/views/pages/billing/products/⚡form-modal/form-modal.blade.php
  60. 83
      resources/views/pages/billing/products/⚡form-modal/form-modal.php
  61. 13
      routes/web.php
  62. 4
      tests/Unit/Domains/Account/Enums/GenderOptionTest.php

19
app/Domains/Accounting/Actions/LedgerConfiguration/AdjustAccountClassification.php

@ -0,0 +1,19 @@
<?php
namespace App\Domains\Accounting\Actions\LedgerConfiguration;
use App\Domains\Accounting\DTOs\LedgerConfiguration\AdjustAccountClassificationDTO;
use App\Domains\Accounting\Models\ChartOfAccount;
class AdjustAccountClassification
{
public function execute(ChartOfAccount $coa, AdjustAccountClassificationDTO $dto): void
{
$coa->update([
'code' => $dto->code,
'normal_balance' => $dto->normal_balance,
'classification' => $dto->classification,
'parent_id' => $dto->parentId,
]);
}
}

20
app/Domains/Accounting/Actions/LedgerConfiguration/RegisterChartOfAccount.php

@ -0,0 +1,20 @@
<?php
namespace App\Domains\Accounting\Actions\LedgerConfiguration;
use App\Domains\Accounting\DTOs\LedgerConfiguration\RegisterChartOfAccountDTO;
use App\Domains\Accounting\Models\ChartOfAccount;
class RegisterChartOfAccount
{
public function execute(RegisterChartOfAccountDTO $dto): void
{
ChartOfAccount::create([
'code' => $dto->code,
'name' => $dto->name,
'classification' => $dto->classification,
'normal_balance' => $dto->normal_balance,
'parent_id' => $dto->parentId,
]);
}
}

16
app/Domains/Accounting/Actions/LedgerConfiguration/SuspendAccountPostings.php

@ -0,0 +1,16 @@
<?php
namespace App\Domains\Accounting\Actions\LedgerConfiguration;
use App\Domains\Accounting\Models\ChartOfAccount;
use App\Domains\System\Enums\LifecycleStatus;
class SuspendAccountPostings
{
public function execute(ChartOfAccount $coa): void
{
$coa->update([
'status' => LifecycleStatus::INACTIVE,
]);
}
}

17
app/Domains/Accounting/DTOs/LedgerConfiguration/AdjustAccountClassificationDTO.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Accounting\DTOs\LedgerConfiguration;
use App\Domains\Accounting\Enums\Ledger\AccountBalance;
use App\Domains\Accounting\Enums\Ledger\AccountClassification;
readonly class AdjustAccountClassificationDTO
{
public function __construct(
public string $code,
public string $name,
public AccountClassification $classification,
public AccountBalance $normal_balance,
public ?int $parentId,
) {}
}

19
app/Domains/Accounting/DTOs/LedgerConfiguration/RegisterChartOfAccountDTO.php

@ -0,0 +1,19 @@
<?php
namespace App\Domains\Accounting\DTOs\LedgerConfiguration;
use App\Domains\Accounting\Enums\Ledger\AccountBalance;
use App\Domains\Accounting\Enums\Ledger\AccountClassification;
use App\Domains\System\Enums\LifecycleStatus;
readonly class RegisterChartOfAccountDTO
{
public function __construct(
public string $code,
public string $name,
public AccountClassification $classification,
public AccountBalance $normal_balance,
public LifecycleStatus $status,
public ?int $parentId,
) {}
}

18
app/Domains/Accounting/Enums/Ledger/AccountBalance.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Accounting\Enums\Ledger;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
use Yajra\DataTables\Html\HasOptions;
enum AccountBalance: string implements HasLabel
{
use HasPredicateMethod;
use HasOptions;
use InteractsWithLabels;
case CREDIT = 'credit';
case DEBIT = 'debit';
}

21
app/Domains/Accounting/Enums/Ledger/AccountClassification.php

@ -0,0 +1,21 @@
<?php
namespace App\Domains\Accounting\Enums\Ledger;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
use Yajra\DataTables\Html\HasOptions;
enum AccountClassification: string implements HasLabel
{
use HasPredicateMethod;
use HasOptions;
use InteractsWithLabels;
case LIABILITY = 'liability';
case ASSET = 'asset';
case EQUITY = 'equity';
case REVENUE = 'revenue';
case EXPENSE = 'expense';
}

42
app/Domains/Accounting/Models/ChartOfAccount.php

@ -0,0 +1,42 @@
<?php
namespace App\Domains\Accounting\Models;
use App\Domains\Accounting\Enums\Ledger\AccountBalance;
use App\Domains\Accounting\Enums\Ledger\AccountClassification;
use App\Domains\Accounting\Policies\ChartOfAccountPolicy;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'code',
'name',
'classification',
'normal_balance',
'parent_id',
])]
#[UsePolicy(ChartOfAccountPolicy::class)]
class ChartOfAccount extends Model
{
use HasPublicUlid;
protected $casts = [
'classification' => AccountClassification::class,
'normal_balance' => AccountBalance::class,
];
public function parent(): BelongsTo
{
return $this->belongsTo(ChartOfAccount::class, 'parent_id');
}
public function children(): HasMany
{
return $this->hasMany(ChartOfAccount::class, 'parent_id');
}
}

34
app/Domains/Accounting/Policies/ChartOfAccountPolicy.php

@ -0,0 +1,34 @@
<?php
namespace App\Domains\Accounting\Policies;
use App\Domains\Accounting\Models\ChartOfAccount;
use App\Domains\Identity\Models\User;
class ChartOfAccountPolicy
{
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('coa.viewAny');
}
public function view(User $user, ChartOfAccount $model): bool
{
return $user->hasPermissionTo('coa.view');
}
public function create(User $user): bool
{
return $user->hasPermissionTo('coa.create');
}
public function update(User $user, ChartOfAccount $model): bool
{
return $user->hasPermissionTo('coa.update');
}
public function delete(User $user, ChartOfAccount $model): bool
{
return $user->hasPermissionTo('coa.delete');
}
}

23
app/Domains/Accounting/Providers/AccountingServiceProvider.php

@ -0,0 +1,23 @@
<?php
namespace App\Domains\Accounting\Providers;
use App\Domains\System\Traits\Provider\RegistersDomainEvents;
use Illuminate\Support\ServiceProvider;
class AccountingServiceProvider extends ServiceProvider
{
use RegistersDomainEvents;
protected array $listen = [];
public function register(): void
{
$this->app->register(RelationshipServiceProvider::class);
}
public function boot(): void
{
$this->registerEvents();
}
}

31
app/Domains/Accounting/Providers/RelationshipServiceProvider.php

@ -0,0 +1,31 @@
<?php
namespace App\Domains\Accounting\Providers;
use Illuminate\Support\ServiceProvider;
class RelationshipServiceProvider extends ServiceProvider
{
/**
* Boot cross-domain runtime relationships.
*
* This provider is strictly dedicated to runtime, decoupled cross-domain
* relationship definitions. Use Model::resolveRelationUsing() here to wire
* explicit polymorphic or foreign-key macros between models that live in
* different domains — without introducing a hard compile-time dependency
* between those domains.
*
* Do NOT add route registrations, bindings, event listeners, or any other
* bootstrapping logic here. Keep this file focused on relationships only.
*
* Example:
*
* Order::resolveRelationUsing('customer', fn (Order $order) =>
* $order->belongsTo(Customer::class, 'customer_id')
* );
*/
public function boot(): void
{
// Register cross-domain relationships below.
}
}

23
app/Domains/Billing/Actions/Catalog/ModifyProduct.php

@ -0,0 +1,23 @@
<?php
namespace App\Domains\Billing\Actions\Catalog;
use App\Domains\Billing\DTOs\Catalog\ModifyProductDTO;
use App\Domains\Billing\Models\Product;
use Illuminate\Support\Facades\DB;
class ModifyProduct
{
public function execute(Product $product, ModifyProductDTO $dto): Product
{
return DB::transaction(function () use ($product, $dto) {
$product->update([
'name' => $dto->name,
'code' => $dto->code,
'coa_id' => $dto->coaId,
]);
return $product->fresh();
});
}
}

21
app/Domains/Billing/Actions/Catalog/RegisterProduct.php

@ -0,0 +1,21 @@
<?php
namespace App\Domains\Billing\Actions\Catalog;
use App\Domains\Billing\DTOs\Catalog\RegisterProductDTO;
use App\Domains\Billing\Models\Product;
use Illuminate\Support\Facades\DB;
class RegisterProduct
{
public function execute(RegisterProductDTO $dto): Product
{
return DB::transaction(function () use ($dto) {
return Product::create([
'name' => $dto->name,
'code' => $dto->code,
'coa_id' => $dto->coaId,
]);
});
}
}

12
app/Domains/Billing/DTOs/Catalog/ModifyProductDTO.php

@ -0,0 +1,12 @@
<?php
namespace App\Domains\Billing\DTOs\Catalog;
readonly class ModifyProductDTO
{
public function __construct(
public string $name,
public string $code,
public int $coaId,
) {}
}

12
app/Domains/Billing/DTOs/Catalog/RegisterProductDTO.php

@ -0,0 +1,12 @@
<?php
namespace App\Domains\Billing\DTOs\Catalog;
readonly class RegisterProductDTO
{
public function __construct(
public string $name,
public string $code,
public int $coaId,
) {}
}

9
app/Domains/Billing/Enums/Catalog/AccountBalance.php

@ -1,9 +0,0 @@
<?php
namespace App\Domains\Billing\Enums\Catalog;
enum AccountBalance: string
{
case CREDIT = 'credit';
case DEBIT = 'debit';
}

12
app/Domains/Billing/Enums/Catalog/AccountClassification.php

@ -1,12 +0,0 @@
<?php
namespace App\Domains\Billing\Enums\Catalog;
enum AccountClassification: string
{
case LIABILITY = 'liability';
case ASSET = 'asset';
case EQUITY = 'equity';
case REVENUE = 'revenue';
case EXPENSE = 'expense';
}

21
app/Domains/Billing/Models/ChartOfAccount.php

@ -1,21 +0,0 @@
<?php
namespace App\Domains\Billing\Models;
use Database\Factories\Billing\ChartOfAccountFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
'code',
'name',
'classification',
'normal_balance'
])]
#[UseFactory(ChartOfAccountFactory::class)]
class ChartOfAccount extends Model
{
use HasFactory;
}

2
app/Domains/Billing/Models/InvoiceItem.php

@ -19,7 +19,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
'net_amount',
'invoice_id',
'product_id',
'chart_of_account_id'
'coa_id'
])]
#[UseFactory(InvoiceItemFactory::class)]
class InvoiceItem extends Model

4
app/Domains/Billing/Models/Product.php

@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
'name',
'pricing_type',
'pricing_id',
'chart_of_account_id'
'coa_id'
])]
#[UseFactory(ProductFactory::class)]
class Product extends Model
@ -24,7 +24,7 @@ class Product extends Model
public function chartOfAccount(): BelongsTo
{
return $this->belongsTo(ChartOfAccount::class);
return $this->belongsTo(ChartOfAccount::class, 'coa_id');
}
public function pricing(): MorphTo

6
app/Domains/Identity/Enums/RoleType.php

@ -45,9 +45,11 @@ enum RoleType: string
...self::generatePolicy('client', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('coa', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('product', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('discount', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('invoice', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('product-mapping', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('chart-of-account', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('role', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('user', [self::SYSTEM_ADMIN, self::ADMIN]),

11
app/Domains/Payment/Listeners/VirtualAccount/InitializeVirtualAccountForPublishedInvoice.php

@ -1,11 +0,0 @@
<?php
namespace App\Domains\Payment\Listeners\VirtualAccount;
class InitializeVirtualAccountForPublishedInvoice
{
public function handle(object $event): void
{
//
}
}

32
app/Http/Controllers/Web/Accounting/ChartOfAccountController.php

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\Web\Accounting;
use App\Attributes\LayoutData;
use App\Attributes\Seo;
use App\Http\Controllers\Controller;
use App\Http\DataTables\Accounting\ChartOfAccountDataTable;
class ChartOfAccountController extends Controller
{
/**
* Handle the incoming request.
*/
#[Seo(
title: 'domains/accounting/seo.coa.title',
description: 'domains/accounting/seo.coa.description',
keywords: 'domains/accounting/seo.coa.keywords'
)]
#[LayoutData(
header: 'domains/accounting/seo.coa.title',
breadcrumbs: [
'ui/menu.dashboard' => 'dashboard',
'domains/accounting/seo.coa.title' => '',
],
)]
public function __invoke(ChartOfAccountDataTable $dataTable)
{
return $dataTable->render('pages.accounting.chartofaccounts.index');
}
}

31
app/Http/Controllers/Web/Billing/ProductController.php

@ -0,0 +1,31 @@
<?php
namespace App\Http\Controllers\Web\Billing;
use App\Attributes\LayoutData;
use App\Attributes\Seo;
use App\Http\Controllers\Controller;
use App\Http\DataTables\Billing\ProductDataTable;
class ProductController extends Controller
{
/**
* Handle the incoming request.
*/
#[Seo(
title: 'domains/billing/seo.product.title',
description: 'domains/billing/seo.product.description',
keywords: 'domains/billing/seo.product.keywords'
)]
#[LayoutData(
header: 'domains/billing/seo.product.title',
breadcrumbs: [
'ui/menu.dashboard' => 'dashboard',
'domains/billing/seo.product.title' => '',
],
)]
public function __invoke(ProductDataTable $dataTable)
{
return $dataTable->render('pages.billing.products.index');
}
}

130
app/Http/DataTables/Accounting/ChartOfAccountDataTable.php

@ -0,0 +1,130 @@
<?php
namespace App\Http\DataTables\Accounting;
use App\Domains\Accounting\Models\ChartOfAccount;
use Illuminate\Database\Eloquent\Builder as QueryBuilder;
use Yajra\DataTables\EloquentDataTable;
use Yajra\DataTables\Html\Builder as HtmlBuilder;
use Yajra\DataTables\Html\Button;
use Yajra\DataTables\Html\Column;
use Yajra\DataTables\Services\DataTable;
class ChartOfAccountDataTable extends DataTable
{
/**
* Build the DataTable class.
*
* @param QueryBuilder<ChartOfAccount> $query Results from query() method.
*/
public function dataTable(QueryBuilder $query): EloquentDataTable
{
return (new EloquentDataTable($query))
->editColumn('normal_balance', fn (ChartOfAccount $chartOfAccount) => $chartOfAccount->normal_balance->label())
->editColumn('classification', fn (ChartOfAccount $chartOfAccount) => $chartOfAccount->classification->label())
->addColumn(
'action',
fn (ChartOfAccount $chartOfAccount) => view('components.datatables.action-button', [
'edit' => [
'modal' => 'chartofaccount-form-modal',
'permission' => auth()->user()->can('update', $chartOfAccount),
],
'delete' => [
'url' => null,
'title' => __('ui/button.delete'),
'permission' => auth()->user()->can('delete', $chartOfAccount),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.chartOfAccount')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.chartOfAccount')]),
],
'table_name' => 'chart-of-account-table',
'id' => $chartOfAccount->ulid,
])
)
->addIndexColumn();
}
/**
* Get the query source of dataTable.
*
* @return QueryBuilder<ChartOfAccount>
*/
public function query(ChartOfAccount $model): QueryBuilder
{
return $model->newQuery();
}
/**
* Optional method if you want to use the html builder.
*/
public function html(): HtmlBuilder
{
return $this->builder()
->setTableId('chart-of-account-table')
->columns($this->getColumns())
->minifiedAjax()
->orderBy(1)
->layout([
'topStart' => [
'className' => 'col-md-auto me-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-start align-items-center align-items-md-start gap-1',
'features' => ['buttons', 'pageLength'],
],
'topEnd' => [
'className' => 'col-md-auto ms-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-end align-items-center align-items-md-start gap-1',
'features' => ['search'],
],
'bottomStart' => 'info',
'bottomEnd' => 'paging',
])
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui/button.lookup'),
],
])
->buttons([
Button::make('add')
->action('$("#chartofaccount-form-modal").modal("show");')
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')
->text(svg('tabler-reload', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
]);
}
/**
* Get the dataTable columns definition.
*/
public function getColumns(): array
{
return [
Column::computed('DT_RowIndex')
->title('#')
->searchable(false)
->orderable(false),
Column::make('code')
->title(__('domains/accounting/field.coa.code')),
Column::make('name')
->title(__('domains/accounting/field.coa.name')),
Column::make('normal_balance')
->title(__('domains/accounting/field.coa.normal_balance')),
Column::make('classification')
->title(__('domains/accounting/field.coa.classification')),
Column::computed('action')
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)
->addClass('text-center'),
];
}
/**
* Get the filename for export.
*/
protected function filename(): string
{
return 'ChartOfAccountDataTable_' . date('YmdHis');
}
}

131
app/Http/DataTables/Billing/ProductDataTable.php

@ -0,0 +1,131 @@
<?php
namespace App\Http\DataTables\Billing;
use App\Domains\Billing\Models\Product;
use Illuminate\Database\Eloquent\Builder as QueryBuilder;
use Yajra\DataTables\EloquentDataTable;
use Yajra\DataTables\Html\Builder as HtmlBuilder;
use Yajra\DataTables\Html\Button;
use Yajra\DataTables\Html\Column;
use Yajra\DataTables\Services\DataTable;
class ProductDataTable extends DataTable
{
/**
* Build the DataTable class.
*
* @param QueryBuilder<Product> $query Results from query() method.
*/
public function dataTable(QueryBuilder $query): EloquentDataTable
{
return (new EloquentDataTable($query))
->editColumn('chart_of_account.name', fn (Product $product) => $product->chartOfAccount?->name ?? '-')
->addColumn(
'action',
fn (Product $product) => view('components.datatables.action-button', [
'edit' => [
'modal' => 'product-form-modal',
'permission' => auth()->user()->can('update', $product),
],
'delete' => [
'url' => null,
'title' => __('ui/button.delete'),
'permission' => auth()->user()->can('delete', $product),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.product')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.product')]),
],
'table_name' => 'product-table',
'id' => $product->id,
])
)
->rawColumns(['action'])
->addIndexColumn();
}
/**
* Get the query source of dataTable.
*
* @return QueryBuilder<Product>
*/
public function query(Product $model): QueryBuilder
{
return $model->newQuery()->with(['chartOfAccount', 'pricing']);
}
/**
* Optional method if you want to use the html builder.
*/
public function html(): HtmlBuilder
{
return $this->builder()
->setTableId('product-table')
->columns($this->getColumns())
->minifiedAjax()
->orderBy(1)
->layout([
'topStart' => [
'className' => 'col-md-auto me-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-start align-items-center align-items-md-start gap-1',
'features' => ['buttons', 'pageLength'],
],
'topEnd' => [
'className' => 'col-md-auto ms-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-end align-items-center align-items-md-start gap-1',
'features' => ['search'],
],
'bottomStart' => 'info',
'bottomEnd' => 'paging',
])
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui/button.lookup'),
],
'scrollX' => true,
'scrollCollapse' => true,
'responsive' => true,
])
->buttons([
Button::make('add')
->action('$("#product-form-modal").modal("show");')
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')
->text(svg('tabler-reload', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
]);
}
/**
* Get the dataTable columns definition.
*/
public function getColumns(): array
{
return [
Column::computed('DT_RowIndex')
->title('#')
->searchable(false)
->orderable(false),
Column::make('code')
->title(__('domains/billing/field.product.code')),
Column::make('name')
->title(__('domains/billing/field.product.name')),
Column::make('chart_of_account.name')
->title(__('domains/billing/field.product.coa')),
Column::computed('action')
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)
->addClass('text-center'),
];
}
/**
* Get the filename for export.
*/
protected function filename(): string
{
return 'ProductDataTable_' . date('YmdHis');
}
}

30
app/Livewire/Forms/Accounting/ChartOfAccountForm.php

@ -0,0 +1,30 @@
<?php
namespace App\Livewire\Forms\Accounting;
use App\Domains\Accounting\Enums\Ledger\AccountBalance;
use App\Domains\Accounting\Enums\Ledger\AccountClassification;
use App\Domains\System\Enums\LifecycleStatus;
use Livewire\Attributes\Validate;
use Livewire\Form;
class ChartOfAccountForm extends Form
{
#[Validate('required|string|max:255', as: 'domains/accounting/field.coa.code')]
public string $code = '';
#[Validate('required|string|max:255', as: 'domains/accounting/field.coa.name')]
public string $name = '';
#[Validate('required', as: 'domains/accounting/field.coa.classification')]
public ?AccountClassification $classification = null;
#[Validate('required', as: 'domains/accounting/field.coa.normal_balance')]
public ?AccountBalance $normal_balance = null;
#[Validate(as: 'domains/accounting/field.coa.parent')]
public int|string|null $parent_id = null;
#[Validate('required', as: 'domains/accounting/field.coa.status')]
public ?LifecycleStatus $status = LifecycleStatus::ACTIVE;
}

18
app/Livewire/Forms/Billing/ProductForm.php

@ -0,0 +1,18 @@
<?php
namespace App\Livewire\Forms\Billing;
use Livewire\Attributes\Validate;
use Livewire\Form;
class ProductForm extends Form
{
#[Validate('required|string|max:255', as: 'domains/billing/field.product.code')]
public string $code = '';
#[Validate('required|string|max:255', as: 'domains/billing/field.product.name')]
public string $name = '';
#[Validate('required|integer|exists:chart_of_accounts,id', as: 'domains/billing/field.product.coa')]
public ?int $coa_id = null;
}

26
app/Livewire/Forms/Finance/ChartOfAccountForm.php

@ -1,26 +0,0 @@
<?php
namespace App\Livewire\Forms\Finance;
use App\Domains\Finance\Enums\AccountClassification;
use App\Domains\System\Enums\LifecycleStatus;
use Livewire\Attributes\Validate;
use Livewire\Form;
class ChartOfAccountForm extends Form
{
#[Validate('required|string|max:255', as: 'domains/finance/field.coa.code')]
public string $code = '';
#[Validate('required|string|max:255', as: 'domains/finance/field.coa.name')]
public string $name = '';
#[Validate('required', as: 'domains/finance/field.coa.classification')]
public AccountClassification $classification = AccountClassification::REVENUE;
#[Validate(as: 'domains/finance/field.coa.parent')]
public ?string $parent_id = null;
#[Validate('required', as: 'domains/finance/field.coa.status')]
public LifecycleStatus $status = LifecycleStatus::ACTIVE;
}

7
bootstrap/providers.php

@ -4,7 +4,6 @@ use App\Domains\Academic\Providers\AcademicServiceProvider;
use App\Domains\Account\Providers\AccountServiceProvider;
use App\Domains\Admission\Providers\AdmissionServiceProvider;
use App\Domains\Channel\Providers\ChannelServiceProvider;
use App\Domains\Finance\Providers\FinanceServiceProvider;
use App\Domains\Identity\Providers\IdentityServiceProvider;
use App\Domains\System\Providers\SystemServiceProvider;
use App\Providers\AppServiceProvider;
@ -25,9 +24,6 @@ return [
// Domain: System
SystemServiceProvider::class,
// Domain: Finance
FinanceServiceProvider::class,
// Domain: Channel
ChannelServiceProvider::class,
@ -39,4 +35,7 @@ return [
// Domain: Billing
App\Domains\Billing\Providers\BillingServiceProvider::class,
// Domain: Accounting
App\Domains\Accounting\Providers\AccountingServiceProvider::class,
];

1
database/migrations/2026_09_08_022815_create_chart_of_accounts_table.php

@ -19,6 +19,7 @@ return new class extends Migration
$table->string('classification');
$table->string('normal_balance');
$table->foreignId('parent_id')->nullable();
$table->string('status');
$table->timestamps();
});
}

5
lang/en/domains/account/enum.php

@ -22,4 +22,9 @@ return [
],
],
],
'user_settings' => [
'notification' => 'Notification',
'language' => 'Language',
'timezone' => 'Timezone',
],
];

21
lang/en/domains/accounting/enum.php

@ -0,0 +1,21 @@
<?php
use App\Domains\Accounting\Enums\Ledger\AccountClassification;
use App\Domains\Accounting\Enums\Ledger\AccountBalance;
return [
'account_balance' => [
AccountBalance::CREDIT->value => 'Credit',
AccountBalance::DEBIT->value => 'Debit',
],
'account_classification' => [
AccountClassification::ASSET->value => 'Asset',
AccountClassification::LIABILITY->value => 'Liability',
AccountClassification::EQUITY->value => 'Equity',
AccountClassification::REVENUE->value => 'Revenue',
AccountClassification::EXPENSE->value => 'Expense',
],
];

12
lang/en/domains/accounting/field.php

@ -0,0 +1,12 @@
<?php
return [
'coa' => [
'code' => 'Account Code',
'name' => 'Account Name',
'normal_balance' => 'Normal Balance',
'classification' => 'Classification',
'status' => 'Status',
'parent' => 'Derived From',
],
];

9
lang/en/domains/accounting/seo.php

@ -0,0 +1,9 @@
<?php
return [
'coa' => [
'title' => 'Chart of Accounts (COA) Data',
'description' => 'Manage chart of accounts for financial records.',
'keywords' => 'chart of accounts, coa, accounting, finance',
],
];

9
lang/en/domains/billing/field.php

@ -0,0 +1,9 @@
<?php
return [
'product' => [
'code' => 'Product Code',
'name' => 'Product Name',
'coa' => 'Chart of Account (COA)',
],
];

9
lang/en/domains/billing/seo.php

@ -0,0 +1,9 @@
<?php
return [
'product' => [
'title' => 'Product Data',
'description' => 'Manage products and their chart of account mappings.',
'keywords' => 'product, products, billing, finance, coa',
],
];

1
lang/en/domains/channel/field.php

@ -10,6 +10,7 @@ return [
'username' => 'Username',
'password' => 'Password',
'token' => 'Token',
'bearer' => 'Bearer Token',
],
'endpoint_url' => 'Endpoint URL',
],

2
lang/en/resources.php

@ -18,6 +18,8 @@ return [
'fee_type' => 'Fee Type',
'term' => 'Term',
'chart_of_account' => 'Chart of Account',
'chartOfAccount' => 'Chart of Account',
'product' => 'Product',
'product_mapping' => 'Product Mapping',
'backup_file' => 'Backup File',
'invoice' => 'Invoice',

5
lang/en/ui/menu.php

@ -14,9 +14,12 @@ return [
'admission_schedules' => 'Admission Schedules',
'channel' => 'Channel',
'clients' => 'Clients',
'accounting' => 'Accounting',
'chart_of_accounts' => 'Chart of Accounts',
'billing' => 'Billing',
'products' => 'Products',
'finance' => 'Finance',
'invoices' => 'Invoices',
'chart_of_accounts' => 'Chart of Accounts',
'product_mappings' => 'Product Mappings',
'identity' => 'User Management',
'users' => 'Users',

5
lang/id/domains/account/enum.php

@ -22,4 +22,9 @@ return [
],
],
],
'user_settings' => [
'notification' => 'Notifikasi',
'language' => 'Bahasa',
'timezone' => 'Zona Waktu',
],
];

19
lang/id/domains/accounting/enum.php

@ -0,0 +1,19 @@
<?php
use App\Domains\Accounting\Enums\Ledger\AccountBalance;
use App\Domains\Accounting\Enums\Ledger\AccountClassification;
return [
'account_balance' => [
AccountBalance::CREDIT->value => 'Kredit',
AccountBalance::DEBIT->value => 'Debit',
],
'account_classification' => [
AccountClassification::ASSET->value => 'Aset',
AccountClassification::LIABILITY->value => 'Liabilitas',
AccountClassification::EQUITY->value => 'Ekuitas',
AccountClassification::REVENUE->value => 'Pendapatan',
AccountClassification::EXPENSE->value => 'Beban',
],
];

12
lang/id/domains/accounting/field.php

@ -0,0 +1,12 @@
<?php
return [
'coa' => [
'code' => 'Kode Akun',
'name' => 'Nama Akun',
'normal_balance' => 'Saldo Normal',
'classification' => 'Klasifikasi',
'status' => 'Status',
'parent' => 'Turunan Dari',
],
];

9
lang/id/domains/accounting/seo.php

@ -0,0 +1,9 @@
<?php
return [
'coa' => [
'title' => 'Data Bagan Akun (COA)',
'description' => 'Kelola bagan akun untuk pencatatan keuangan.',
'keywords' => 'bagan akun, coa, akuntansi, keuangan',
],
];

9
lang/id/domains/billing/field.php

@ -0,0 +1,9 @@
<?php
return [
'product' => [
'code' => 'Kode Produk',
'name' => 'Nama Produk',
'coa' => 'Bagan Akun (COA)',
],
];

9
lang/id/domains/billing/seo.php

@ -0,0 +1,9 @@
<?php
return [
'product' => [
'title' => 'Data Produk',
'description' => 'Kelola produk dan pemetaan bagan akun.',
'keywords' => 'produk, billing, tagihan, coa',
],
];

1
lang/id/domains/channel/field.php

@ -10,6 +10,7 @@ return [
'username' => 'Nama Pengguna',
'password' => 'Password',
'token' => 'Token',
'bearer' => 'Token Bearer',
],
'endpoint_url' => 'URL Endpoint',
],

8
lang/id/domains/channel/messages.php

@ -0,0 +1,8 @@
<?php
return [
'clients' => [
'synced' => 'Klien berhasil disinkronkan.',
'fail_to_sync' => 'Klien gagal disinkronkan: :error.',
],
];

28
lang/id/domains/finance/enum.php

@ -1,17 +1,14 @@
<?php
use App\Domains\Finance\Enums\AccountClassification;
use App\Domains\Finance\Enums\InvoiceStatus;
use App\Domains\Finance\Enums\InvoiceType;
use App\Domains\Finance\Enums\PaymentDirection;
use App\Domains\Finance\Enums\PaymentStatus;
use App\Domains\Billing\Enums\Invoicing\InvoiceStatus;
use App\Domains\Billing\Enums\Invoicing\InvoiceType;
return [
'invoice_status' => [
InvoiceStatus::PAID->value => 'Lunas',
InvoiceStatus::PENDING->value => 'Tertunda',
InvoiceStatus::PARTIALLY_PAID->value => 'Dibayar Sebagian',
InvoiceStatus::OVERDUE->value => 'Jatuh Tempo',
InvoiceStatus::VOID->value => 'Batal',
],
'invoice_type' => [
InvoiceType::STUDENT_INVOICE->value => 'Faktur Pelanggan',
@ -19,20 +16,13 @@ return [
InvoiceType::CREDIT_NOTE->value => 'Nota Kredit',
],
'payment_status' => [
PaymentStatus::PENDING->value => 'Tertunda',
PaymentStatus::SUCCESS->value => 'Berhasil',
PaymentStatus::FAILED->value => 'Gagal',
PaymentStatus::EXPIRED->value => 'Kedaluwarsa',
'pending' => 'Tertunda',
'success' => 'Berhasil',
'failed' => 'Gagal',
'expired' => 'Kedaluwarsa',
],
'payment_direction' => [
PaymentDirection::RECEIPT->value => 'Penerimaan',
PaymentDirection::DISBURSEMENT->value => 'Pengeluaran',
],
'account_classification' => [
AccountClassification::ASSET->value => 'Aset',
AccountClassification::LIABILITY->value => 'Kewajiban',
AccountClassification::EQUITY->value => 'Ekuitas',
AccountClassification::REVENUE->value => 'Pendapatan',
AccountClassification::EXPENSE->value => 'Beban',
'receipt' => 'Penerimaan',
'disbursement' => 'Pengeluaran',
],
];

2
lang/id/resources.php

@ -19,6 +19,8 @@ return [
'fee_type' => 'Jenis Biaya',
'term' => 'Tahun Ajaran',
'chart_of_account' => 'Bagan Akun',
'chartOfAccount' => 'Bagan Akun',
'product' => 'Produk',
'product_mapping' => 'Pemetaan Produk',
'invoice' => 'Tagihan',
'client-api' => 'Titik Akhir Sinkronisasi API',

5
lang/id/ui/menu.php

@ -14,9 +14,12 @@ return [
'admission_schedules' => 'Jadwal Penerimaan',
'channel' => 'Saluran',
'clients' => 'Klien',
'accounting' => 'Akuntansi',
'chart_of_accounts' => 'Bagan Akun',
'billing' => 'Penagihan',
'products' => 'Produk',
'finance' => 'Keuangan',
'invoices' => 'Tagihan',
'chart_of_accounts' => 'Bagan Akun',
'product_mappings' => 'Pemetaan Produk',
'identity' => 'Manajemen Pengguna',
'users' => 'Pengguna',

4
resources/scss/plugin/datatables.scss

@ -223,6 +223,10 @@ table.dataTable {
}
}
.dt-layout-table {
overflow: auto;
}
// Scoped CoreUI Theme High Contrast Shadows
[data-coreui-theme="dark"] {
.dataTables_wrapper .dataTables_processing {

4
resources/views/components/button.blade.php

@ -36,7 +36,7 @@
</template>
<template x-if="{{ $loading }}">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">{{ __('ui/loading') }}</span>
<span class="visually-hidden">{{ __('ui/common.loading') }}</span>
</div>
</template>
@elseif ($attributes->has('wire:loading'))
@ -56,7 +56,7 @@
@if ($attributes->has('wire:target')) wire:target="{{ $attributes->get('wire:target') }}" @endif
>
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">{{ __('ui/loading') }}</span>
<span class="visually-hidden">{{ __('ui/common.loading') }}</span>
</div>
</div>
@else

16
resources/views/components/layouts/nav/sidebar.blade.php

@ -78,6 +78,22 @@
permission="client.viewAny"
/>
<x-layouts.nav.sidebar.nav-title>{{ __('ui/menu.accounting') }}</x-layouts.nav.sidebar.nav-title>
<x-layouts.nav.sidebar.nav-link
:route="route('chart-of-accounts.index')"
icon="tabler-users"
:text="__('ui/menu.chart_of_accounts')"
permission="coa.viewAny"
/>
<x-layouts.nav.sidebar.nav-title>{{ __('ui/menu.billing') }}</x-layouts.nav.sidebar.nav-title>
<x-layouts.nav.sidebar.nav-link
:route="route('products.index')"
icon="tabler-users"
:text="__('ui/menu.products')"
permission="product.viewAny"
/>
<x-layouts.nav.sidebar.nav-title>{{ __('ui/menu.management') }}</x-layouts.nav.sidebar.nav-title>
<x-layouts.nav.sidebar.nav-link
:route="route('roles.index')"

20
resources/views/pages/accounting/chartofaccounts/index.blade.php

@ -0,0 +1,20 @@
<x-layouts.app>
<x-card> {{ $dataTable->table() }} </x-card>
<livewire:pages::system.audit.audit-view-modal
key-name="ulid"
:model="\App\Domains\Accounting\Models\ChartOfAccount::class"
translation="domains/accounting/field.coa."
/>
<livewire:pages::accounting.chartofaccounts.form-modal />
@can('chart-of-account.delete')
<livewire:datatables.delete-action key-name="ulid" :model="\App\Domains\Accounting\Models\ChartOfAccount::class" />
@endcan
@push('page-scripts')
@vite('resources/js/plugin/datatables.js')
{{ $dataTable->scripts(attributes: ['type' => 'module']) }}
@endpush
</x-layouts.app>

56
resources/views/pages/accounting/chartofaccounts/⚡form-modal/form-modal.blade.php

@ -0,0 +1,56 @@
@use(App\Domains\Accounting\Enums\Ledger\AccountBalance)
@use(App\Domains\Accounting\Enums\Ledger\AccountClassification)
@use(App\Domains\System\Enums\LifecycleStatus)
<x-modal id="chartofaccount-form-modal" :title="$this->title" wire:submit="save" wire:loading form livewire>
<div class="d-flex flex-column gap-3">
<x-form.select
name="form.parent"
:label="__('domains/accounting/field.coa.parent')"
wire:model.live="form.parent_id"
:options="$this->chartOfAccounts->pluck('name', 'ulid')"
/>
<x-form.input
name="form.code"
wire:model="form.code"
:disabled="$mode == 'update'"
:label="__('domains/accounting/field.coa.code')"
/>
<x-form.input
name="form.name"
wire:model="form.name"
:disabled="$mode == 'update'"
:label="__('domains/accounting/field.coa.name')"
/>
<x-form.select
name="form.classification"
:label="__('domains/accounting/field.coa.normal_balance')"
wire:model="form.normal_balance"
:disabled="$form->parent_id != null"
:options="AccountBalance::options()"
/>
<x-form.select
name="form.classification"
:label="__('domains/accounting/field.coa.classification')"
wire:model="form.classification"
:disabled="$form->parent_id != null"
:options="AccountClassification::options()"
/>
<x-form.select
name="form.status"
:label="__('domains/accounting/field.coa.status')"
wire:model="form.status"
:disabled="$mode == 'update'"
:options="LifecycleStatus::options()"
/>
</div>
<x-slot:footer>
<x-button type="button" theme="secondary" :label="__('ui/button.cancel')" data-coreui-dismiss="modal"/>
<x-button type="submit" theme="primary" :label="__('ui/button.save')"/>
</x-slot:footer>
</x-modal>

115
resources/views/pages/accounting/chartofaccounts/⚡form-modal/form-modal.php

@ -0,0 +1,115 @@
<?php
use App\Domains\Accounting\Actions\LedgerConfiguration\AdjustAccountClassification;
use App\Domains\Accounting\Actions\LedgerConfiguration\RegisterChartOfAccount;
use App\Domains\Accounting\DTOs\LedgerConfiguration\AdjustAccountClassificationDTO;
use App\Domains\Accounting\DTOs\LedgerConfiguration\RegisterChartOfAccountDTO;
use App\Domains\Accounting\Models\ChartOfAccount;
use App\Livewire\Concerns\WithModal;
use App\Livewire\Concerns\WithToast;
use App\Livewire\Forms\Accounting\ChartOfAccountForm;
use Illuminate\Support\Collection;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked;
use Livewire\Component;
new class extends Component
{
use WithModal;
use WithToast;
public ChartOfAccountForm $form;
#[Locked]
public ?string $id = null;
#[Locked]
public string $mode = 'create';
protected string $resourceName = 'chart_of_account';
public function updating($property, $value): void
{
if ($property === 'form.parent_id') {
$props = explode('.', $property);
$props = implode('->', $props);
$this->{$props} = $value;
$coa = $this->chartOfAccounts->where('ulid', $value)->first();
if ($coa) {
$this->form->code = $coa->code + 1;
$this->form->classification = $coa->classification;
}
}
}
public function show(int|string $id): void
{
$this->id = $id;
$this->mode = 'update';
$coa = $this->coa->except(['ulid', 'created_at', 'updated_at']);
$coa['parent_id'] = $this->chartOfAccounts
->where('id', $coa['parent_id'])
->first()
?->ulid;
$this->form->fill($coa);
}
#[Computed]
public function coa(): ChartOfAccount
{
return $this->id ? ChartOfAccount::where('ulid', $this->id)->first() : new ChartOfAccount;
}
#[Computed]
public function chartOfAccounts(): Collection
{
return ChartOfAccount::query()
->select(['id', 'name', 'code', 'ulid', 'classification'])
->get();
}
public function save(RegisterChartOfAccount $register, AdjustAccountClassification $adjust): void
{
$this->form->validate();
$parentId = null;
if($this->form->parent_id) {
$parentId = $this->chartOfAccounts
->where('ulid', $this->form->parent_id)
->first()
->id;
}
if ($this->mode === 'create') {
$register->execute(new RegisterChartOfAccountDTO(
code: $this->form->code,
name: $this->form->name,
classification: $this->form->classification,
normal_balance: $this->form->normal_balance,
status: $this->form->status,
parentId: $parentId,
));
$this->success(__('ui/crud.success.created', ['resource' => __('resources.chart_of_account')]));
} elseif ($this->mode == 'update') {
$adjust->execute($this->coa, new AdjustAccountClassificationDTO(
code: $this->form->code,
name: $this->form->name,
classification: $this->form->classification,
normal_balance: $this->form->normal_balance,
parentId: $parentId,
));
$this->success(__('ui/crud.success.updated', ['resource' => __('resources.chart_of_account')]));
}
$this->dispatch('hide-chartofaccount-form-modal');
$this->js("LaravelDataTables['chart-of-account-table'].ajax.reload(null, false)");
}
public function hide(): void
{
$this->form->reset();
$this->reset('id', 'mode');
$this->form->reset();
}
};

17
resources/views/pages/billing/products/index.blade.php

@ -0,0 +1,17 @@
<x-layouts.app>
<x-card>
{{ $dataTable->table() }}
</x-card>
<livewire:pages::system.audit.audit-view-modal key-name="id" model="\App\Domains\Billing\Models\Product::class"
translation="domains/billing/field.product."/>
<livewire:pages::billing.products.form-modal />
<livewire:datatables.delete-action key-name="id" :model="\App\Domains\Billing\Models\Product::class" />
@push('page-scripts')
@vite(['resources/js/plugin/datatables.js', 'resources/js/plugin/select2.js'])
{{ $dataTable->scripts(attributes: ['type' => 'module']) }}
@endpush
</x-layouts.app>

27
resources/views/pages/billing/products/⚡form-modal/form-modal.blade.php

@ -0,0 +1,27 @@
<x-modal id="product-form-modal" :title="$this->title" wire:submit="save" wire:loading form livewire>
<div class="d-flex flex-column gap-3">
<x-form.input
name="form.code"
:label="__('domains/billing/field.product.code')"
wire:model="form.code"
/>
<x-form.input
name="form.name"
:label="__('domains/billing/field.product.name')"
wire:model="form.name"
/>
<x-form.select x-select2="{
dropdownParent: $('#product-form-modal')
}"
name="form.coa_id"
:label="__('domains/billing/field.product.coa')"
wire:model="form.coa_id"
:options="$this->coaOptions"
/>
</div>
<x-slot:footer>
<x-button type="button" theme="secondary" :label="__('ui/button.cancel')" data-bs-dismiss="modal" />
<x-button type="submit" theme="primary" :label="__('ui/button.save')" />
</x-slot:footer>
</x-modal>

83
resources/views/pages/billing/products/⚡form-modal/form-modal.php

@ -0,0 +1,83 @@
<?php
use App\Domains\Accounting\Models\ChartOfAccount;
use App\Domains\Billing\Actions\Catalog\ModifyProduct;
use App\Domains\Billing\Actions\Catalog\RegisterProduct;
use App\Domains\Billing\DTOs\Catalog\ModifyProductDTO;
use App\Domains\Billing\DTOs\Catalog\RegisterProductDTO;
use App\Domains\Billing\Models\Product;
use App\Livewire\Concerns\WithModal;
use App\Livewire\Concerns\WithToast;
use App\Livewire\Forms\Billing\ProductForm;
use Illuminate\Support\Collection;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked;
use Livewire\Component;
new class extends Component
{
use WithModal;
use WithToast;
#[Locked]
public ?int $id = null;
#[Locked]
public string $mode = 'create';
public ProductForm $form;
protected string $resourceName = 'product';
public function show(int|string $id): void
{
$this->id = $id;
$this->mode = 'update';
$product = Product::findOrFail($id);
$this->form->fill([
'code' => $product->code,
'name' => $product->name,
'coa_id' => $product->coa_id,
]);
}
#[Computed]
public function coaOptions(): Collection
{
return ChartOfAccount::query()
->select(['id', 'name', 'code'])
->orderBy('code')
->get()
->mapWithKeys(fn ($coa) => [$coa->id => "{$coa->code} - {$coa->name}"]);
}
public function save(RegisterProduct $register, ModifyProduct $modify): void
{
$this->form->validate();
if ($this->mode === 'create') {
$register->execute(new RegisterProductDTO(
name: $this->form->name,
code: $this->form->code,
coaId: $this->form->coa_id,
));
} elseif ($this->mode === 'update') {
$product = Product::findOrFail($this->id);
$modify->execute($product, new ModifyProductDTO(
name: $this->form->name,
code: $this->form->code,
coaId: $this->form->coa_id,
));
}
$this->dispatch('hide-product-form-modal');
$this->js("LaravelDataTables['product-table'].ajax.reload(null, false)");
}
public function hide(): void
{
$this->form->reset();
$this->reset('id', 'mode');
}
};

13
routes/web.php

@ -6,14 +6,13 @@ use App\Http\Controllers\Web\Academic\FacultyController;
use App\Http\Controllers\Web\Academic\StudyProgramController;
use App\Http\Controllers\Web\Academic\TermController;
use App\Http\Controllers\Web\Account\ProfileController;
use App\Http\Controllers\Web\Accounting\ChartOfAccountController;
use App\Http\Controllers\Web\Admission\AcademicProgramController;
use App\Http\Controllers\Web\Admission\AdmissionScheduleController;
use App\Http\Controllers\Web\Admission\AdmissionTrackController;
use App\Http\Controllers\Web\Admission\FeeTypeController;
use App\Http\Controllers\Web\Billing\ProductController;
use App\Http\Controllers\Web\Channel\ClientController;
use App\Http\Controllers\Web\Finance\ChartOfAccountController;
use App\Http\Controllers\Web\Finance\InvoiceController;
use App\Http\Controllers\Web\Finance\ProductMappingController;
use App\Http\Controllers\Web\Identity\RoleController;
use App\Http\Controllers\Web\Identity\UserController;
use Illuminate\Support\Facades\Route;
@ -66,6 +65,14 @@ Route::middleware(['web', 'auth', 'verified', 'seo', 'layouts'])->group(function
->can('client.viewAny')
->name('clients.index');
Route::get('/products', ProductController::class)
->can('product.viewAny')
->name('products.index');
Route::get('/chart-of-accounts', ChartOfAccountController::class)
->can('coa.viewAny')
->name('chart-of-accounts.index');
Route::middleware('password.confirm')->group(function () {
Route::livewire('/system/settings', 'pages::system.settings.setting-list')->name('system-setting.index');
Route::livewire('/system/backups', 'pages::system.backups.backup-list')->name('system-backup.index');

4
tests/Unit/Domains/Account/Enums/GenderOptionTest.php

@ -6,6 +6,6 @@ use Tests\TestCase;
uses(TestCase::class);
test('it has correct labels', function () {
expect(GenderOption::MALE->label())->toBe(__('domains/account/enum.gender-option.male'))
->and(GenderOption::FEMALE->label())->toBe(__('domains/account/enum.gender-option.female'));
expect(GenderOption::MALE->label())->toBe(__('domains/account/enum.gender_option.male'))
->and(GenderOption::FEMALE->label())->toBe(__('domains/account/enum.gender_option.female'));
});

Loading…
Cancel
Save