diff --git a/app/Domains/Accounting/Actions/LedgerConfiguration/AdjustAccountClassification.php b/app/Domains/Accounting/Actions/LedgerConfiguration/AdjustAccountClassification.php new file mode 100644 index 0000000..7201fc2 --- /dev/null +++ b/app/Domains/Accounting/Actions/LedgerConfiguration/AdjustAccountClassification.php @@ -0,0 +1,19 @@ +update([ + 'code' => $dto->code, + 'normal_balance' => $dto->normal_balance, + 'classification' => $dto->classification, + 'parent_id' => $dto->parentId, + ]); + } +} diff --git a/app/Domains/Accounting/Actions/LedgerConfiguration/RegisterChartOfAccount.php b/app/Domains/Accounting/Actions/LedgerConfiguration/RegisterChartOfAccount.php new file mode 100644 index 0000000..f28b42c --- /dev/null +++ b/app/Domains/Accounting/Actions/LedgerConfiguration/RegisterChartOfAccount.php @@ -0,0 +1,20 @@ + $dto->code, + 'name' => $dto->name, + 'classification' => $dto->classification, + 'normal_balance' => $dto->normal_balance, + 'parent_id' => $dto->parentId, + ]); + } +} diff --git a/app/Domains/Accounting/Actions/LedgerConfiguration/SuspendAccountPostings.php b/app/Domains/Accounting/Actions/LedgerConfiguration/SuspendAccountPostings.php new file mode 100644 index 0000000..8701d5b --- /dev/null +++ b/app/Domains/Accounting/Actions/LedgerConfiguration/SuspendAccountPostings.php @@ -0,0 +1,16 @@ +update([ + 'status' => LifecycleStatus::INACTIVE, + ]); + } +} diff --git a/app/Domains/Accounting/DTOs/LedgerConfiguration/AdjustAccountClassificationDTO.php b/app/Domains/Accounting/DTOs/LedgerConfiguration/AdjustAccountClassificationDTO.php new file mode 100644 index 0000000..401f351 --- /dev/null +++ b/app/Domains/Accounting/DTOs/LedgerConfiguration/AdjustAccountClassificationDTO.php @@ -0,0 +1,17 @@ + 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'); + } +} + diff --git a/app/Domains/Accounting/Policies/ChartOfAccountPolicy.php b/app/Domains/Accounting/Policies/ChartOfAccountPolicy.php new file mode 100644 index 0000000..570ff00 --- /dev/null +++ b/app/Domains/Accounting/Policies/ChartOfAccountPolicy.php @@ -0,0 +1,34 @@ +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'); + } +} diff --git a/app/Domains/Accounting/Providers/AccountingServiceProvider.php b/app/Domains/Accounting/Providers/AccountingServiceProvider.php new file mode 100644 index 0000000..ac3264d --- /dev/null +++ b/app/Domains/Accounting/Providers/AccountingServiceProvider.php @@ -0,0 +1,23 @@ +app->register(RelationshipServiceProvider::class); + } + + public function boot(): void + { + $this->registerEvents(); + } +} diff --git a/app/Domains/Accounting/Providers/RelationshipServiceProvider.php b/app/Domains/Accounting/Providers/RelationshipServiceProvider.php new file mode 100644 index 0000000..cda4592 --- /dev/null +++ b/app/Domains/Accounting/Providers/RelationshipServiceProvider.php @@ -0,0 +1,31 @@ + + * $order->belongsTo(Customer::class, 'customer_id') + * ); + */ + public function boot(): void + { + // Register cross-domain relationships below. + } +} diff --git a/app/Domains/Billing/Actions/Catalog/ModifyProduct.php b/app/Domains/Billing/Actions/Catalog/ModifyProduct.php new file mode 100644 index 0000000..c7a96b4 --- /dev/null +++ b/app/Domains/Billing/Actions/Catalog/ModifyProduct.php @@ -0,0 +1,23 @@ +update([ + 'name' => $dto->name, + 'code' => $dto->code, + 'coa_id' => $dto->coaId, + ]); + + return $product->fresh(); + }); + } +} diff --git a/app/Domains/Billing/Actions/Catalog/RegisterProduct.php b/app/Domains/Billing/Actions/Catalog/RegisterProduct.php new file mode 100644 index 0000000..b9f6bfb --- /dev/null +++ b/app/Domains/Billing/Actions/Catalog/RegisterProduct.php @@ -0,0 +1,21 @@ + $dto->name, + 'code' => $dto->code, + 'coa_id' => $dto->coaId, + ]); + }); + } +} diff --git a/app/Domains/Billing/DTOs/Catalog/ModifyProductDTO.php b/app/Domains/Billing/DTOs/Catalog/ModifyProductDTO.php new file mode 100644 index 0000000..9791ac9 --- /dev/null +++ b/app/Domains/Billing/DTOs/Catalog/ModifyProductDTO.php @@ -0,0 +1,12 @@ +belongsTo(ChartOfAccount::class); + return $this->belongsTo(ChartOfAccount::class, 'coa_id'); } public function pricing(): MorphTo diff --git a/app/Domains/Identity/Enums/RoleType.php b/app/Domains/Identity/Enums/RoleType.php index 4e41c67..7dc0a84 100644 --- a/app/Domains/Identity/Enums/RoleType.php +++ b/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]), diff --git a/app/Domains/Payment/Listeners/VirtualAccount/InitializeVirtualAccountForPublishedInvoice.php b/app/Domains/Payment/Listeners/VirtualAccount/InitializeVirtualAccountForPublishedInvoice.php deleted file mode 100644 index 3def441..0000000 --- a/app/Domains/Payment/Listeners/VirtualAccount/InitializeVirtualAccountForPublishedInvoice.php +++ /dev/null @@ -1,11 +0,0 @@ - 'dashboard', + 'domains/accounting/seo.coa.title' => '', + ], + )] + public function __invoke(ChartOfAccountDataTable $dataTable) + { + return $dataTable->render('pages.accounting.chartofaccounts.index'); + } +} + diff --git a/app/Http/Controllers/Web/Billing/ProductController.php b/app/Http/Controllers/Web/Billing/ProductController.php new file mode 100644 index 0000000..975450b --- /dev/null +++ b/app/Http/Controllers/Web/Billing/ProductController.php @@ -0,0 +1,31 @@ + 'dashboard', + 'domains/billing/seo.product.title' => '', + ], + )] + public function __invoke(ProductDataTable $dataTable) + { + return $dataTable->render('pages.billing.products.index'); + } +} diff --git a/app/Http/DataTables/Accounting/ChartOfAccountDataTable.php b/app/Http/DataTables/Accounting/ChartOfAccountDataTable.php new file mode 100644 index 0000000..ef3d4a8 --- /dev/null +++ b/app/Http/DataTables/Accounting/ChartOfAccountDataTable.php @@ -0,0 +1,130 @@ + $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 + */ + 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'); + } +} diff --git a/app/Http/DataTables/Billing/ProductDataTable.php b/app/Http/DataTables/Billing/ProductDataTable.php new file mode 100644 index 0000000..46c8020 --- /dev/null +++ b/app/Http/DataTables/Billing/ProductDataTable.php @@ -0,0 +1,131 @@ + $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 + */ + 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'); + } +} diff --git a/app/Livewire/Forms/Accounting/ChartOfAccountForm.php b/app/Livewire/Forms/Accounting/ChartOfAccountForm.php new file mode 100644 index 0000000..2c0ff8e --- /dev/null +++ b/app/Livewire/Forms/Accounting/ChartOfAccountForm.php @@ -0,0 +1,30 @@ +string('classification'); $table->string('normal_balance'); $table->foreignId('parent_id')->nullable(); + $table->string('status'); $table->timestamps(); }); } diff --git a/lang/en/domains/account/enum.php b/lang/en/domains/account/enum.php index 244282d..c5e3252 100644 --- a/lang/en/domains/account/enum.php +++ b/lang/en/domains/account/enum.php @@ -22,4 +22,9 @@ return [ ], ], ], + 'user_settings' => [ + 'notification' => 'Notification', + 'language' => 'Language', + 'timezone' => 'Timezone', + ], ]; diff --git a/lang/en/domains/accounting/enum.php b/lang/en/domains/accounting/enum.php new file mode 100644 index 0000000..7fdb999 --- /dev/null +++ b/lang/en/domains/accounting/enum.php @@ -0,0 +1,21 @@ + [ + 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', + ], +]; diff --git a/lang/en/domains/accounting/field.php b/lang/en/domains/accounting/field.php new file mode 100644 index 0000000..38d371f --- /dev/null +++ b/lang/en/domains/accounting/field.php @@ -0,0 +1,12 @@ + [ + 'code' => 'Account Code', + 'name' => 'Account Name', + 'normal_balance' => 'Normal Balance', + 'classification' => 'Classification', + 'status' => 'Status', + 'parent' => 'Derived From', + ], +]; diff --git a/lang/en/domains/accounting/seo.php b/lang/en/domains/accounting/seo.php new file mode 100644 index 0000000..60b97b1 --- /dev/null +++ b/lang/en/domains/accounting/seo.php @@ -0,0 +1,9 @@ + [ + 'title' => 'Chart of Accounts (COA) Data', + 'description' => 'Manage chart of accounts for financial records.', + 'keywords' => 'chart of accounts, coa, accounting, finance', + ], +]; diff --git a/lang/en/domains/billing/field.php b/lang/en/domains/billing/field.php new file mode 100644 index 0000000..ca1a271 --- /dev/null +++ b/lang/en/domains/billing/field.php @@ -0,0 +1,9 @@ + [ + 'code' => 'Product Code', + 'name' => 'Product Name', + 'coa' => 'Chart of Account (COA)', + ], +]; diff --git a/lang/en/domains/billing/seo.php b/lang/en/domains/billing/seo.php new file mode 100644 index 0000000..b04c65b --- /dev/null +++ b/lang/en/domains/billing/seo.php @@ -0,0 +1,9 @@ + [ + 'title' => 'Product Data', + 'description' => 'Manage products and their chart of account mappings.', + 'keywords' => 'product, products, billing, finance, coa', + ], +]; diff --git a/lang/en/domains/channel/field.php b/lang/en/domains/channel/field.php index 5565145..b7c885a 100644 --- a/lang/en/domains/channel/field.php +++ b/lang/en/domains/channel/field.php @@ -10,6 +10,7 @@ return [ 'username' => 'Username', 'password' => 'Password', 'token' => 'Token', + 'bearer' => 'Bearer Token', ], 'endpoint_url' => 'Endpoint URL', ], diff --git a/lang/en/resources.php b/lang/en/resources.php index 3038e71..c626dc3 100644 --- a/lang/en/resources.php +++ b/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', diff --git a/lang/en/ui/menu.php b/lang/en/ui/menu.php index 4f39a6c..b2554c5 100644 --- a/lang/en/ui/menu.php +++ b/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', diff --git a/lang/id/domains/account/enum.php b/lang/id/domains/account/enum.php index ae9aa34..9bc68d3 100644 --- a/lang/id/domains/account/enum.php +++ b/lang/id/domains/account/enum.php @@ -22,4 +22,9 @@ return [ ], ], ], + 'user_settings' => [ + 'notification' => 'Notifikasi', + 'language' => 'Bahasa', + 'timezone' => 'Zona Waktu', + ], ]; diff --git a/lang/id/domains/accounting/enum.php b/lang/id/domains/accounting/enum.php new file mode 100644 index 0000000..78d8d0d --- /dev/null +++ b/lang/id/domains/accounting/enum.php @@ -0,0 +1,19 @@ + [ + 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', + ], +]; diff --git a/lang/id/domains/accounting/field.php b/lang/id/domains/accounting/field.php new file mode 100644 index 0000000..9eebfa1 --- /dev/null +++ b/lang/id/domains/accounting/field.php @@ -0,0 +1,12 @@ + [ + 'code' => 'Kode Akun', + 'name' => 'Nama Akun', + 'normal_balance' => 'Saldo Normal', + 'classification' => 'Klasifikasi', + 'status' => 'Status', + 'parent' => 'Turunan Dari', + ], +]; diff --git a/lang/id/domains/accounting/seo.php b/lang/id/domains/accounting/seo.php new file mode 100644 index 0000000..86e7372 --- /dev/null +++ b/lang/id/domains/accounting/seo.php @@ -0,0 +1,9 @@ + [ + 'title' => 'Data Bagan Akun (COA)', + 'description' => 'Kelola bagan akun untuk pencatatan keuangan.', + 'keywords' => 'bagan akun, coa, akuntansi, keuangan', + ], +]; diff --git a/lang/id/domains/billing/field.php b/lang/id/domains/billing/field.php new file mode 100644 index 0000000..cb61c5f --- /dev/null +++ b/lang/id/domains/billing/field.php @@ -0,0 +1,9 @@ + [ + 'code' => 'Kode Produk', + 'name' => 'Nama Produk', + 'coa' => 'Bagan Akun (COA)', + ], +]; diff --git a/lang/id/domains/billing/seo.php b/lang/id/domains/billing/seo.php new file mode 100644 index 0000000..b140412 --- /dev/null +++ b/lang/id/domains/billing/seo.php @@ -0,0 +1,9 @@ + [ + 'title' => 'Data Produk', + 'description' => 'Kelola produk dan pemetaan bagan akun.', + 'keywords' => 'produk, billing, tagihan, coa', + ], +]; diff --git a/lang/id/domains/channel/field.php b/lang/id/domains/channel/field.php index e0ab847..d77c70c 100644 --- a/lang/id/domains/channel/field.php +++ b/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', ], diff --git a/lang/id/domains/channel/messages.php b/lang/id/domains/channel/messages.php new file mode 100644 index 0000000..d3c695c --- /dev/null +++ b/lang/id/domains/channel/messages.php @@ -0,0 +1,8 @@ + [ + 'synced' => 'Klien berhasil disinkronkan.', + 'fail_to_sync' => 'Klien gagal disinkronkan: :error.', + ], +]; diff --git a/lang/id/domains/finance/enum.php b/lang/id/domains/finance/enum.php index 1a4c5b0..c9204af 100644 --- a/lang/id/domains/finance/enum.php +++ b/lang/id/domains/finance/enum.php @@ -1,17 +1,14 @@ [ 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', ], ]; diff --git a/lang/id/resources.php b/lang/id/resources.php index 28db34a..e99005e 100644 --- a/lang/id/resources.php +++ b/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', diff --git a/lang/id/ui/menu.php b/lang/id/ui/menu.php index 9168c74..d501fd1 100644 --- a/lang/id/ui/menu.php +++ b/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', diff --git a/resources/scss/plugin/datatables.scss b/resources/scss/plugin/datatables.scss index 9019747..24321ef 100644 --- a/resources/scss/plugin/datatables.scss +++ b/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 { diff --git a/resources/views/components/button.blade.php b/resources/views/components/button.blade.php index 694ff2b..800b7d9 100644 --- a/resources/views/components/button.blade.php +++ b/resources/views/components/button.blade.php @@ -36,7 +36,7 @@ @elseif ($attributes->has('wire:loading')) @@ -56,7 +56,7 @@ @if ($attributes->has('wire:target')) wire:target="{{ $attributes->get('wire:target') }}" @endif >
- {{ __('ui/loading') }} + {{ __('ui/common.loading') }}
@else diff --git a/resources/views/components/layouts/nav/sidebar.blade.php b/resources/views/components/layouts/nav/sidebar.blade.php index 085746b..afda060 100644 --- a/resources/views/components/layouts/nav/sidebar.blade.php +++ b/resources/views/components/layouts/nav/sidebar.blade.php @@ -78,6 +78,22 @@ permission="client.viewAny" /> + {{ __('ui/menu.accounting') }} + + + {{ __('ui/menu.billing') }} + + {{ __('ui/menu.management') }} + {{ $dataTable->table() }} + + + + + + @can('chart-of-account.delete') + + @endcan + + @push('page-scripts') + @vite('resources/js/plugin/datatables.js') + {{ $dataTable->scripts(attributes: ['type' => 'module']) }} + @endpush + diff --git a/resources/views/pages/accounting/chartofaccounts/⚡form-modal/form-modal.blade.php b/resources/views/pages/accounting/chartofaccounts/⚡form-modal/form-modal.blade.php new file mode 100644 index 0000000..6eca456 --- /dev/null +++ b/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) + +
+ + + + + + + + + + + +
+ + + + + +
diff --git a/resources/views/pages/accounting/chartofaccounts/⚡form-modal/form-modal.php b/resources/views/pages/accounting/chartofaccounts/⚡form-modal/form-modal.php new file mode 100644 index 0000000..2654823 --- /dev/null +++ b/resources/views/pages/accounting/chartofaccounts/⚡form-modal/form-modal.php @@ -0,0 +1,115 @@ +', $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(); + } +}; diff --git a/resources/views/pages/billing/products/index.blade.php b/resources/views/pages/billing/products/index.blade.php new file mode 100644 index 0000000..5c84583 --- /dev/null +++ b/resources/views/pages/billing/products/index.blade.php @@ -0,0 +1,17 @@ + + + {{ $dataTable->table() }} + + + + + + + + + @push('page-scripts') + @vite(['resources/js/plugin/datatables.js', 'resources/js/plugin/select2.js']) + {{ $dataTable->scripts(attributes: ['type' => 'module']) }} + @endpush + diff --git a/resources/views/pages/billing/products/⚡form-modal/form-modal.blade.php b/resources/views/pages/billing/products/⚡form-modal/form-modal.blade.php new file mode 100644 index 0000000..379a4ca --- /dev/null +++ b/resources/views/pages/billing/products/⚡form-modal/form-modal.blade.php @@ -0,0 +1,27 @@ + +
+ + + +
+ + + + + +
diff --git a/resources/views/pages/billing/products/⚡form-modal/form-modal.php b/resources/views/pages/billing/products/⚡form-modal/form-modal.php new file mode 100644 index 0000000..b947dbd --- /dev/null +++ b/resources/views/pages/billing/products/⚡form-modal/form-modal.php @@ -0,0 +1,83 @@ +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'); + } +}; diff --git a/routes/web.php b/routes/web.php index 60df9ca..e3aa012 100644 --- a/routes/web.php +++ b/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'); diff --git a/tests/Unit/Domains/Account/Enums/GenderOptionTest.php b/tests/Unit/Domains/Account/Enums/GenderOptionTest.php index 9636e0d..66ed089 100644 --- a/tests/Unit/Domains/Account/Enums/GenderOptionTest.php +++ b/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')); });