Compare commits

...

2 Commits

Author SHA1 Message Date
Syarif Ubaidillah 7fa427ef65 feat: restructure file paths, improve data mapping, and update naming conventions 2 months ago
Syarif Ubaidillah 33da380c20 feat: enhance `SystemSettingKey` and refactor Alpine plugins 3 months ago
  1. 10
      README.id.md
  2. 10
      README.md
  3. 6
      app/Domains/Academic/Providers/RelationshipServiceProvider.php
  4. 18
      app/Domains/Academic/Queries/Lookup/FacultyLookup.php
  5. 8
      app/Domains/Academic/Queries/Lookup/StudyProgramLookup.php
  6. 18
      app/Domains/Academic/Queries/Lookup/TermLookup.php
  7. 15
      app/Domains/Academic/Queries/Selection/SelectFacultyQuery.php
  8. 15
      app/Domains/Academic/Queries/Selection/SelectTermQuery.php
  9. 1
      app/Domains/Admission/Actions/AdmissionPricing/DefineFeeType.php
  10. 7
      app/Domains/Admission/Actions/AdmissionPricing/SetUpAdmissionFeeRates.php
  11. 1
      app/Domains/Admission/DTOs/AdmissionPricing/FeeTypeDTO.php
  12. 15
      app/Domains/Admission/Events/Pricing/AdmissionFeeRateWasSetUp.php
  13. 108
      app/Domains/Admission/Integration/Mappers/FeeRateDataMapper.php
  14. 2
      app/Domains/Admission/Integration/Mappers/FeeTypeDataMapper.php
  15. 2
      app/Domains/Admission/Models/FeeRate.php
  16. 2
      app/Domains/Admission/Models/FeeType.php
  17. 5
      app/Domains/Admission/Providers/AdmissionServiceProvider.php
  18. 18
      app/Domains/Admission/Queries/Lookup/FeeTypeLookup.php
  19. 13
      app/Domains/Admission/Queries/Selection/SelectFeeTypeQuery.php
  20. 1
      app/Domains/Channel/Enums/Api/ApiType.php
  21. 17
      app/Domains/Channel/Providers/RelationshipServiceProvider.php
  22. 11
      app/Domains/Finance/Actions/DisbursementProcessing/DisburseFunds.php
  23. 11
      app/Domains/Finance/Actions/DisbursementProcessing/RecordVendorBill.php
  24. 18
      app/Domains/Finance/Actions/LedgerConfiguration/AdjustAccountClassification.php
  25. 20
      app/Domains/Finance/Actions/LedgerConfiguration/RegisterChartOfAccount.php
  26. 16
      app/Domains/Finance/Actions/LedgerConfiguration/SuspendAccountPostings.php
  27. 23
      app/Domains/Finance/Actions/PaymentProcessing/InitializeInvoice.php
  28. 11
      app/Domains/Finance/Actions/PaymentProcessing/ProcessIncomingPaymentWebhook.php
  29. 13
      app/Domains/Finance/Actions/ProductConfiguration/ModifyProductMapping.php
  30. 18
      app/Domains/Finance/Actions/ProductConfiguration/RegisterProductMapping.php
  31. 10
      app/Domains/Finance/DTOs/DisbursementProcessing/DisburseFundsDTO.php
  32. 10
      app/Domains/Finance/DTOs/DisbursementProcessing/VendorBillDTO.php
  33. 14
      app/Domains/Finance/DTOs/LedgerConfiguration/AdjustAccountClassificationDTO.php
  34. 17
      app/Domains/Finance/DTOs/LedgerConfiguration/RegisterChartOfAccountDTO.php
  35. 17
      app/Domains/Finance/DTOs/PaymentProcessing/BankWebhookPayloadDTO.php
  36. 10
      app/Domains/Finance/DTOs/PaymentProcessing/Detail/InvoiceDetailDTO.php
  37. 36
      app/Domains/Finance/DTOs/PaymentProcessing/Detail/StudentInvoiceDetailDTO.php
  38. 21
      app/Domains/Finance/DTOs/PaymentProcessing/IssueInvoiceDTO.php
  39. 10
      app/Domains/Finance/DTOs/ProductConfiguration/ModifyProductMappingDTO.php
  40. 12
      app/Domains/Finance/DTOs/ProductConfiguration/RegisterProductMappingDTO.php
  41. 19
      app/Domains/Finance/Enums/AccountClassification.php
  42. 18
      app/Domains/Finance/Enums/InvoiceStatus.php
  43. 49
      app/Domains/Finance/Enums/InvoiceType.php
  44. 16
      app/Domains/Finance/Enums/PaymentDirection.php
  45. 18
      app/Domains/Finance/Enums/PaymentStatus.php
  46. 38
      app/Domains/Finance/Models/ChartOfAccount.php
  47. 55
      app/Domains/Finance/Models/Invoice.php
  48. 42
      app/Domains/Finance/Models/InvoiceReport.php
  49. 49
      app/Domains/Finance/Models/Payment.php
  50. 24
      app/Domains/Finance/Models/ProductMapping.php
  51. 47
      app/Domains/Finance/Models/StudentInvoiceReport.php
  52. 49
      app/Domains/Finance/Policies/ChartOfAccountPolicy.php
  53. 49
      app/Domains/Finance/Policies/InvoicePolicy.php
  54. 49
      app/Domains/Finance/Policies/InvoiceReportPolicy.php
  55. 49
      app/Domains/Finance/Policies/PaymentPolicy.php
  56. 49
      app/Domains/Finance/Policies/ProductMappingPolicy.php
  57. 49
      app/Domains/Finance/Policies/StudentInvoiceReportPolicy.php
  58. 23
      app/Domains/Finance/Providers/FinanceServiceProvider.php
  59. 31
      app/Domains/Finance/Providers/RelationshipServiceProvider.php
  60. 79
      app/Domains/Finance/Schemas/Invoice/StudentInvoiceSchema.php
  61. 25
      app/Domains/Finance/Support/Schemas/InvoiceSchema.php
  62. 4
      app/Domains/System/Casts/MoneyCurrency.php
  63. 11
      app/Domains/System/Enums/SystemSettingKey.php
  64. 3
      app/Domains/System/Models/SystemSettings.php
  65. 2
      app/Domains/System/Support/ValueObjects/Money.php
  66. 6
      app/Http/Controllers/Api/V1/Lookup/RoleLookupController.php
  67. 21
      app/Http/Controllers/Api/V1/Lookup/TermLookupController.php
  68. 32
      app/Http/Controllers/Web/Finance/ChartOfAccountController.php
  69. 32
      app/Http/Controllers/Web/Finance/InvoiceController.php
  70. 32
      app/Http/Controllers/Web/Finance/ProductMappingController.php
  71. 34
      app/Http/DataTables/Admission/AdmissionScheduleDataTable.php
  72. 143
      app/Http/DataTables/Finance/ChartOfAccountDataTable.php
  73. 115
      app/Http/DataTables/Finance/InvoiceDataTable.php
  74. 137
      app/Http/DataTables/Finance/ProductMappingDataTable.php
  75. 2
      app/Http/DataTables/Identity/RoleDataTable.php
  76. 4
      app/Livewire/Forms/Finance/InvoiceForm.php
  77. 2
      app/Livewire/Forms/Finance/ProductMappingForm.php
  78. 1
      database/migrations/2026_07_06_031009_create_fee_types_table.php
  79. 2
      database/migrations/2026_07_13_045042_create_product_mappings_table.php
  80. 3
      lang/en/domains/academic/field.php
  81. 5
      lang/en/domains/admission/field.php
  82. 10
      lang/en/domains/admission/label.php
  83. 1
      lang/en/domains/channel/field.php
  84. 38
      lang/en/domains/finance/enum.php
  85. 50
      lang/en/domains/finance/field.php
  86. 19
      lang/en/domains/finance/seo.php
  87. 1
      lang/en/domains/system/enum.php
  88. 3
      lang/en/ui/button.php
  89. 7
      lang/en/ui/placeholder.php
  90. 3
      lang/id/domains/academic/field.php
  91. 5
      lang/id/domains/admission/field.php
  92. 9
      lang/id/domains/admission/label.php
  93. 1
      lang/id/domains/channel/field.php
  94. 1
      lang/id/domains/system/enum.php
  95. 3
      lang/id/ui/button.php
  96. 7
      lang/id/ui/placeholder.php
  97. 215
      resources/js/alpine/chart.js
  98. 104
      resources/js/alpine/quill.js
  99. 36
      resources/js/alpine/select2.js
  100. 40
      resources/js/alpinejs.js

10
README.id.md

@ -561,11 +561,11 @@ Kedua kelas Mailable berada pada **domain System**, bukan di namespace utama `Ap
| File | Kunci (Key) | Tujuan |
| --- | --- | --- |
| `lang/{locale}/ui.php` | `ui.excel.import.file_label` | Label unggahan FilePond di dalam modal impor. |
| `lang/{locale}/ui.php` | `ui.excel.import.success` | Pesan Toast muncul setelah impor masuk antrean. |
| `lang/{locale}/ui.php` | `ui.excel.export.success` | Pesan Toast muncul setelah ekspor masuk antrean. |
| `lang/{locale}/domains/system.php` | `notifications.excel.import_email.*` | Isi (body) email untuk pemberitahuan selesai impor. |
| `lang/{locale}/domains/system.php` | `notifications.excel.export_email.*` | Isi (body) email untuk pemberitahuan ekspor sudah siap. |
| `lang/{locale}/ui/excel.php` | `ui/excel.import.file_label` | Label unggahan FilePond di dalam modal impor. |
| `lang/{locale}/ui/excel.php` | `ui/excel.import.success` | Pesan Toast muncul setelah impor masuk antrean. |
| `lang/{locale}/ui/excel.php` | `ui/excel.export.success` | Pesan Toast muncul setelah ekspor masuk antrean. |
| `lang/{locale}/domains/system/notifications.php` | `domains/system/notifications.excel.import_email.*` | Isi (body) email untuk pemberitahuan selesai impor. |
| `lang/{locale}/domains/system/notifications.php` | `domains/system/notifications.excel.export_email.*` | Isi (body) email untuk pemberitahuan ekspor sudah siap. |
---

10
README.md

@ -676,11 +676,11 @@ Both Mailable classes live in the **System domain**, not the root `App\Mail\` na
| File | Key | Purpose |
| --- | --- | --- |
| `lang/{locale}/ui.php` | `ui.excel.import.file_label` | FilePond upload label inside the import modal. |
| `lang/{locale}/ui.php` | `ui.excel.import.success` | Toast shown after import is queued. |
| `lang/{locale}/ui.php` | `ui.excel.export.success` | Toast shown after export is queued. |
| `lang/{locale}/domains/system.php` | `notifications.excel.import_email.*` | Email body for the import completion notification. |
| `lang/{locale}/domains/system.php` | `notifications.excel.export_email.*` | Email body for the export ready notification. |
| `lang/{locale}/ui/excel.php` | `ui/excel.import.file_label` | FilePond upload label inside the import modal. |
| `lang/{locale}/ui/excel.php` | `ui/excel.import.success` | Toast shown after import is queued. |
| `lang/{locale}/ui/excel.php` | `ui/excel.export.success` | Toast shown after export is queued. |
| `lang/{locale}/domains/system/notifications.php` | `domains/system/notifications.excel.import_email.*` | Email body for the import completion notification. |
| `lang/{locale}/domains/system/notifications.php` | `domains/system/notifications.excel.export_email.*` | Email body for the export ready notification. |
---

6
app/Domains/Academic/Providers/RelationshipServiceProvider.php

@ -35,17 +35,17 @@ class RelationshipServiceProvider extends ServiceProvider
related: Term::class,
foreignKey: 'term_id'
));
Term::resolveRelationUsing('admission_schedules', fn (Term $term) => $term->hasMany(
Term::resolveRelationUsing('admissionSchedules', fn (Term $term) => $term->hasMany(
related: AdmissionSchedule::class,
foreignKey: 'term_id',
localKey: 'id'
));
FeeRate::resolveRelationUsing('study_program', fn (FeeRate $feeRate) => $feeRate->belongsTo(
FeeRate::resolveRelationUsing('studyProgram', fn (FeeRate $feeRate) => $feeRate->belongsTo(
related: StudyProgram::class,
foreignKey: 'study_program_id'
));
StudyProgram::resolveRelationUsing('fee_rates', fn (StudyProgram $studyProgram) => $studyProgram->hasMany(
StudyProgram::resolveRelationUsing('feeRates', fn (StudyProgram $studyProgram) => $studyProgram->hasMany(
related: FeeRate::class,
foreignKey: 'study_program_id',
localKey: 'id'

18
app/Domains/Academic/Queries/Lookup/FacultyLookup.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Academic\Queries\Lookup;
use App\Domains\Academic\Models\Faculty;
use Illuminate\Support\Collection;
class FacultyLookup
{
public static function fetch(): Collection
{
return once(fn () => Faculty::query()
->select(['id', 'name'])
->latest()
->get()
->pluck('name', 'id'));
}
}

8
app/Domains/Academic/Queries/Selection/SelectStudyProgramQuery.php → app/Domains/Academic/Queries/Lookup/StudyProgramLookup.php

@ -1,11 +1,11 @@
<?php
namespace App\Domains\Academic\Queries\Selection;
namespace App\Domains\Academic\Queries\Lookup;
use App\Domains\Academic\Models\StudyProgram;
use Illuminate\Support\Collection;
class SelectStudyProgramQuery
class StudyProgramLookup
{
public static function fetch(array $context = []): Collection
{
@ -13,8 +13,10 @@ class SelectStudyProgramQuery
return new Collection;
}
return once(fn () => StudyProgram::select('id', 'name')
return once(fn () => StudyProgram::query()
->select('id', 'name')
->where('faculty_id', $context['faculty'])
->latest()
->get()
->pluck('name', 'id'));
}

18
app/Domains/Academic/Queries/Lookup/TermLookup.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Academic\Queries\Lookup;
use App\Domains\Academic\Models\Term;
use Illuminate\Support\Collection;
class TermLookup
{
public static function fetch($search = null): Collection
{
return once(fn () => Term::query()
->select(['id', 'name'])
->when($search, fn ($query) => $query->where('name', 'like', "%{$search}%"))
->latest('date_start')
->pluck('name', 'name'));
}
}

15
app/Domains/Academic/Queries/Selection/SelectFacultyQuery.php

@ -1,15 +0,0 @@
<?php
namespace App\Domains\Academic\Queries\Selection;
use App\Domains\Academic\Models\Faculty;
use Illuminate\Support\Collection;
class SelectFacultyQuery
{
public static function fetch(): Collection
{
return once(fn () => Faculty::all(['id', 'name'])
->pluck('name', 'id'));
}
}

15
app/Domains/Academic/Queries/Selection/SelectTermQuery.php

@ -1,15 +0,0 @@
<?php
namespace App\Domains\Academic\Queries\Selection;
use App\Domains\Academic\Models\Term;
use Illuminate\Support\Collection;
class SelectTermQuery
{
public static function fetch(): Collection
{
return once(fn () => Term::all(['id', 'name'])
->pluck('name', 'id'));
}
}

1
app/Domains/Admission/Actions/AdmissionPricing/DefineFeeType.php

@ -11,6 +11,7 @@ class DefineFeeType
{
FeeType::create([
'name' => $dto->name,
'code' => $dto->code,
'billing_cycle' => $dto->billingCycle,
]);
}

7
app/Domains/Admission/Actions/AdmissionPricing/SetUpAdmissionFeeRates.php

@ -3,13 +3,14 @@
namespace App\Domains\Admission\Actions\AdmissionPricing;
use App\Domains\Admission\DTOs\AdmissionPricing\AdmissionFeeRatesDTO;
use App\Domains\Admission\Events\Pricing\AdmissionFeeRateWasSetUp;
use App\Domains\Admission\Models\FeeRate;
class SetUpAdmissionFeeRates
{
public function execute(AdmissionFeeRatesDTO $dto): void
{
FeeRate::updateOrCreate([
$rate = FeeRate::updateOrCreate([
'academic_program_id' => $dto->academicProgramId,
'admission_schedule_id' => $dto->admissionScheduleId,
'study_program_id' => $dto->studyProgramId,
@ -17,5 +18,9 @@ class SetUpAdmissionFeeRates
], [
'amount' => $dto->amount,
]);
$name = $rate->generateProductName();
AdmissionFeeRateWasSetUp::dispatch(productName: $name);
}
}

1
app/Domains/Admission/DTOs/AdmissionPricing/FeeTypeDTO.php

@ -8,6 +8,7 @@ readonly class FeeTypeDTO
{
public function __construct(
public string $name,
public string $code,
public BillingCycle $billingCycle
) {}
}

15
app/Domains/Admission/Events/Pricing/AdmissionFeeRateWasSetUp.php

@ -0,0 +1,15 @@
<?php
namespace App\Domains\Admission\Events\Pricing;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class AdmissionFeeRateWasSetUp
{
use Dispatchable, SerializesModels;
public function __construct(
public string $productName
) {}
}

108
app/Domains/Admission/Integration/Mappers/FeeRateDataMapper.php

@ -0,0 +1,108 @@
<?php
namespace App\Domains\Admission\Integration\Mappers;
use App\Domains\Academic\Models\StudyProgram;
use App\Domains\Admission\Actions\AdmissionPricing\SetUpAdmissionFeeRates;
use App\Domains\Admission\DTOs\AdmissionPricing\AdmissionFeeRatesDTO;
use App\Domains\Admission\Models\AcademicProgram;
use App\Domains\Admission\Models\AdmissionSchedule;
use App\Domains\Admission\Models\FeeRate;
use App\Domains\Admission\Models\FeeType;
use Illuminate\Database\Eloquent\Model;
use App\Domains\System\Support\Integration\DataPayloadMapper;
class FeeRateDataMapper implements DataPayloadMapper
{
private static array $cache = [];
public function __construct(
protected SetUpAdmissionFeeRates $setUpAdmissionFeeRates,
)
{
// Inject required Domain and Cross-Domain Actions via constructor composition
}
public function getLookupKey(): array
{
// Return the unique string key used to identify existing records (e.g., 'email')
return ['admission_schedule_id', 'fee_type_id', 'academic_program_id'];
}
public function transform(array $rawData): array
{
if(!isset($rawData['jalur'])){
dd($rawData);
}
// Normalize incoming data array formats into an internal domain-safe layout
return [
'admission_schedule_id' => $this->resolveAdmissionScheduleId($rawData['jalur'], $rawData['angkatan']),
'academic_program_id' => $this->resolveAcademicProgramId($rawData['kode_Skuliah']),
'study_program_id' => $this->resolveStudyProgramId($rawData['kode_prodi']),
'fee_type_id'=> $this->resolveFeeTypeId($rawData['kode_Jtagihan']),
'amount' => $rawData['nominal_TRFtagihan'],
];
}
public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void
{
$this->setUpAdmissionFeeRates->execute(new AdmissionFeeRatesDTO(
academicProgramId: $payload['academic_program_id'],
admissionScheduleId: $payload['admission_schedule_id'],
studyProgramId: $payload['study_program_id'],
feeTypeId: $payload['fee_type_id'],
amount: $payload['amount'],
));
}
/**
* @return class-string
*/
public function getModelClass(): string
{
return FeeRate::class;
}
public function resolveAdmissionScheduleId(string $admissionTrackSlug, int $yearStudy): int {
$key = hex2bin(md5("{$admissionTrackSlug}-{$yearStudy}") );
if (!isset(self::$cache['admission_schedule'][$key])) {
self::$cache['admission_schedule'][$key] = AdmissionSchedule::query()
->whereHas(
'admissionTrack',
fn($builder) => $builder->where('slug', str($admissionTrackSlug)->slug())
)->whereHas(
'term',
fn($builder) => $builder->where('year_study', $yearStudy)
)->first()?->id;
}
return self::$cache['admission_schedule'][$key];
}
public function resolveStudyProgramId($code): int {
if (!isset(self::$cache['study_program'][$code])) {
self::$cache['study_program'][$code] = StudyProgram::where('code', $code)->first()?->id;
}
return self::$cache['study_program'][$code];
}
public function resolveFeeTypeId(string $code): int {
$key = hex2bin(md5($code));
if (!isset(self::$cache['fee_type'][$key])) {
self::$cache['fee_type'][$key] = FeeType::where('code', $code)->first()?->id;
}
return self::$cache['fee_type'][$key];
}
public function resolveAcademicProgramId(string $code): int {
$key = hex2bin(md5($code));
if (!isset(self::$cache['academic_program'][$key])) {
self::$cache['academic_program'][$key] = AcademicProgram::where('code', $code)->first()?->id;
}
return self::$cache['academic_program'][$key];
}
}

2
app/Domains/Admission/Integration/Mappers/FeeTypeDataMapper.php

@ -29,6 +29,7 @@ class FeeTypeDataMapper implements DataPayloadMapper
// Normalize incoming data array formats into an internal domain-safe layout
return [
'name' => $rawData['nama_Jtagihan'],
'code' => $rawData['kode_Jtagihan'],
'billing_cycle' => $rawData['periodesasi_Jtagihan'],
];
}
@ -41,6 +42,7 @@ class FeeTypeDataMapper implements DataPayloadMapper
if(!$existingModel) {
$this->defineFeeType->execute(new FeeTypeDTO(
name: $payload['name'],
code: $payload['code'],
billingCycle: BillingCycle::tryFromPMB($payload['billing_cycle']),
));
}

2
app/Domains/Admission/Models/FeeRate.php

@ -3,7 +3,7 @@
namespace App\Domains\Admission\Models;
use App\Domains\Admission\Policies\FeeRatePolicy;
use App\Domains\Finance\Casts\MoneyCurrency;
use App\Domains\System\Casts\MoneyCurrency;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Admission\FeeRateFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;

2
app/Domains/Admission/Models/FeeType.php

@ -12,7 +12,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['name', 'billing_cycle'])]
#[Fillable(['name', 'billing_cycle' ,'code'])]
#[UsePolicy(FeeTypePolicy::class)]
class FeeType extends Model
{

5
app/Domains/Admission/Providers/AdmissionServiceProvider.php

@ -5,6 +5,7 @@ namespace App\Domains\Admission\Providers;
use App\Domains\Admission\Integration\Mappers\AcademicProgramDataMapper;
use App\Domains\Admission\Integration\Mappers\AdmissionScheduleDataMapper;
use App\Domains\Admission\Integration\Mappers\AdmissionTrackDataMapper;
use App\Domains\Admission\Integration\Mappers\FeeRateDataMapper;
use App\Domains\Admission\Integration\Mappers\FeeTypeDataMapper;
use App\Domains\Channel\Enums\Api\ApiType;
use App\Domains\Channel\Support\Mapper\ApiMapperRegistry;
@ -35,6 +36,10 @@ class AdmissionServiceProvider extends ServiceProvider
type: ApiType::FEE_TYPE,
mapperClass: FeeTypeDataMapper::class,
);
ApiMapperRegistry::register(
type: ApiType::FEE_RATE,
mapperClass: FeeRateDataMapper::class,
);
$this->app->register(RelationshipServiceProvider::class);
}

18
app/Domains/Admission/Queries/Lookup/FeeTypeLookup.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Admission\Queries\Lookup;
use App\Domains\Admission\Models\FeeType;
use Illuminate\Support\Collection;
class FeeTypeLookup
{
public static function fetch(): Collection
{
return once(fn () => FeeType::query()
->select(['id', 'name'])
->latest()
->get()
->pluck('name', 'id'));
}
}

13
app/Domains/Admission/Queries/Selection/SelectFeeTypeQuery.php

@ -1,13 +0,0 @@
<?php
namespace App\Domains\Admission\Queries\Selection;
use Illuminate\Database\Eloquent\Collection;
class SelectFeeTypeQuery
{
public function fetch(): Collection
{
return new Collection;
}
}

1
app/Domains/Channel/Enums/Api/ApiType.php

@ -18,6 +18,7 @@ enum ApiType: string implements HasLabel
case ADMISSION_SCHEDULE = 'admission_schedule';
case ACADEMIC_PROGRAM = 'academic_program';
case FEE_TYPE = 'fee_type';
case FEE_RATE = 'fee_rate';
public function mapper(): DataPayloadMapper
{

17
app/Domains/Channel/Providers/RelationshipServiceProvider.php

@ -30,22 +30,5 @@ class RelationshipServiceProvider extends ServiceProvider
public function boot(): void
{
// Register cross-domain relationships below.
Client::resolveRelationUsing('transaction', fn (Client $client) => $client->hasMany(
related: Invoice::class,
foreignKey: 'client_id'
));
Invoice::resolveRelationUsing('client', fn (Invoice $transaction) => $transaction->belongsTo(
related: Client::class,
foreignKey: 'client_id'
));
Client::resolveRelationUsing('product_mapping', fn (Client $client) => $client->hasMany(
related: ProductMapping::class,
foreignKey: 'client_id'
));
ProductMapping::resolveRelationUsing('client', fn (ProductMapping $productMapping) => $productMapping->belongsTo(
related: Client::class,
foreignKey: 'client_id'
));
}
}

11
app/Domains/Finance/Actions/DisbursementProcessing/DisburseFunds.php

@ -1,11 +0,0 @@
<?php
namespace App\Domains\Finance\Actions\DisbursementProcessing;
class DisburseFunds
{
public function execute(): void
{
//
}
}

11
app/Domains/Finance/Actions/DisbursementProcessing/RecordVendorBill.php

@ -1,11 +0,0 @@
<?php
namespace App\Domains\Finance\Actions\DisbursementProcessing;
class RecordVendorBill
{
public function execute(): void
{
//
}
}

18
app/Domains/Finance/Actions/LedgerConfiguration/AdjustAccountClassification.php

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

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

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

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

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

23
app/Domains/Finance/Actions/PaymentProcessing/InitializeInvoice.php

@ -1,23 +0,0 @@
<?php
namespace App\Domains\Finance\Actions\PaymentProcessing;
use App\Domains\Finance\DTOs\PaymentProcessing\IssueInvoiceDTO;
use App\Domains\Finance\Models\Invoice;
class InitializeInvoice
{
public function execute(IssueInvoiceDTO $dto): void
{
Invoice::create([
'name' => $dto->name,
'detail' => $dto->detail->toArray(),
'client_ref_id' => $dto->clientRefId,
'type' => $dto->type,
'amount' => $dto->amount,
'status' => $dto->status,
'client_id' => $dto->clientId,
'chart_of_account_id' => $dto->coaId,
]);
}
}

11
app/Domains/Finance/Actions/PaymentProcessing/ProcessIncomingPaymentWebhook.php

@ -1,11 +0,0 @@
<?php
namespace App\Domains\Finance\Actions\PaymentProcessing;
class ProcessIncomingPaymentWebhook
{
public function execute(): void
{
//
}
}

13
app/Domains/Finance/Actions/ProductConfiguration/ModifyProductMapping.php

@ -1,13 +0,0 @@
<?php
namespace App\Domains\Finance\Actions\ProductConfiguration;
use App\Domains\Finance\Models\ProductMapping;
class ModifyProductMapping
{
public function execute(ProductMapping $productMapping, string|int $coaId): void
{
$productMapping->update(['chart_of_account_id' => $coaId]);
}
}

18
app/Domains/Finance/Actions/ProductConfiguration/RegisterProductMapping.php

@ -1,18 +0,0 @@
<?php
namespace App\Domains\Finance\Actions\ProductConfiguration;
use App\Domains\Finance\DTOs\ProductConfiguration\RegisterProductMappingDTO;
use App\Domains\Finance\Models\ProductMapping;
class RegisterProductMapping
{
public function execute(RegisterProductMappingDTO $dto): void
{
ProductMapping::create([
'name' => $dto->name,
'code' => $dto->code,
'chart_of_account_id' => $dto->coaId,
]);
}
}

10
app/Domains/Finance/DTOs/DisbursementProcessing/DisburseFundsDTO.php

@ -1,10 +0,0 @@
<?php
namespace App\Domains\Finance\DTOs\DisbursementProcessing;
readonly class DisburseFundsDTO
{
public function __construct(
//
) {}
}

10
app/Domains/Finance/DTOs/DisbursementProcessing/VendorBillDTO.php

@ -1,10 +0,0 @@
<?php
namespace App\Domains\Finance\DTOs\DisbursementProcessing;
readonly class VendorBillDTO
{
public function __construct(
//
) {}
}

14
app/Domains/Finance/DTOs/LedgerConfiguration/AdjustAccountClassificationDTO.php

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

17
app/Domains/Finance/DTOs/LedgerConfiguration/RegisterChartOfAccountDTO.php

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

17
app/Domains/Finance/DTOs/PaymentProcessing/BankWebhookPayloadDTO.php

@ -1,17 +0,0 @@
<?php
namespace App\Domains\Finance\DTOs\PaymentProcessing;
use Carbon\CarbonImmutable;
readonly class BankWebhookPayloadDTO
{
public function __construct(
public int $virtualAccount,
public string $bankRefId,
public CarbonImmutable $validUntil,
public float $amount,
public string $status,
public string $transactionId
) {}
}

10
app/Domains/Finance/DTOs/PaymentProcessing/Detail/InvoiceDetailDTO.php

@ -1,10 +0,0 @@
<?php
namespace App\Domains\Finance\DTOs\PaymentProcessing\Detail;
interface InvoiceDetailDTO
{
public function toArray(): array;
public static function fromArray(array $data): self;
}

36
app/Domains/Finance/DTOs/PaymentProcessing/Detail/StudentInvoiceDetailDTO.php

@ -1,36 +0,0 @@
<?php
namespace App\Domains\Finance\DTOs\PaymentProcessing\Detail;
class StudentInvoiceDetailDTO implements InvoiceDetailDTO
{
public function __construct(
public string $studentId,
public string $studentName,
public string $faculty,
public string $studyProgram,
public string $term,
) {}
public function toArray(): array
{
return [
'student_id' => $this->studentId,
'student_name' => $this->studentName,
'faculty' => $this->faculty,
'study_program' => $this->studyProgram,
'term' => $this->term,
];
}
public static function fromArray(array $data): self
{
return new self(
studentId: $data['student_id'],
studentName: $data['student_name'],
faculty: $data['faculty'],
studyProgram: $data['study_program'],
term: $data['term'],
);
}
}

21
app/Domains/Finance/DTOs/PaymentProcessing/IssueInvoiceDTO.php

@ -1,21 +0,0 @@
<?php
namespace App\Domains\Finance\DTOs\PaymentProcessing;
use App\Domains\Finance\DTOs\PaymentProcessing\Detail\InvoiceDetailDTO;
use App\Domains\Finance\Enums\InvoiceStatus;
use App\Domains\Finance\Enums\InvoiceType;
readonly class IssueInvoiceDTO
{
public function __construct(
public string $name,
public string $clientRefId,
public InvoiceDetailDTO $detail,
public InvoiceType $type,
public InvoiceStatus $status,
public int $amount,
public string $coaId,
public string $clientId
) {}
}

10
app/Domains/Finance/DTOs/ProductConfiguration/ModifyProductMappingDTO.php

@ -1,10 +0,0 @@
<?php
namespace App\Domains\Finance\DTOs\ProductConfiguration;
readonly class ModifyProductMappingDTO
{
public function __construct(
//
) {}
}

12
app/Domains/Finance/DTOs/ProductConfiguration/RegisterProductMappingDTO.php

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

19
app/Domains/Finance/Enums/AccountClassification.php

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

18
app/Domains/Finance/Enums/InvoiceStatus.php

@ -1,18 +0,0 @@
<?php
namespace App\Domains\Finance\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
enum InvoiceStatus: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case PAID = 'paid';
case PENDING = 'pending';
case PARTIALLY_PAID = 'partially_paid';
case OVERDUE = 'overdue';
}

49
app/Domains/Finance/Enums/InvoiceType.php

@ -1,49 +0,0 @@
<?php
namespace App\Domains\Finance\Enums;
use App\Domains\Finance\DTOs\PaymentProcessing\Detail\InvoiceDetailDTO;
use App\Domains\Finance\DTOs\PaymentProcessing\Detail\StudentInvoiceDetailDTO;
use App\Domains\Finance\Schemas\Invoice\StudentInvoiceSchema;
use App\Domains\Finance\Support\Schemas\InvoiceSchema;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
use App\UI\Enums\Contracts\HasSchema;
use App\UI\Support\Schemas\InputSchemaField;
enum InvoiceType: string implements HasLabel, HasSchema
{
use HasPredicateMethod;
use InteractsWithLabels;
case STUDENT_INVOICE = 'student_invoice';
case VENDOR_BILL = 'vendor_bill';
case CREDIT_NOTE = 'credit_note';
public function schema(): InvoiceSchema
{
return match ($this) {
self::STUDENT_INVOICE => StudentInvoiceSchema::make(),
default => InvoiceSchema::make()->withCoaVisibility(),
};
}
public function dtoTransform(array $data): InvoiceDetailDTO
{
return match ($this) {
self::STUDENT_INVOICE => StudentInvoiceDetailDTO::fromArray($data),
default => false
};
}
/**
* Resolve all field schemas, keyed by their input keys.
*
* @return array<string, InputSchemaField>
*/
public function getSchema(): array
{
return $this->schema()->getSchema();
}
}

16
app/Domains/Finance/Enums/PaymentDirection.php

@ -1,16 +0,0 @@
<?php
namespace App\Domains\Finance\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
enum PaymentDirection: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case RECEIPT = 'receipt';
case DISBURSEMENT = 'disbursement';
}

18
app/Domains/Finance/Enums/PaymentStatus.php

@ -1,18 +0,0 @@
<?php
namespace App\Domains\Finance\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
enum PaymentStatus: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case PENDING = 'pending';
case SUCCESS = 'success';
case FAILED = 'failed';
case EXPIRED = 'expired';
}

38
app/Domains/Finance/Models/ChartOfAccount.php

@ -1,38 +0,0 @@
<?php
namespace App\Domains\Finance\Models;
use App\Domains\Finance\Enums\AccountClassification;
use App\Domains\Finance\Policies\ChartOfAccountPolicy;
use App\Domains\System\Enums\LifecycleStatus;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Finance\ChartOfAccountFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
'code',
'name',
'classification',
'status',
'parent_id',
])]
#[UsePolicy(ChartOfAccountPolicy::class)]
class ChartOfAccount extends Model
{
use HasFactory;
use HasPublicUlid;
protected $casts = [
'classification' => AccountClassification::class,
'status' => LifecycleStatus::class,
];
protected static function newFactory(): Factory
{
return ChartOfAccountFactory::new();
}
}

55
app/Domains/Finance/Models/Invoice.php

@ -1,55 +0,0 @@
<?php
namespace App\Domains\Finance\Models;
use App\Domains\Finance\Casts\MoneyCurrency;
use App\Domains\Finance\Enums\InvoiceStatus;
use App\Domains\Finance\Enums\InvoiceType;
use App\Domains\Finance\Policies\InvoicePolicy;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Finance\InvoiceFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'name',
'detail',
'client_ref_id',
'type',
'amount',
'status',
'client_id',
'chart_of_account_id',
])]
#[UsePolicy(InvoicePolicy::class)]
class Invoice extends Model
{
use HasFactory;
use HasPublicUlid;
protected $casts = [
'amount' => MoneyCurrency::class,
'type' => InvoiceType::class,
'status' => InvoiceStatus::class,
];
protected static function newFactory(): Factory
{
return InvoiceFactory::new();
}
public function chartOfAccount(): BelongsTo
{
return $this->belongsTo(ChartOfAccount::class);
}
public function payments(): HasMany
{
return $this->hasMany(Payment::class);
}
}

42
app/Domains/Finance/Models/InvoiceReport.php

@ -1,42 +0,0 @@
<?php
namespace App\Domains\Finance\Models;
use App\Domains\Finance\Casts\MoneyCurrency;
use App\Domains\Finance\Enums\InvoiceStatus;
use App\Domains\Finance\Policies\InvoiceReportPolicy;
use BadMethodCallException;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
'invoice_id',
'invoice_name',
'customer_id',
'customer_name',
'amount',
'paid_amount',
'issued_at',
'paid_at',
'status',
])]
#[UsePolicy(InvoiceReportPolicy::class)]
class InvoiceReport extends Model
{
public static function boot(): void
{
self::boot();
static::creating(fn (InvoiceReport $invoiceReport) => throw new BadMethodCallException('Cannot create invoice report from here, use event instead.'));
static::deleting(fn (InvoiceReport $invoiceReport) => throw new BadMethodCallException('Cannot delete invoice report.'));
static::updating(fn (InvoiceReport $invoiceReport) => throw new BadMethodCallException('Cannot update invoice report.'));
}
protected $casts = [
'amount' => MoneyCurrency::class,
'paid_amount' => MoneyCurrency::class,
'issued_at' => 'datetime',
'paid_at' => 'datetime',
'status' => InvoiceStatus::class,
];
}

49
app/Domains/Finance/Models/Payment.php

@ -1,49 +0,0 @@
<?php
namespace App\Domains\Finance\Models;
use App\Domains\Finance\Casts\MoneyCurrency;
use App\Domains\Finance\Enums\PaymentDirection;
use App\Domains\Finance\Enums\PaymentStatus;
use App\Domains\Finance\Policies\PaymentPolicy;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Finance\PaymentFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'virtual_account',
'bank_ref_id',
'amount',
'valid_until',
'direction',
'status',
'transaction_id',
])]
#[UsePolicy(PaymentPolicy::class)]
class Payment extends Model
{
use HasFactory;
use HasPublicUlid;
protected $casts = [
'amount' => MoneyCurrency::class,
'valid_until' => 'datetime_immutable',
'direction' => PaymentDirection::class,
'status' => PaymentStatus::class,
];
protected static function newFactory(): Factory
{
return PaymentFactory::new();
}
public function transaction(): BelongsTo
{
return $this->belongsTo(Invoice::class);
}
}

24
app/Domains/Finance/Models/ProductMapping.php

@ -1,24 +0,0 @@
<?php
namespace App\Domains\Finance\Models;
use App\Domains\Finance\Policies\ProductMappingPolicy;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'code',
'name',
'chart_of_account_id',
'client_id',
])]
#[UsePolicy(ProductMappingPolicy::class)]
class ProductMapping extends Model
{
public function chartOfAccount(): BelongsTo
{
return $this->belongsTo(ChartOfAccount::class);
}
}

47
app/Domains/Finance/Models/StudentInvoiceReport.php

@ -1,47 +0,0 @@
<?php
namespace App\Domains\Finance\Models;
use App\Domains\Finance\Casts\MoneyCurrency;
use App\Domains\Finance\Enums\InvoiceStatus;
use App\Domains\Finance\Policies\StudentInvoiceReportPolicy;
use BadMethodCallException;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
'invoice_id',
'student_id',
'name',
'faculty_id',
'faculty_name',
'study_program_id',
'study_program_name',
'term_id',
'term_name',
'amount',
'paid_amount',
'issued_at',
'paid_at',
'status',
])]
#[UsePolicy(StudentInvoiceReportPolicy::class)]
class StudentInvoiceReport extends Model
{
public static function boot(): void
{
self::boot();
static::creating(fn (InvoiceReport $invoiceReport) => throw new BadMethodCallException('Cannot create invoice report from here, use event instead.'));
static::deleting(fn (InvoiceReport $invoiceReport) => throw new BadMethodCallException('Cannot delete invoice report.'));
static::updating(fn (InvoiceReport $invoiceReport) => throw new BadMethodCallException('Cannot update invoice report.'));
}
protected $casts = [
'amount' => MoneyCurrency::class,
'paid_amount' => MoneyCurrency::class,
'issued_at' => 'datetime',
'paid_at' => 'datetime',
'status' => InvoiceStatus::class,
];
}

49
app/Domains/Finance/Policies/ChartOfAccountPolicy.php

@ -1,49 +0,0 @@
<?php
namespace App\Domains\Finance\Policies;
use App\Domains\Finance\Models\ChartOfAccount;
use App\Domains\Identity\Models\User;
class ChartOfAccountPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('chart-of-account.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, ChartOfAccount $model): bool
{
return $user->hasPermissionTo('chart-of-account.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('chart-of-account.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, ChartOfAccount $model): bool
{
return $user->hasPermissionTo('chart-of-account.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, ChartOfAccount $model): bool
{
return $user->hasPermissionTo('chart-of-account.delete');
}
}

49
app/Domains/Finance/Policies/InvoicePolicy.php

@ -1,49 +0,0 @@
<?php
namespace App\Domains\Finance\Policies;
use App\Domains\Finance\Models\Invoice;
use App\Domains\Identity\Models\User;
class InvoicePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('invoice.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Invoice $model): bool
{
return $user->hasPermissionTo('invoice.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('invoice.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Invoice $model): bool
{
return $user->hasPermissionTo('invoice.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Invoice $model): bool
{
return $user->hasPermissionTo('invoice.delete');
}
}

49
app/Domains/Finance/Policies/InvoiceReportPolicy.php

@ -1,49 +0,0 @@
<?php
namespace App\Domains\Finance\Policies;
use App\Domains\Finance\Models\InvoiceReport;
use App\Domains\Identity\Models\User;
class InvoiceReportPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('invoice-report.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, InvoiceReport $model): bool
{
return $user->hasPermissionTo('invoice-report.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, InvoiceReport $model): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, InvoiceReport $model): bool
{
return false;
}
}

49
app/Domains/Finance/Policies/PaymentPolicy.php

@ -1,49 +0,0 @@
<?php
namespace App\Domains\Finance\Policies;
use App\Domains\Finance\Models\Payment;
use App\Domains\Identity\Models\User;
class PaymentPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('payment.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Payment $model): bool
{
return $user->hasPermissionTo('payment.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('payment.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Payment $model): bool
{
return $user->hasPermissionTo('payment.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Payment $model): bool
{
return $user->hasPermissionTo('payment.delete');
}
}

49
app/Domains/Finance/Policies/ProductMappingPolicy.php

@ -1,49 +0,0 @@
<?php
namespace App\Domains\Finance\Policies;
use App\Domains\Finance\Models\ProductMapping;
use App\Domains\Identity\Models\User;
class ProductMappingPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('product-mapping.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, ProductMapping $model): bool
{
return $user->hasPermissionTo('product-mapping.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('product-mapping.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, ProductMapping $model): bool
{
return $user->hasPermissionTo('product-mapping.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, ProductMapping $model): bool
{
return $user->hasPermissionTo('product-mapping.delete');
}
}

49
app/Domains/Finance/Policies/StudentInvoiceReportPolicy.php

@ -1,49 +0,0 @@
<?php
namespace App\Domains\Finance\Policies;
use App\Domains\Finance\Models\StudentInvoiceReport;
use App\Domains\Identity\Models\User;
class StudentInvoiceReportPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('student-invoice-report.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, StudentInvoiceReport $model): bool
{
return $user->hasPermissionTo('student-invoice-report.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, StudentInvoiceReport $model): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, StudentInvoiceReport $model): bool
{
return false;
}
}

23
app/Domains/Finance/Providers/FinanceServiceProvider.php

@ -1,23 +0,0 @@
<?php
namespace App\Domains\Finance\Providers;
use App\Domains\System\Traits\Provider\RegistersDomainEvents;
use Illuminate\Support\ServiceProvider;
class FinanceServiceProvider 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/Finance/Providers/RelationshipServiceProvider.php

@ -1,31 +0,0 @@
<?php
namespace App\Domains\Finance\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.
}
}

79
app/Domains/Finance/Schemas/Invoice/StudentInvoiceSchema.php

@ -1,79 +0,0 @@
<?php
namespace App\Domains\Finance\Schemas\Invoice;
use App\Domains\Academic\Queries\Selection\SelectFacultyQuery;
use App\Domains\Academic\Queries\Selection\SelectStudyProgramQuery;
use App\Domains\Academic\Queries\Selection\SelectTermQuery;
use App\Domains\Finance\Support\Schemas\InvoiceSchema;
use App\UI\Enums\InputType;
use App\UI\Support\Schemas\InputSchema;
class StudentInvoiceSchema
{
public static function make(): InvoiceSchema
{
return InvoiceSchema::make()
->addInput(
InputSchema::make()
->key('student_id')
->label(__('domains/finance/field.invoice.detail.student_id'))
->type(InputType::TEXTLINE)
->rules(['required'])
)
->addInput(
InputSchema::make()
->key('student_name')
->label(__('domains/finance/field.invoice.detail.student_name'))
->type(InputType::TEXTLINE)
->rules(['required', 'string'])
)
->addInput(
InputSchema::make()
->key('faculty')
->label(__('domains/finance/field.invoice.detail.faculty'))
->type(InputType::SELECT)
->rules(['required', 'string'])
->select2()
->live()
->options(fn () => SelectFacultyQuery::fetch())
)
->addInput(
InputSchema::make()
->key('study_program')
->label(__('domains/finance/field.invoice.detail.study_program'))
->type(InputType::SELECT)
->rules(['required', 'string'])
->select2()
->dependsOn('faculty')
->setContextKeys(['faculty'])
->options(fn ($context) => SelectStudyProgramQuery::fetch($context))
)
->addInput(
InputSchema::make()
->key('term_entry')
->label(__('domains/finance/field.invoice.detail.term_entry'))
->type(InputType::SELECT)
->rules(['required'])
->options(fn () => SelectTermQuery::fetch())
->select2()
)
->addInput(
InputSchema::make()
->key('semester')
->label(__('domains/finance/field.invoice.detail.semester'))
->type(InputType::SELECT)
->rules(['required'])
->options(array_combine(range(1, 13), range(1, 13)))
->select2()
)
->addInput(
InputSchema::make()
->key('fee_type')
->label(__('domains/finance/field.invoice.detail.fee_type'))
->type(InputType::SELECT)
->rules(['required'])
->select2()
);
}
}

25
app/Domains/Finance/Support/Schemas/InvoiceSchema.php

@ -1,25 +0,0 @@
<?php
namespace App\Domains\Finance\Support\Schemas;
use App\UI\Support\Schemas\AbstractGroupSchema;
class InvoiceSchema extends AbstractGroupSchema
{
protected bool $coaVisibility = false;
/**
* Fluent setter for COA visibility.
*/
public function withCoaVisibility(bool $visible = true): static
{
$this->coaVisibility = $visible;
return $this;
}
public function isCoaVisible(): bool
{
return $this->coaVisibility;
}
}

4
app/Domains/Finance/Casts/MoneyCurrency.php → app/Domains/System/Casts/MoneyCurrency.php

@ -1,8 +1,8 @@
<?php
namespace App\Domains\Finance\Casts;
namespace App\Domains\System\Casts;
use App\Domains\Finance\Support\ValueObjects\Money;
use App\Domains\System\Support\ValueObjects\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;

11
app/Domains/System/Enums/SystemSettingKey.php

@ -2,6 +2,7 @@
namespace App\Domains\System\Enums;
use App\Domains\Academic\Queries\Lookup\TermLookup;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
@ -25,6 +26,7 @@ enum SystemSettingKey: string implements HasLabel, HasSchema
case WEB_ADDRESS = 'web-address';
case DEFAULT_LANGUAGE = 'default_language';
case TIMEZONE = 'timezone';
case CURRENT_TERM = 'current_term';
case GOOGLE_TAG_MANAGER_ID = 'google-tag_manager_id';
case GOOGLE_WEBMASTER_ID = 'google-webmaster_id';
@ -84,6 +86,13 @@ enum SystemSettingKey: string implements HasLabel, HasSchema
'id' => __('domains/system/enum.system_setting_key_options.default_language.id'),
]),
self::CURRENT_TERM => InputSchema::make()
->type(InputType::SELECT)
->default('-')
->label($this->label())
->select2()
->options(fn() => TermLookup::fetch()),
self::TIMEZONE => InputSchema::make()
->type(InputType::SELECT)
->default('UTC')
@ -112,7 +121,7 @@ enum SystemSettingKey: string implements HasLabel, HasSchema
],
],
[
__('domains/system/pages.settings.sections.general') => [self::DEFAULT_LANGUAGE, self::TIMEZONE],
__('domains/system/pages.settings.sections.general') => [self::DEFAULT_LANGUAGE, self::TIMEZONE, self::CURRENT_TERM],
__('domains/system/pages.settings.sections.webmaster') => [self::GOOGLE_TAG_MANAGER_ID, self::GOOGLE_WEBMASTER_ID],
],
];

3
app/Domains/System/Models/SystemSettings.php

@ -25,6 +25,9 @@ class SystemSettings extends Model
if ($schema->type->isFile() && isset($this->attributes['value'])) {
return asset_static($this->attributes['value']);
} elseif ($schema->type->isSelect()) {
if (! isset($schema->attributes['options'][$this->attributes['value']])) {
return null;
}
return $schema->attributes['options'][$this->attributes['value']];
}

2
app/Domains/Finance/Support/ValueObjects/Money.php → app/Domains/System/Support/ValueObjects/Money.php

@ -1,6 +1,6 @@
<?php
namespace App\Domains\Finance\Support\ValueObjects;
namespace App\Domains\System\Support\ValueObjects;
use InvalidArgumentException;
use NumberFormatter;

6
app/Http/Controllers/Api/V1/Lookup/RoleLookupController.php

@ -14,11 +14,7 @@ class RoleLookupController extends Controller
*/
public function __invoke(Request $request, RoleLookup $lookup)
{
$result = $lookup->fetch($request->input('search'))
->map(fn ($res) => (object) [
'id' => $res->name,
'text' => $res->name,
]);
$result = $lookup->fetch($request->input('search'));
return LookupResource::collection($result);
}

21
app/Http/Controllers/Api/V1/Lookup/TermLookupController.php

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Api\V1\Lookup;
use App\Domains\Academic\Queries\Lookup\TermLookup;
use App\Http\Controllers\Controller;
use App\Http\Resources\LookupResource;
use Illuminate\Http\Request;
class TermLookupController extends Controller
{
public function __invoke(Request $request, TermLookup $lookup)
{
$terms = $lookup->fetch()->map(fn ($term) => (object) [
'id' => $term,
'text' => $term,
]);
return LookupResource::collection($terms);
}
}

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

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

32
app/Http/Controllers/Web/Finance/InvoiceController.php

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

32
app/Http/Controllers/Web/Finance/ProductMappingController.php

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

34
app/Http/DataTables/Admission/AdmissionScheduleDataTable.php

@ -3,6 +3,8 @@
namespace App\Http\DataTables\Admission;
use App\Domains\Admission\Models\AdmissionSchedule;
use App\Domains\System\Enums\SystemSettingKey;
use App\Domains\System\Queries\GetSystemSettings;
use Illuminate\Database\Eloquent\Builder as QueryBuilder;
use Yajra\DataTables\EloquentDataTable;
use Yajra\DataTables\Html\Builder as HtmlBuilder;
@ -54,10 +56,20 @@ class AdmissionScheduleDataTable extends DataTable
*/
public function query(AdmissionSchedule $model): QueryBuilder
{
return $model->newQuery()
$query = $model->newQuery()
->join('admission_tracks', 'admission_tracks.id', '=', 'admission_schedules.admission_track_id')
->join('terms', 'terms.id', '=', 'admission_schedules.term_id')
->select('admission_schedules.*', 'admission_tracks.name as admission_track_name', 'terms.name as term_name');
->join('terms', 'terms.id', '=', 'admission_schedules.term_id');
if (request()->has('term') && request('term') != '') {
$query->whereHas('term', fn ($query) => $query
->where('name', request('term')));
} else {
$currentTerm = app(GetSystemSettings::class)->get(SystemSettingKey::CURRENT_TERM);
$query->whereHas('term', fn ($query) => $query
->where('name', $currentTerm));
}
return $query->select('admission_schedules.*', 'admission_tracks.name as admission_track_name', 'terms.name as term_name');
}
/**
@ -68,7 +80,11 @@ class AdmissionScheduleDataTable extends DataTable
return $this->builder()
->setTableId('admissionschedule-table')
->columns($this->getColumns())
->minifiedAjax()
->ajax([
'data' => 'function(d) {
d.term = $("#term-filter").val()
}',
])
->orderBy(-1)
->layout([
'topStart' => [
@ -78,7 +94,15 @@ class AdmissionScheduleDataTable extends DataTable
],
'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'],
'features' => [
[
'custom-features' => [
'targetId' => 'template-term-filter',
'style' => 'width: 200px;',
],
],
'search'
],
],
'bottomStart' => 'info',

143
app/Http/DataTables/Finance/ChartOfAccountDataTable.php

@ -1,143 +0,0 @@
<?php
namespace App\Http\DataTables\Finance;
use App\Domains\Finance\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('status', fn (ChartOfAccount $coa) => view('components.badge', [
'label' => $coa->status->label(),
'variant' => $coa->status->variant(),
]))
->editColumn('classification', fn (ChartOfAccount $coa) => $coa->classification->label())
->addColumn(
'action',
fn ($coa) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'chartofaccount-form-modal',
'permission' => auth()->user()->can('update', $coa),
],
'delete' => [
'url' => null,
'title' => __('ui/button.delete'),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.chart_of_account')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.chart_of_account')]),
'permission' => auth()->user()->can('delete', $coa),
],
'table_name' => 'chartofaccount-table',
'id' => $coa->ulid,
])
)
->rawColumns(['action'])
->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('chartofaccount-table')
->columns($this->getColumns())
->minifiedAjax()
->orderBy(-1)
->layout([
'topStart' => [
'rowClass' => 'row gap-1',
'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'),
],
'fixedColumns' => [
'start' => 2,
],
'scrollX' => true,
'scrollCollapse' => true,
'responsive' => true,
])
->buttons([
Button::make('add')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#chartofaccount-form-modal'])
->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')
->width(10)
->title('#'),
Column::make('code')
->title(__('domains/finance/field.coa.code')),
Column::make('name')
->title(__('domains/finance/field.coa.name')),
Column::make('classification')
->title(__('domains/finance/field.coa.classification')),
Column::make('status')
->width(100)
->title(__('domains/finance/field.coa.status')),
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 'ChartOfAccount_'.date('YmdHis');
}
}

115
app/Http/DataTables/Finance/InvoiceDataTable.php

@ -1,115 +0,0 @@
<?php
namespace App\Http\DataTables\Finance;
use App\Domains\Finance\Models\Invoice;
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 InvoiceDataTable extends DataTable
{
/**
* Build the DataTable class.
*
* @param QueryBuilder<Invoice> $query Results from query() method.
*/
public function dataTable(QueryBuilder $query): EloquentDataTable
{
return (new EloquentDataTable($query))
->addColumn('action', 'invoice.action')
->setRowId('id');
}
/**
* Get the query source of dataTable.
*
* @return QueryBuilder<Invoice>
*/
public function query(Invoice $model): QueryBuilder
{
return $model->newQuery();
}
/**
* Optional method if you want to use the html builder.
*/
public function html(): HtmlBuilder
{
return $this->builder()
->setTableId('invoice-table')
->columns($this->getColumns())
->minifiedAjax()
->orderBy(-1)
->layout([
'topStart' => [
'rowClass' => 'row gap-1',
'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'),
],
'fixedColumns' => [
'start' => 2,
],
'scrollX' => true,
'scrollCollapse' => true,
'responsive' => true,
])
->buttons([
Button::make('add')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#invoice-form-modal'])
->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')
->width(10)
->title('#'),
Column::make('name')
->title(__('domains/finance/field.invoice.name')),
Column::make('type')
->title(__('domains/finance/field.invoice.type')),
Column::make('amount')
->title(__('domains/finance/field.invoice.amount')),
Column::make('client.name')
->title(__('domains/finance/field.invoice.client_name')),
Column::make('status')
->width(100)
->title(__('domains/finance/field.invoice.status')),
];
}
/**
* Get the filename for export.
*/
protected function filename(): string
{
return 'Invoice_'.date('YmdHis');
}
}

137
app/Http/DataTables/Finance/ProductMappingDataTable.php

@ -1,137 +0,0 @@
<?php
namespace App\Http\DataTables\Finance;
use App\Domains\Finance\Models\ProductMapping;
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 ProductMappingDataTable extends DataTable
{
/**
* Build the DataTable class.
*
* @param QueryBuilder<ProductMapping> $query Results from query() method.
*/
public function dataTable(QueryBuilder $query): EloquentDataTable
{
return (new EloquentDataTable($query))
->addColumn(
'action',
fn ($productMapping) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'product-mapping-form-modal',
'permission' => auth()->user()->can('update', $productMapping),
],
'delete' => [
'url' => null,
'title' => __('ui/button.delete'),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.product_mapping')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.product_mapping')]),
'permission' => auth()->user()->can('delete', $productMapping),
],
'table_name' => 'productmapping-table',
'id' => $productMapping->ulid,
])
)
->rawColumns(['action'])
->addIndexColumn();
}
/**
* Get the query source of dataTable.
*
* @return QueryBuilder<ProductMapping>
*/
public function query(ProductMapping $model): QueryBuilder
{
return $model->newQuery();
}
/**
* Optional method if you want to use the html builder.
*/
public function html(): HtmlBuilder
{
return $this->builder()
->setTableId('productmapping-table')
->columns($this->getColumns())
->minifiedAjax()
->orderBy(-1)
->layout([
'topStart' => [
'rowClass' => 'row gap-1',
'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'),
],
'fixedColumns' => [
'start' => 2,
],
'scrollX' => true,
'scrollCollapse' => true,
'responsive' => true,
])
->buttons([
Button::make('add')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#product-mapping-form-modal'])
->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')
->width(10)
->title('#'),
Column::make('code')
->title(__('domains/finance/field.product_mapping.code')),
Column::make('name')
->title(__('domains/finance/field.product_mapping.name')),
Column::make('chart_of_account.name')
->title(__('domains/finance/field.product_mapping.coa_name')),
Column::make('client.name')
->title(__('domains/finance/field.product_mapping.client_name')),
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 'ProductMapping_'.date('YmdHis');
}
}

2
app/Http/DataTables/Identity/RoleDataTable.php

@ -45,7 +45,7 @@ class RoleDataTable extends DataTable
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.role')]),
],
'table_name' => 'role-table',
'id' => $role->ulid,
'id' => $role->id,
])
)
->addIndexColumn();

4
app/Livewire/Forms/Finance/InvoiceForm.php

@ -58,12 +58,12 @@ class InvoiceForm extends Form
public function rules(): array
{
return $this->type?->schema()?->getValidationRules('detail');
return $this->type?->schema()?->getValidationRules('detail') ?? [];
}
public function validationAttributes(): array
{
return $this->type?->schema()?->getValidationAttributes('detail');
return $this->type?->schema()?->getValidationAttributes('detail') ?? [];
}
public function toDto(): IssueInvoiceDTO

2
app/Livewire/Forms/Finance/ProductMappingForm.php

@ -14,7 +14,7 @@ class ProductMappingForm extends Form
public string $name = '';
#[Validate('required|string', as: 'domains/finance/field.product-mapping.chart_of_account')]
public string $coa_id = '';
public ?int $chart_of_account_id = null;
// #[Validate('required|string', as: 'domains/finance/field.product-mapping.client')]
// public string $client = '';

1
database/migrations/2026_07_06_031009_create_fee_types_table.php

@ -15,6 +15,7 @@ return new class extends Migration
$table->id();
$table->ulid();
$table->string('name');
$table->string('code');
$table->string('billing_cycle');
$table->timestamps();
});

2
database/migrations/2026_07_13_045042_create_product_mappings_table.php

@ -16,7 +16,7 @@ return new class extends Migration
$table->string('code');
$table->string('name');
$table->foreignId('chart_of_account_id')->constrained();
$table->unsignedBigInteger('client_id')->index();
$table->unsignedBigInteger('client_id')->index()->nullable();
$table->timestamps();
});
}

3
lang/en/domains/academic/field.php

@ -5,11 +5,14 @@ return [
'name' => 'Faculty Name',
'code' => 'Faculty Code',
'status' => 'Status',
'external_id' => 'External ID',
],
'study_program' => [
'name' => 'Study Program Name',
'code' => 'Study Program Code',
'status' => 'Status',
'level' => 'Level',
'external_id' => 'External ID',
],
'term' => [
'name' => 'Term Name',

5
lang/en/domains/admission/field.php

@ -27,5 +27,10 @@ return [
'admission_schedule_detail' => [
'schedule_detail' => 'Admission Schedule Detail',
'fee_detail' => 'Fee Detail',
'admission_period' => 'Admission Period',
're_registration_period' => 'Re-registration Period',
'update_fee_rate' => 'Update Fee Rate',
'enter_new_fee' => 'Enter the new fee amount:',
'amount_required' => 'You need to enter an amount!',
],
];

10
lang/en/domains/admission/label.php

@ -0,0 +1,10 @@
<?php
return [
'fee_type' => [
'not_defined' => 'Not Set',
'partially_defined' => 'Partial',
'completely_defined' => 'Complete',
],
];

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

@ -11,5 +11,6 @@ return [
'password' => 'Password',
'token' => 'Token',
],
'endpoint_url' => 'Endpoint URL',
],
];

38
lang/en/domains/finance/enum.php

@ -1,38 +0,0 @@
<?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;
return [
'invoice_status' => [
InvoiceStatus::PAID->value => 'Paid',
InvoiceStatus::PENDING->value => 'Pending',
InvoiceStatus::PARTIALLY_PAID->value => 'Partially Paid',
InvoiceStatus::OVERDUE->value => 'Overdue',
],
'invoice_type' => [
InvoiceType::STUDENT_INVOICE->value => 'Student Invoice',
InvoiceType::VENDOR_BILL->value => 'Vendor Bill',
InvoiceType::CREDIT_NOTE->value => 'Credit Note',
],
'payment_status' => [
PaymentStatus::PENDING->value => 'Pending',
PaymentStatus::SUCCESS->value => 'Success',
PaymentStatus::FAILED->value => 'Failed',
PaymentStatus::EXPIRED->value => 'Expired',
],
'payment_direction' => [
PaymentDirection::RECEIPT->value => 'Receipt',
PaymentDirection::DISBURSEMENT->value => 'Disbursement',
],
'account_classification' => [
AccountClassification::ASSET->value => 'Asset',
AccountClassification::LIABILITY->value => 'Liability',
AccountClassification::EQUITY->value => 'Equity',
AccountClassification::REVENUE->value => 'Revenue',
AccountClassification::EXPENSE->value => 'Expense',
],
];

50
lang/en/domains/finance/field.php

@ -1,50 +0,0 @@
<?php
return [
'coa' => [
'code' => 'Account Code',
'name' => 'Account Name',
'classification' => 'Classification',
'status' => 'Status',
'parent' => 'Derived From',
],
'invoice' => [
'name' => 'Invoice Name',
'code' => 'Invoice Code',
'type' => 'Invoice Type',
'amount' => 'Invoice Amount',
'client_name' => 'Client Name',
'status' => 'Status',
'coa_name' => 'Account Name (COA)',
'detail' => [
// Detail for Student
'student_name' => 'Student Name',
'student_id' => 'Student ID',
'faculty' => 'Faculty',
'study_program' => 'Study Program',
'term_entry' => 'Term Entry',
'fee_type' => 'Fee Type',
'semester' => 'Semester',
// Vendor Bill
'vendor_name' => 'Vendor Name',
'vendor_id' => 'Vendor ID',
],
],
'payment' => [
'virtual_account' => 'Virtual Account',
'bank_ref_id' => 'Bank Reference Code',
'bank_name' => 'Bank Name',
'nominal' => 'Nominal',
'validity' => 'Validity',
'payment_date' => 'Payment Date',
'direction' => 'Direction',
'status' => 'Status',
],
'product_mapping' => [
'code' => 'Product Code',
'name' => 'Product Name',
'coa_name' => 'Account Name (COA)',
'client_name' => 'Client Name',
],
];

19
lang/en/domains/finance/seo.php

@ -1,19 +0,0 @@
<?php
return [
'coa' => [
'title' => 'Chart of Accounts (COA) Data',
'description' => 'Manage chart of accounts for financial records.',
'keywords' => 'chart of accounts, coa, accounting, finance',
],
'invoice' => [
'title' => 'Invoice Data',
'description' => 'Manage student payment invoices.',
'keywords' => 'invoice, payment, finance',
],
'product_mapping' => [
'title' => 'Product Mapping Data',
'description' => 'Manage cost product mappings with Chart of Accounts (COA).',
'keywords' => 'product mapping, product, coa, finance',
],
];

1
lang/en/domains/system/enum.php

@ -20,6 +20,7 @@ return [
SystemSettingKey::TIMEZONE->value => 'Timezone',
SystemSettingKey::GOOGLE_TAG_MANAGER_ID->value => 'Google Tag Manager ID',
SystemSettingKey::GOOGLE_WEBMASTER_ID->value => 'Google Webmaster ID',
SystemSettingKey::CURRENT_TERM->value => 'Current Term',
],
'system_setting_key_options' => [
'default_language' => [

3
lang/en/ui/button.php

@ -18,4 +18,7 @@ return [
'no' => 'No',
'lookup' => 'Search...',
'logout' => 'Logout',
'publish' => 'Publish',
'sync' => 'Sync',
'run-sync' => 'Run Synchronization',
];

7
lang/en/ui/placeholder.php

@ -0,0 +1,7 @@
<?php
return [
'select' => 'Select :attribute',
'email' => 'name@example.com',
'filter' => ':attribute Filter',
];

3
lang/id/domains/academic/field.php

@ -5,11 +5,14 @@ return [
'name' => 'Nama Fakultas',
'code' => 'Kode Fakultas',
'status' => 'Status',
'external_id' => 'ID Eksternal',
],
'study_program' => [
'name' => 'Nama Program Studi',
'code' => 'Kode Program Studi',
'status' => 'Status',
'level' => 'Jenjang',
'external_id' => 'ID Eksternal',
],
'term' => [
'name' => 'Nama Tahun Ajaran',

5
lang/id/domains/admission/field.php

@ -27,5 +27,10 @@ return [
'admission_schedule_detail' => [
'schedule_detail' => 'Detail Jadwal Penerimaan',
'fee_detail' => 'Detail Biaya',
'admission_period' => 'Periode Penerimaan',
're_registration_period' => 'Periode Daftar Ulang',
'update_fee_rate' => 'Perbarui Tarif Biaya',
'enter_new_fee' => 'Masukkan jumlah biaya baru:',
'amount_required' => 'Anda harus memasukkan jumlah!',
],
];

9
lang/id/domains/admission/label.php

@ -0,0 +1,9 @@
<?php
return [
'fee_type' => [
'not_defined' => 'Not Set',
'partially_defined' => 'Partial',
'completely_defined' => 'Complete',
],
];

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

@ -11,5 +11,6 @@ return [
'password' => 'Password',
'token' => 'Token',
],
'endpoint_url' => 'URL Endpoint',
],
];

1
lang/id/domains/system/enum.php

@ -20,6 +20,7 @@ return [
SystemSettingKey::TIMEZONE->value => 'Zona Waktu',
SystemSettingKey::GOOGLE_TAG_MANAGER_ID->value => 'ID Google Tag Manager',
SystemSettingKey::GOOGLE_WEBMASTER_ID->value => 'ID Google Webmaster',
SystemSettingKey::CURRENT_TERM->value => 'Periode Saat Ini',
],
'system_setting_key_options' => [
'default_language' => [

3
lang/id/ui/button.php

@ -17,4 +17,7 @@ return [
'no' => 'Tidak',
'lookup' => 'Cari...',
'logout' => 'Keluar',
'publish' => 'Terbitkan',
'sync' => 'Sinkronisasi',
'run-sync' => 'Jalankan Sinkronisasi',
];

7
lang/id/ui/placeholder.php

@ -0,0 +1,7 @@
<?php
return [
'select' => 'Pilih :attribute',
'email' => 'nama@contoh.com',
'filter' => 'Filter :attribute',
];

215
resources/js/alpine/chart.js

@ -1,25 +1,212 @@
import '../plugin/apexchart.js'
export default function AlpineChart(Alpine) {
Alpine.directive('chart', function (el, { expression }, { evaluate, cleanup }) {
let config = evaluate(expression);
Alpine.directive(
"chart",
function (el, { expression }, { evaluateLater, cleanup }) {
const getContent = evaluateLater(expression);
const chart = new window.ApexCharts(el, config);
chart.render();
import("apexcharts").then(({ default: ApexCharts }) => {
getContent((userConfig) => {
if (!userConfig || typeof userConfig !== "object") return;
// 1. Fetch live computed CSS variables directly from :root (document.documentElement)
const getCoreUIColors = () => {
const style = getComputedStyle(document.body);
// CoreUI v4/v5 & Bootstrap 5 theme variable names
const bodyColor =
style.getPropertyValue("--cui-body-color").trim() ||
style.getPropertyValue("--bs-body-color").trim() ||
"#a6b0cf";
const secondaryColor =
style
.getPropertyValue("--cui-secondary-color")
.trim() ||
style
.getPropertyValue("--bs-secondary-color")
.trim() ||
"#8a93a2";
const borderColor =
style
.getPropertyValue("--cui-border-color")
.trim() ||
style
.getPropertyValue(
"--bs-border-color-translucent",
)
.trim() ||
style
.getPropertyValue("--bs-border-color")
.trim() ||
"rgba(255, 255, 255, 0.08)";
const primaryColor =
style.getPropertyValue("--cui-primary").trim() ||
style.getPropertyValue("--bs-primary").trim() ||
"#6366f1";
const theme =
document.documentElement.getAttribute(
"data-coreui-theme",
) ||
document.documentElement.getAttribute(
"data-bs-theme",
) ||
"dark";
return {
bodyColor,
secondaryColor,
borderColor,
primaryColor,
theme,
};
};
const colors = getCoreUIColors();
// 2. Base Configuration explicitly overriding ApexCharts SVG text fills
const baseConfig = {
chart: {
type: "line",
height: 360,
foreColor: colors.secondaryColor, // Sets default text/legend colors
background: "transparent",
},
colors: [colors.primaryColor],
theme: {
mode: colors.theme,
},
tooltip: {
theme: colors.theme,
},
grid: {
borderColor: colors.borderColor,
},
xaxis: {
labels: {
style: {
colors: colors.secondaryColor,
fontSize: "12px",
},
},
axisBorder: { color: colors.borderColor },
axisTicks: { color: colors.borderColor },
},
yaxis: {
labels: {
style: {
colors: colors.secondaryColor,
fontSize: "12px",
},
},
},
};
// Deep merge user options with base configuration
const options = Object.assign({}, baseConfig, userConfig);
options.chart = Object.assign(
{},
baseConfig.chart,
userConfig.chart,
);
options.theme = Object.assign(
{},
baseConfig.theme,
userConfig.theme,
);
options.tooltip = Object.assign(
{},
baseConfig.tooltip,
userConfig.tooltip,
);
const event = function(event) {
const newData = event.detail
chart.updateSeries(newData.series, true, true);
chart.updateOptions(newData.options, true, true);
// Preserve user primary colors if explicitly provided in PHP, otherwise use CoreUI's
if (!userConfig.colors) {
options.colors = baseConfig.colors;
}
name = el.getAttribute('id');
requestAnimationFrame(() => {
const chart = new ApexCharts(el, options);
chart.render();
// 3. Theme switch handler
const observer = new MutationObserver(() => {
const updated = getCoreUIColors();
chart.updateOptions(
{
chart: {
foreColor: updated.secondaryColor,
},
theme: { mode: updated.theme },
tooltip: { theme: updated.theme },
grid: { borderColor: updated.borderColor },
xaxis: {
labels: {
style: {
colors: updated.secondaryColor,
},
},
axisBorder: {
color: updated.borderColor,
},
axisTicks: {
color: updated.borderColor,
},
},
yaxis: {
labels: {
style: {
colors: updated.secondaryColor,
},
},
},
},
false,
true,
);
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: [
"data-coreui-theme",
"data-bs-theme",
"class",
],
});
window.addEventListener(`chart-${name}-update`, event)
// 4. Dynamic update event listener
const name = el.getAttribute("id");
const eventHandler = (event) => {
const detail = event.detail || event;
if (detail.series)
chart.updateSeries(detail.series, true);
if (detail.options)
chart.updateOptions(detail.options, true);
};
if (name) {
window.addEventListener(
`chart-${name}-update`,
eventHandler,
);
}
cleanup(() => {
observer.disconnect();
if (name)
window.removeEventListener(
`chart-${name}-update`,
eventHandler,
);
chart.destroy();
window.removeEventListener(`chart-${name}-update`, event)
});
})
});
});
});
},
);
}

104
resources/js/alpine/quill.js

@ -1,46 +1,94 @@
import Quill from "quill";
import "quill/dist/quill.snow.css";
export default function alpineQuill(Alpine) {
Alpine.directive('quill', function (el, { expression }, { evaluate, cleanup }) {
const options = expression ? evaluate(expression) : {};
const defaultOptions = { theme: 'snow', ...options };
Alpine.directive(
"quill",
(el, { expression }, { evaluateLater, cleanup }) => {
// 1. Fallback gracefully if x-quill has no parameters
const getContent = expression
? evaluateLater(expression)
: (cb) => cb({});
getContent((userOptions) => {
const options = Object.assign(
{
theme: "snow",
placeholder: "Write something...",
},
typeof userOptions === "object" ? userOptions : {},
);
// 2. Identify or create container DIV (Quill crashes on <textarea>)
let editorContainer;
let targetInput = el;
if (el.tagName === "TEXTAREA" || el.tagName === "INPUT") {
// Create a container DIV for Quill right before the textarea
editorContainer = document.createElement("div");
el.parentNode.insertBefore(editorContainer, el);
if (typeof window.Quill === 'undefined') {
throw new Error('Quill must be loaded');
// Hide the textarea without breaking Livewire bindings
el.style.display = "none";
} else {
editorContainer = el;
}
const container = document.createElement('div');
el.appendChild(container);
const quill = new window.Quill(container, defaultOptions);
// 3. Instantiate Quill on the container DIV
const quill = new Quill(editorContainer, options);
// Populate initial content from textarea / Alpine model
if (targetInput.value && targetInput.value !== "<p><br></p>") {
quill.root.innerHTML = targetInput.value;
}
let isUpdating = false;
// 1. Text changes handler
quill.on('text-change', () => {
// 4. Sync Quill edits -> hidden input -> Livewire
quill.on("text-change", () => {
if (isUpdating) return;
isUpdating = true;
const htmlContent = quill.root.innerHTML;
el.value = htmlContent === '<p><br></p>' ? '' : htmlContent;
el.dispatchEvent(new Event('input', { bubbles: true }));
const html = quill.root.innerHTML;
const value = html === "<p><br></p>" ? "" : html;
targetInput.value = value;
// Dispatch input & change events on the target input so Livewire detects it
targetInput.dispatchEvent(
new Event("input", { bubbles: true }),
);
targetInput.dispatchEvent(
new Event("change", { bubbles: true }),
);
isUpdating = false;
});
// 2. Element Value Getter/Setter
Object.defineProperty(el, 'value', {
get() { return quill.root.innerHTML; },
set(newValue) {
if (isUpdating || newValue === quill.root.innerHTML) return;
// 5. Sync Livewire dynamic resets -> Quill
const handleExternalUpdate = () => {
if (isUpdating) return;
if (targetInput.value !== quill.root.innerHTML) {
isUpdating = true;
quill.root.innerHTML = newValue || '';
quill.root.innerHTML = targetInput.value || "";
isUpdating = false;
},
configurable: true
});
}
};
quill.on('selection-change', (range) => {
if (range == null) {
el.dispatchEvent(new Event('blur', { bubbles: true }));
targetInput.addEventListener("input", handleExternalUpdate);
// 6. Cleanup DOM when Livewire removes the element
cleanup(() => {
targetInput.removeEventListener(
"input",
handleExternalUpdate,
);
if (editorContainer !== el) {
editorContainer.remove();
el.style.display = "";
}
});
cleanup(() => el.innerHTML = '')
})
});
},
);
}

36
resources/js/alpine/select2.js

@ -4,14 +4,20 @@ export default function alpineSelect2(Alpine) {
function (el, { expression }, { evaluate, cleanup }) {
let config = evaluate(expression) || {};
Promise.all([import("../plugin/select2.js")]).then(
([select2]) => {
if (
(window.jQuery || window.jquery || window.$) &&
typeof window.$.fn.select2 !== "undefined"
) {
let modalContainer = el.closest(".modal");
let select2Default = {
placeholder: config.placeholder || "Select an option",
dropdownParent: config.dropdownParent || modalContainer || el.parentElement,
placeholder:
config.placeholder || "Select an option",
dropdownParent:
config.dropdownParent ||
modalContainer ||
el.parentElement,
allowClear: config.allowClear ?? true,
ajax: config.url
? {
@ -45,7 +51,8 @@ export default function alpineSelect2(Alpine) {
config = Object.assign(select2Default, config);
let $el = $(el).select2(config);
const $select2Container = $el.next(".select2-container");
const $select2Container =
$el.next(".select2-container");
const modelName =
el.getAttribute("wire:model") ||
@ -58,7 +65,9 @@ export default function alpineSelect2(Alpine) {
$el.on("change", (e) => {
if (isUpdating) return;
isUpdating = true;
el.dispatchEvent(new Event("change", { bubbles: true }));
el.dispatchEvent(
new Event("change", { bubbles: true }),
);
isUpdating = false;
});
@ -122,19 +131,30 @@ export default function alpineSelect2(Alpine) {
// Listen to Livewire DOM updates to force error styling inside wire:ignore wrapper
if (window.Livewire) {
Livewire.hook("morph.updated", ({ el: updatedEl }) => {
if (updatedEl === el || updatedEl.contains(el)) {
Livewire.hook(
"morph.updated",
({ el: updatedEl }) => {
if (
updatedEl === el ||
updatedEl.contains(el)
) {
syncErrorState();
}
});
},
);
}
cleanup(() => {
$el.select2("destroy");
window.removeEventListener(`${modelName}-clear`, event);
window.removeEventListener(
`${modelName}-clear`,
event,
);
observer.disconnect();
});
}
},
);
},
);
}

40
resources/js/alpinejs.js

@ -1,23 +1,23 @@
// resources/js/app.js
import { Alpine, Livewire } from '../../vendor/livewire/livewire/dist/livewire.esm';
import bs from './alpine/bs';
import alpineAxios from './alpine/axios';
import alpineSelect2 from "./alpine/select2.js";
import alpineQuill from "./alpine/quill.js";
import alpineAsk from "./alpine/ask.js";
import AlpineChart from "./alpine/chart.js";
// resources/js/app.js
import { Alpine, Livewire } from '../../vendor/livewire/livewire/dist/livewire.esm';
import bs from './alpine/bs';
import alpineAxios from './alpine/axios';
import alpineSelect2 from "./alpine/select2.js";
import alpineQuill from "./alpine/quill.js";
import alpineAsk from "./alpine/ask.js";
import AlpineChart from "./alpine/chart.js";
// 1. Expose both instances globally so Blade directives can see them
window.Alpine = Alpine;
window.Livewire = Livewire;
// 1. Expose both instances globally so Blade directives can see them
window.Alpine = Alpine;
window.Livewire = Livewire;
// 2. Wrap plugin registration inside the initialization event listener
Alpine.plugin(bs);
Alpine.plugin(alpineAxios);
Alpine.plugin(alpineAsk);
Alpine.plugin(alpineSelect2);
Alpine.plugin(alpineQuill);
Alpine.plugin(AlpineChart);
// 2. Wrap plugin registration inside the initialization event listener
Alpine.plugin(bs);
Alpine.plugin(alpineAxios);
Alpine.plugin(alpineAsk);
Alpine.plugin(alpineSelect2);
Alpine.plugin(alpineQuill);
Alpine.plugin(AlpineChart);
// 3. Boot Livewire
Livewire.start();
// 3. Boot Livewire
Livewire.start();

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save