Browse Source

feat: refactor core architecture and enhance developer tooling

- Implement new console commands (, ) and stubs for improved development workflow.
- Standardize domain structures and refactor relationships across Academic, Account, Admission, and Channel modules.
- Add infrastructure support for API endpoints and update system settings synchronization.
- Apply widespread UI/UX updates to Livewire components and Blade views across identity and system modules.

Co-authored-by: Junie <junie@jetbrains.com>
master
Syarif Ubaidillah 1 month ago
parent
commit
60ea3d567e
  1. 193
      app/Console/Commands/DataTableMakeCommand.php
  2. 91
      app/Console/Commands/DomainMakeCommand.php
  3. 90
      app/Console/Commands/PageMakeCommand.php
  4. 18
      app/Console/stubs/domain-datatable/datatable-view.stub
  5. 125
      app/Console/stubs/domain-datatable/datatable.stub
  6. 11
      app/Console/stubs/domain-make/integration.stub
  7. 5
      app/Console/stubs/domain-make/model.stub
  8. 33
      app/Console/stubs/domain-make/page-modal-class.stub
  9. 11
      app/Console/stubs/domain-make/page-modal-view.stub
  10. 5
      app/Console/stubs/domain-make/page.stub
  11. 3
      app/Console/stubs/domain-make/policy.stub
  12. 2
      app/Domains/Academic/Providers/RelationshipServiceProvider.php
  13. 17
      app/Domains/Account/Enums/GenderOption.php
  14. 9
      app/Domains/Account/Models/Profile.php
  15. 2
      app/Domains/Account/Providers/RelationshipServiceProvider.php
  16. 5
      app/Domains/Admission/Models/FeeRate.php
  17. 2
      app/Domains/Admission/Providers/RelationshipServiceProvider.php
  18. 29
      app/Domains/Channel/Actions/Api/AssignEndpointUrl.php
  19. 13
      app/Domains/Channel/Actions/AppProvisioning/DeauthorizConsumerApp.php
  20. 18
      app/Domains/Channel/Actions/AppProvisioning/ModifyConsumerAppDetail.php
  21. 6
      app/Domains/Channel/Actions/AppProvisioning/ProvisionConsumerApp.php
  22. 14
      app/Domains/Channel/DTOs/Api/EndpointUrlDTO.php
  23. 10
      app/Domains/Channel/DTOs/Api/SyncDTO.php
  24. 14
      app/Domains/Channel/DTOs/AppProvisioning/ModifyConsumerAppDetailDTO.php
  25. 11
      app/Domains/Channel/DTOs/AppProvisioning/ProvisionConsumerAppDTO.php
  26. 63
      app/Domains/Channel/Enums/Api/ApiType.php
  27. 17
      app/Domains/Channel/Enums/Client/AuthType.php
  28. 21
      app/Domains/Channel/Models/ApiEndpoint.php
  29. 5
      app/Domains/Channel/Models/Client.php
  30. 2
      app/Domains/Channel/Providers/RelationshipServiceProvider.php
  31. 4
      app/Domains/Finance/Casts/MoneyCurrency.php
  32. 2
      app/Domains/Finance/Providers/RelationshipServiceProvider.php
  33. 9
      app/Domains/Finance/Support/ValueObjects/Money.php
  34. 26
      app/Domains/Identity/Actions/AccessControl/DefineSystemRole.php
  35. 26
      app/Domains/Identity/Actions/AccessControl/DeleteSystemRole.php
  36. 22
      app/Domains/Identity/Actions/Governance/DeleteUser.php
  37. 22
      app/Domains/Identity/Enums/RoleType.php
  38. 78
      app/Domains/Identity/Enums/UserSettingKey.php
  39. 14
      app/Domains/Identity/Enums/UserStatus.php
  40. 11
      app/Domains/Identity/Models/User.php
  41. 2
      app/Domains/Identity/Providers/RelationshipServiceProvider.php
  42. 24
      app/Domains/System/Actions/Backup/ArchiveSystemBackup.php
  43. 42
      app/Domains/System/Actions/Backup/SystemBackup.php
  44. 2
      app/Domains/System/Actions/Files/PruneOrphanedFiles.php
  45. 4
      app/Domains/System/Actions/Settings/UpdateSettings.php
  46. 13
      app/Domains/System/Enums/LifecycleStatus.php
  47. 151
      app/Domains/System/Enums/SystemSettingKey.php
  48. 16
      app/Domains/System/Models/SystemSettings.php
  49. 2
      app/Domains/System/Providers/RelationshipServiceProvider.php
  50. 1
      app/Domains/System/Providers/SystemServiceProvider.php
  51. 4
      app/Domains/System/Queries/GetSystemSettings.php
  52. 17
      app/Domains/System/Traits/Enum/HasPredicateMethod.php
  53. 4
      app/Domains/System/Traits/Model/HasSlugs.php
  54. 222
      app/Domains/System/Traits/Model/HasTranslation.php
  55. 2
      app/Http/Controllers/Web/Academic/FacultyController.php
  56. 12
      app/Http/Controllers/Web/Academic/StudyProgramController.php
  57. 2
      app/Http/Controllers/Web/Academic/TermController.php
  58. 2
      app/Http/Controllers/Web/Account/ProfileController.php
  59. 12
      app/Http/Controllers/Web/Admission/AcademicProgramController.php
  60. 12
      app/Http/Controllers/Web/Admission/AdmissionScheduleController.php
  61. 12
      app/Http/Controllers/Web/Admission/AdmissionTrackController.php
  62. 12
      app/Http/Controllers/Web/Admission/FeeTypeController.php
  63. 2
      app/Http/Controllers/Web/Channel/ClientController.php
  64. 2
      app/Http/Controllers/Web/Finance/ChartOfAccountController.php
  65. 2
      app/Http/Controllers/Web/Finance/InvoiceController.php
  66. 12
      app/Http/Controllers/Web/Finance/ProductMappingController.php
  67. 2
      app/Http/Controllers/Web/Identity/RoleController.php
  68. 2
      app/Http/Controllers/Web/Identity/UserController.php
  69. 10
      app/Http/DataTables/Academic/FacultyDataTable.php
  70. 16
      app/Http/DataTables/Academic/StudyProgramDataTable.php
  71. 10
      app/Http/DataTables/Academic/TermDataTable.php
  72. 16
      app/Http/DataTables/Admission/AcademicProgramDataTable.php
  73. 18
      app/Http/DataTables/Admission/AdmissionScheduleDataTable.php
  74. 14
      app/Http/DataTables/Admission/AdmissionTrackDataTable.php
  75. 14
      app/Http/DataTables/Admission/FeeTypeDataTable.php
  76. 27
      app/Http/DataTables/Channel/ClientDataTable.php
  77. 14
      app/Http/DataTables/Finance/ChartOfAccountDataTable.php
  78. 2
      app/Http/DataTables/Finance/InvoiceDataTable.php
  79. 10
      app/Http/DataTables/Finance/ProductMappingDataTable.php
  80. 10
      app/Http/DataTables/Identity/RoleDataTable.php
  81. 18
      app/Http/DataTables/Identity/UserDataTable.php
  82. 2
      app/Http/Requests/Api/ApiRequest.php
  83. 8
      app/Livewire/Concerns/WithModal.php
  84. 4
      app/Livewire/Forms/Academic/FacultyForm.php
  85. 43
      app/Livewire/Forms/Channel/ClientForm.php
  86. 19
      app/UI/Actions/ApplyLayoutMetadata.php
  87. 2
      app/UI/Actions/ResolveDynamicText.php
  88. 13
      app/UI/Enums/Concerns/InteractsWithLabels.php
  89. 10
      app/UI/Enums/Contracts/HasSchema.php
  90. 6
      app/UI/Enums/FileType.php
  91. 21
      app/UI/Enums/InputType.php
  92. 5
      app/UI/Support/Settings/BaseSchema.php
  93. 44
      app/UI/Support/Settings/SettingSchema.php
  94. 959
      composer.lock
  95. 1
      database/migrations/2026_05_01_172941_create_system_settings_table.php
  96. 8
      database/migrations/2026_07_06_031003_create_clients_table.php
  97. 30
      database/migrations/2026_07_31_035159_create_api_endpoints_table.php
  98. 10
      database/seeders/SystemSettingSeeder.php
  99. 2
      lang/en/domains/academic/field.php
  100. 2
      lang/en/domains/academic/seo.php

193
app/Console/Commands/DataTableMakeCommand.php

@ -0,0 +1,193 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Yajra\DataTables\Generators\DataTablesMakeCommand;
class DataTableMakeCommand extends DataTablesMakeCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'domain:datatable
{name : The name of the DataTable.}
{domain : The domain name.}
{--model= : The name of the model to be used.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new DataTable service class in a domain.';
/**
* Build the class with the given name.
*
* @param string $name
*/
protected function buildClass($name): string
{
$stub = $this->files->get($this->getStub());
$domain = ucfirst($this->argument('domain'));
$className = class_basename(str_replace('/', '\\', $this->getNameInput()));
$modelOption = $this->option('model') ?? Str::replaceLast('DataTable', '', $className);
$modelData = $this->resolveModel($modelOption, $domain);
$modelClass = $modelData['class'];
$modelVariable = Str::camel($modelClass);
$tableId = Str::kebab($modelClass).'-table';
$replacements = [
'{{ namespace }}' => $this->getNamespace($name),
'{{ class }}' => $className,
'{{ modelImport }}' => "use {$modelData['full']};\n",
'{{ model }}' => $modelClass,
'{{ modelVariable }}' => $modelVariable,
'{{ tableId }}' => $tableId,
];
return str_replace(array_keys($replacements), array_values($replacements), $stub);
}
/**
* Resolve model data.
*/
protected function resolveModel(string $modelOption, string $domain): array
{
if (Str::contains($modelOption, ['\\', '/'])) {
if (Str::startsWith(str_replace('/', '\\', $modelOption), 'App\\')) {
$full = str_replace('/', '\\', $modelOption);
$class = class_basename($full);
} else {
$parts = explode('/', str_replace('\\', '/', $modelOption));
$targetDomain = ucfirst($parts[0]);
$targetName = implode('\\', array_map('ucfirst', array_slice($parts, 1)));
$full = "App\\Domains\\{$targetDomain}\\Models\\{$targetName}";
$class = class_basename($full);
}
} else {
$full = "App\\Domains\\{$domain}\\Models\\".ucfirst($modelOption);
$class = ucfirst($modelOption);
}
return [
'full' => $full,
'class' => $class,
];
}
/**
* Get the destination class path.
*
* @param string $name
*/
protected function getPath($name): string
{
$name = Str::replaceFirst($this->rootNamespace(), '', $name);
return $this->laravel->basePath().'/app/'.str_replace('\\', '/', $name).'.php';
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
*/
protected function getDefaultNamespace($rootNamespace): string
{
$domain = ucfirst($this->argument('domain'));
return $rootNamespace."\\Http\\DataTables\\{$domain}";
}
/**
* Get the stub file for the generator.
*/
protected function getStub(): string
{
return app_path('Console/stubs/domain-datatable/datatable.stub');
}
/**
* Parse the name and format according to the root namespace.
*
* @param string $name
* @return string
*/
protected function qualifyClass($name)
{
$name = ltrim($name, '\\/');
$rootNamespace = $this->rootNamespace();
if (Str::startsWith($name, $rootNamespace)) {
return $name;
}
return $this->qualifyClass(
$this->getDefaultNamespace(trim($rootNamespace, '\\')).'\\'.$name
);
}
/**
* Execute the console command.
*
* @return bool|null
*/
public function handle()
{
$status = GeneratorCommand::handle();
if ($status !== false) {
$this->createView();
}
return $status;
}
protected function createView(): void
{
$domain = ucfirst($this->argument('domain'));
$className = class_basename(str_replace('/', '\\', $this->getNameInput()));
$capability = Str::replaceLast('DataTable', '', $className);
$directory = resource_path('views/pages/'.Str::lower($domain).'/'.Str::lower(Str::plural($capability)));
$path = "{$directory}/index.blade.php";
if ($this->files->exists($path)) {
$this->components->warn("View already exists: [{$path}]");
return;
}
$this->files->ensureDirectoryExists($directory);
$stub = $this->files->get(app_path('Console/stubs/domain-datatable/datatable-view.stub'));
$modelOption = $this->option('model') ?? $capability;
$modelData = $this->resolveModel($modelOption, $domain);
$replacements = [
'{{ domain }}' => $domain,
'{{ domainLower }}' => Str::lower($domain),
'{{ domainDot }}' => Str::lower($domain),
'{{ capabilityDot }}' => Str::lower(Str::plural($capability)),
'{{ model }}' => $modelData['class'],
'{{ modelFull }}' => $modelData['full'],
'{{ modelVariable }}' => Str::camel($modelData['class']),
];
$content = str_replace(array_keys($replacements), array_values($replacements), $stub);
$this->files->put($path, $content);
$this->components->info('View created: resources/views/pages/'.Str::lower($domain).'/'.Str::lower(Str::plural($capability)).'/index.blade.php');
}
}

91
app/Console/Commands/DomainMakeCommand.php

@ -14,6 +14,8 @@ class DomainMakeCommand extends Command
{name : Class name, supports sub-paths e.g. Backup/DeleteBackup}
{--factory : Also generate a factory (model only)}
{--migration : Also generate a migration (model only)}
{--policy : Also generate a policy (model only)}
{--all : Generate a factory, migration, and policy (model only)}
{--model= : Associate the export with a model}';
protected $description = 'Generate a file directly into the domain structure (app/Domains/)';
@ -35,7 +37,7 @@ class DomainMakeCommand extends Command
'relationship-provider' => 'Providers',
'view-provider' => 'Providers',
'export' => 'Exports',
// Integration layer — files live under Integration/<subDir>/
'integration' => 'Integration',
'mapper' => 'Integration/Mappers',
'mailable' => 'Mail',
];
@ -49,7 +51,7 @@ class DomainMakeCommand extends Command
{
$type = strtolower($this->argument('type'));
$domain = ucfirst($this->argument('domain'));
$name = $this->argument('name'); // may contain sub-path, e.g. Backup/DeleteBackup
$name = $this->argument('name');
if (! isset($this->types[$type])) {
$this->components->error("Unknown type [{$type}]. Supported: ".implode(', ', array_keys($this->types)));
@ -57,24 +59,19 @@ class DomainMakeCommand extends Command
return self::FAILURE;
}
$subDir = $this->types[$type];
$className = class_basename(str_replace('/', '\\', $name));
$subPath = str_contains($name, '/') ? dirname($name) : null;
// ── Integration / Mapper special handling ─────────────────────────────
// Automatically append the 'DataMapper' suffix when the developer omits it,
// keeping the class name consistent with the DataPayloadMapper contract.
if ($type === 'mapper' && ! str_ends_with($className, 'DataMapper')) {
$className .= 'DataMapper';
}
// The $subDir for 'mapper' already encodes the full Integration/Mappers
// nested path, so we must not double-nest an additional subPath beneath it.
// Extra sub-path segments are intentionally ignored for the mapper type.
$relativeDir = ($type === 'mapper')
? "Domains/{$domain}/{$subDir}"
: "Domains/{$domain}/{$subDir}".($subPath ? "/{$subPath}" : '');
// ─────────────────────────────────────────────────────────────────────
$subDir = $this->types[$type];
$relativeDir = "Domains/{$domain}/{$subDir}";
if ($type !== 'mapper' && $subPath) {
$relativeDir .= "/{$subPath}";
}
$namespace = 'App\\'.str_replace('/', '\\', $relativeDir);
$path = app_path("{$relativeDir}/{$className}.php");
@ -90,16 +87,31 @@ class DomainMakeCommand extends Command
$this->components->info("File [{$path}] created successfully.");
if ($type === 'model' && $this->option('factory')) {
if ($type === 'model') {
$this->handleModelExtra($domain, $className, $namespace);
}
return self::SUCCESS;
}
protected function handleModelExtra(string $domain, string $className, string $namespace): void
{
if ($this->option('factory') || $this->option('all')) {
$this->createFactory($domain, $className, $namespace);
}
if ($type === 'model' && $this->option('migration')) {
if ($this->option('migration') || $this->option('all')) {
$table = Str::snake(Str::pluralStudly($className));
$this->call('make:migration', ['name' => "create_{$table}_table"]);
}
return self::SUCCESS;
if ($this->option('policy') || $this->option('all')) {
$this->call('domain:make', [
'type' => 'policy',
'domain' => $domain,
'name' => "{$className}Policy",
]);
}
}
// ─── Stubs ────────────────────────────────────────────────────────────────
@ -119,18 +131,34 @@ class DomainMakeCommand extends Command
$replacements = [
'{{ namespace }}' => $namespace,
'{{ class }}' => $name,
'{{ factory }}' => '',
];
if ($type === 'model') {
$replacements['{{ factoryImport }}'] = $this->option('factory')
? "\nuse Database\\Factories\\{$domain}\\{$name}Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;"
: '';
$replacements['{{ factoryMethod }}'] = $this->option('factory')
? "\n protected static function newFactory(): Factory\n {\n return {$name}Factory::new();\n }"
: '';
$replacements = match ($type) {
'model' => array_merge($replacements, $this->getModelReplacements($domain, $name)),
'export' => array_merge($replacements, $this->getExportReplacements($domain)),
default => $replacements,
};
return str_replace(array_keys($replacements), array_values($replacements), $stub);
}
if ($type === 'export') {
protected function getModelReplacements(string $domain, string $name): array
{
$hasFactory = $this->option('factory') || $this->option('all');
return [
'{{ factoryImport }}' => $hasFactory
? "\nuse Database\\Factories\\{$domain}\\{$name}Factory;"
: '',
'{{ factory }}' => $hasFactory
? "#[UseFactory({$name}Factory::class)]"
: '',
];
}
protected function getExportReplacements(string $domain): array
{
$modelOption = $this->option('model');
$modelImport = '';
$queryBody = ' // return YourModel::query();';
@ -147,11 +175,10 @@ class DomainMakeCommand extends Command
PHP;
}
$replacements['{{ modelImport }}'] = $modelImport;
$replacements['{{ queryBody }}'] = $queryBody;
}
return str_replace(array_keys($replacements), array_values($replacements), $stub);
return [
'{{ modelImport }}' => $modelImport,
'{{ queryBody }}' => $queryBody,
];
}
protected function resolveModel(string $modelOption, string $domain): array
@ -180,11 +207,8 @@ PHP;
protected function createFactory(string $domain, string $name, string $modelNamespace): void
{
$factoryNamespace = "Database\\Factories\\{$domain}";
$factoryPath = database_path("factories/{$domain}/{$name}Factory.php");
$this->files->ensureDirectoryExists(dirname($factoryPath));
if ($this->files->exists($factoryPath)) {
$this->components->warn("Factory already exists: [{$factoryPath}]");
@ -196,10 +220,11 @@ PHP;
$stub = str_replace(
['{{ factoryNamespace }}', '{{ modelNamespace }}', '{{ class }}'],
[$factoryNamespace, $modelNamespace, $name],
["Database\\Factories\\{$domain}", $modelNamespace, $name],
$stub
);
$this->files->ensureDirectoryExists(dirname($factoryPath));
$this->files->put($factoryPath, $stub);
$this->components->info("Factory [database/factories/{$domain}/{$name}Factory.php] created successfully.");

90
app/Console/Commands/PageMakeCommand.php

@ -0,0 +1,90 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
class PageMakeCommand extends Command
{
protected $signature = 'domain:make-page
{domain : Domain name, e.g. Identity, Account, System}
{capability : Capability name, e.g. Users, Roles, Profile}
{name : Page name, e.g. index, detail}
{--modal : Generate a Livewire modal component}';
protected $description = 'Generate a blade view inside resources/views/pages/{domain}/{capability}';
public function __construct(protected Filesystem $files)
{
parent::__construct();
}
public function handle(): int
{
$domain = strtolower($this->argument('domain'));
$capability = strtolower($this->argument('capability'));
$name = $this->argument('name');
$isModal = $this->option('modal');
$directory = resource_path("views/pages/{$domain}/{$capability}");
if ($isModal) {
return $this->handleModal($directory, $name);
}
$path = "{$directory}/{$name}.blade.php";
if ($this->files->exists($path)) {
$this->components->error("File already exists: [{$path}]");
return self::FAILURE;
}
$this->files->ensureDirectoryExists($directory);
$stubPath = app_path('Console/stubs/domain-make/page.stub');
$stub = $this->files->get($stubPath);
$this->files->put($path, $stub);
$this->components->info("Page created: resources/views/pages/{$domain}/{$capability}/{$name}.blade.php");
return self::SUCCESS;
}
protected function handleModal(string $directory, string $name): int
{
$modalName = Str::kebab($name);
if (! str_ends_with($modalName, '-modal')) {
$modalName .= '-modal';
}
$modalDir = "{$directory}/⚡{$modalName}";
if ($this->files->exists($modalDir)) {
$this->components->error("Modal directory already exists: [{$modalDir}]");
return self::FAILURE;
}
$this->files->ensureDirectoryExists($modalDir);
$modelVariable = Str::camel(Str::before($modalName, '-modal'));
// Class
$classStub = $this->files->get(app_path('Console/stubs/domain-make/page-modal-class.stub'));
$classContent = str_replace('{{ modelVariable }}', $modelVariable, $classStub);
$this->files->put("{$modalDir}/{$modalName}.php", $classContent);
// View
$viewStub = $this->files->get(app_path('Console/stubs/domain-make/page-modal-view.stub'));
$viewContent = str_replace('{{ modalId }}', $modalName, $viewStub);
$this->files->put("{$modalDir}/{$modalName}.blade.php", $viewContent);
$this->components->info('Livewire Modal created in: resources/views/pages/...'."/⚡{$modalName}");
return self::SUCCESS;
}
}

18
app/Console/stubs/domain-datatable/datatable-view.stub

@ -0,0 +1,18 @@
<x-layouts.app>
<x-card>
{{ $dataTable->table() }}
</x-card>
<livewire:pages::system.audit.audit-view-modal key-name="ulid" model="\{{ modelFull }}::class"
translation="domains/{{ domainLower }}/field.{{ modelVariable }}."/>
<livewire:datatables.delete-action key-name="ulid" :model="\{{ modelFull }}::class"
:action="\App\Domains\{{ domain }}\Actions\Governance\Remove{{ model }}::class"/>
<livewire:datatables.excel-manager :export-class="\App\Domains\{{ domain }}\Exports\{{ model }}Export::class"
:import-class="\App\Http\Ingestion\Excel\{{ domain }}\{{ model }}Import::class"
resource-name="{{ modelVariable }}"/>
@push('page-scripts')
@vite(['resources/js/plugin/datatables.js', 'resources/js/plugin/select2.js'])
{{ $dataTable->scripts(attributes: ['type' => 'module']) }}
@endpush
</x-layouts.app>

125
app/Console/stubs/domain-datatable/datatable.stub

@ -0,0 +1,125 @@
<?php
namespace {{ namespace }};
{{ modelImport }}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 {{ class }} extends DataTable
{
/**
* Build the DataTable class.
*
* @param QueryBuilder<{{ model }}> $query Results from query() method.
*/
public function dataTable(QueryBuilder $query): EloquentDataTable
{
return (new EloquentDataTable($query))
->addColumn(
'action',
fn ({{ model }} ${{ modelVariable }}) => view('components.datatables.action-button', [
'view' => [
'modal' => '{{ modelVariable }}-view-modal',
'permission' => auth()->user()->can('view', ${{ modelVariable }}),
],
'edit' => [
'modal' => '{{ modelVariable }}-form-modal',
'permission' => auth()->user()->can('update', ${{ modelVariable }}),
],
'delete' => [
'url' => null,
'title' => __('ui/button.delete'),
'permission' => auth()->user()->can('delete', ${{ modelVariable }}),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.{{ modelVariable }}')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.{{ modelVariable }}')]),
],
'table_name' => '{{ tableId }}',
'id' => ${{ modelVariable }}->ulid,
])
)
->addIndexColumn();
}
/**
* Get the query source of dataTable.
*
* @return QueryBuilder<{{ model }}>
*/
public function query({{ model }} $model): QueryBuilder
{
return $model->newQuery();
}
/**
* Optional method if you want to use the html builder.
*/
public function html(): HtmlBuilder
{
return $this->builder()
->setTableId('{{ tableId }}')
->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('$("#{{ modelVariable }}-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('created_at')
->title(__('ui/label.created_at')),
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 '{{ class }}_' . date('YmdHis');
}
}

11
app/Console/stubs/domain-make/integration.stub

@ -0,0 +1,11 @@
<?php
namespace {{ namespace }};
class {{ class }}
{
public function __construct()
{
// Inject required Domain and Cross-Domain Actions via constructor composition
}
}

5
app/Console/stubs/domain-make/model.stub

@ -3,10 +3,13 @@
namespace {{ namespace }};
{{ factoryImport }}
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable([])]
{{ factory }}
class {{ class }} extends Model
{
use HasFactory;
{{ factoryMethod }}
}

33
app/Console/stubs/domain-make/page-modal-class.stub

@ -0,0 +1,33 @@
<?php
use App\Livewire\Concerns\WithModal;
use App\Livewire\Concerns\WithToast;
use Livewire\Attributes\Locked;
use Livewire\Component;
new class extends Component
{
use WithModal;
use WithToast;
#[Locked]
public ?string $id = null;
#[Locked]
public string $mode = 'create';
protected string $resourceName = '{{ modelVariable }}';
public function show(int|string $id): void
{
$this->id = $id;
$this->mode = 'update';
// $this->form->fill($this->model->only(['name']));
}
public function hide(): void
{
// $this->form->reset();
$this->reset('id', 'mode');
}
};

11
app/Console/stubs/domain-make/page-modal-view.stub

@ -0,0 +1,11 @@
<x-modal id="{{ modalId }}" :title="$this->title" wire:submit="save" wire:loading form livewire>
<div class="d-flex flex-column gap-3">
{{-- Form Fields --}}
</div>
<x-slot:footer>
<x-button type="button" theme="secondary" :label="__('ui/button.cancel')" data-bs-dismiss="modal" />
<x-button type="submit" theme="primary" :label="__('ui/button.save')" />
</x-slot:footer>
</x-modal>

5
app/Console/stubs/domain-make/page.stub

@ -0,0 +1,5 @@
<x-layouts.app>
<x-card>
{{-- Page Content --}}
</x-card>
</x-layouts.app>

3
app/Console/stubs/domain-make/policy.stub

@ -3,9 +3,12 @@
namespace {{ namespace }};
use App\Domains\Identity\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class {{ class }}
{
use HandlesAuthorization;
public function viewAny(User $user): bool
{
return false;

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

@ -17,7 +17,7 @@ class RelationshipServiceProvider extends ServiceProvider
* 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.
* between those domains/
*
* Do NOT add route registrations, bindings, event listeners, or any other
* bootstrapping logic here. Keep this file focused on relationships only.

17
app/Domains/Account/Enums/GenderOption.php

@ -2,12 +2,17 @@
namespace App\Domains\Account\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
use App\UI\Enums\Contracts\HasUiBadge;
enum GenderOption: string implements HasLabel, HasUiBadge
/*
* @method bool isMale()
* @method bool isFemale()
*/
enum GenderOption: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case MALE = 'male';
@ -20,12 +25,4 @@ enum GenderOption: string implements HasLabel, HasUiBadge
self::FEMALE->label() => self::FEMALE,
};
}
public function variant(): string
{
return match ($this) {
self::MALE => 'primary',
self::FEMALE => 'secondary',
};
}
}

9
app/Domains/Account/Models/Profile.php

@ -6,22 +6,17 @@ use App\Domains\Account\Enums\GenderOption;
use App\Domains\Identity\Models\User;
use Database\Factories\Account\ProfileFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable(['user_id', 'gender', 'date_of_birth', 'phone_number'])]
#[UseFactory(ProfileFactory::class)]
class Profile extends Model
{
/** @use HasFactory<ProfileFactory> */
use HasFactory;
protected static function newFactory(): Factory
{
return ProfileFactory::new();
}
protected function casts(): array
{
return [

2
app/Domains/Account/Providers/RelationshipServiceProvider.php

@ -15,7 +15,7 @@ class RelationshipServiceProvider extends ServiceProvider
* 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.
* between those domains/
*
* Do NOT add route registrations, bindings, event listeners, or any other
* bootstrapping logic here. Keep this file focused on relationships only.

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

@ -3,6 +3,7 @@
namespace App\Domains\Admission\Models;
use App\Domains\Admission\Policies\FeeRatePolicy;
use App\Domains\Finance\Casts\MoneyCurrency;
use Database\Factories\Admission\FeeRateFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
@ -25,6 +26,10 @@ class FeeRate extends Model
use HasFactory;
use HasUlids;
protected $casts = [
'amount' => MoneyCurrency::class,
];
public function uniqueIds(): array
{
return ['ulid'];

2
app/Domains/Admission/Providers/RelationshipServiceProvider.php

@ -13,7 +13,7 @@ class RelationshipServiceProvider extends ServiceProvider
* 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.
* between those domains/
*
* Do NOT add route registrations, bindings, event listeners, or any other
* bootstrapping logic here. Keep this file focused on relationships only.

29
app/Domains/Channel/Actions/Api/AssignEndpointUrl.php

@ -0,0 +1,29 @@
<?php
namespace App\Domains\Channel\Actions\Api;
use App\Domains\Channel\DTOs\Api\EndpointUrlDTO;
use App\Domains\Channel\Models\ApiEndpoint;
use Exception;
class AssignEndpointUrl
{
/**
* @param EndpointUrlDTO[] $endpointToSave
* @throws Exception
*/
public function execute(array $endpointToSave): void
{
$validateArrayDTO = array_map(fn($endpoint) => $endpoint instanceof EndpointUrlDTO, $endpointToSave);
if (in_array(false, $validateArrayDTO)) {
throw new \Exception(sprintf('Invalid parameter. to use this action, the $endpointToSave must be an array of %s', EndpointUrlDTO::class));
}
$toSave = array_map(fn(EndpointUrlDTO $endpoint) => [
'client_id' => $endpoint->clientId,
'key' => $endpoint->field,
'value' => $endpoint->endpointUrl
], $endpointToSave);
ApiEndpoint::upsert($toSave, ['client_id', 'key']);
}
}

13
app/Domains/Channel/Actions/AppProvisioning/DeauthorizConsumerApp.php

@ -1,13 +0,0 @@
<?php
namespace App\Domains\Channel\Actions\AppProvisioning;
use App\Domains\Channel\Models\Client;
class DeauthorizConsumerApp
{
public function execute(Client $client): void
{
//
}
}

18
app/Domains/Channel/Actions/AppProvisioning/ModifyConsumerAppDetail.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Channel\Actions\AppProvisioning;
use App\Domains\Channel\DTOs\AppProvisioning\ModifyConsumerAppDetailDTO;
use App\Domains\Channel\Models\Client;
class ModifyConsumerAppDetail
{
public function execute(Client $client, ModifyConsumerAppDetailDTO $dto): void
{
$client->update([
'webhook' => $dto->webhook_url,
'auth_type' => $dto->auth_type,
'credentials' => $dto->credentials,
]);
}
}

6
app/Domains/Channel/Actions/AppProvisioning/ProvisionConsumerApp.php

@ -11,8 +11,10 @@ class ProvisionConsumerApp
{
Client::create([
'name' => $dto->name,
'code' => $dto->code,
'url' => $dto->url,
'domain' => $dto->domain,
'webhook' => $dto->webhook_url,
'auth_type' => $dto->auth_type,
'credentials' => $dto->credentials,
'secret' => encrypt(uuid_create()),
]);
}

14
app/Domains/Channel/DTOs/Api/EndpointUrlDTO.php

@ -0,0 +1,14 @@
<?php
namespace App\Domains\Channel\DTOs\Api;
use App\Domains\Channel\Enums\Api\ApiType;
readonly class EndpointUrlDTO
{
public function __construct(
public string|int $clientId,
public string $endpointUrl,
public ApiType $field
) {}
}

10
app/Domains/Channel/DTOs/Api/SyncDTO.php

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

14
app/Domains/Channel/DTOs/AppProvisioning/ModifyConsumerAppDetailDTO.php

@ -0,0 +1,14 @@
<?php
namespace App\Domains\Channel\DTOs\AppProvisioning;
use App\Domains\Channel\Enums\Client\AuthType;
readonly class ModifyConsumerAppDetailDTO
{
public function __construct(
public string $webhook_url,
public AuthType $auth_type,
public array $credentials,
) {}
}

11
app/Domains/Channel/DTOs/AppProvisioning/ProvisionConsumerAppDTO.php

@ -2,11 +2,18 @@
namespace App\Domains\Channel\DTOs\AppProvisioning;
use App\Domains\Channel\Enums\Client\AuthType;
readonly class ProvisionConsumerAppDTO
{
/**
* @param array<string, string> $credentials
*/
public function __construct(
public string $name,
public string $code,
public string $url,
public string $domain,
public string $webhook_url,
public AuthType $auth_type,
public array $credentials,
) {}
}

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

@ -0,0 +1,63 @@
<?php
namespace App\Domains\Channel\Enums\Api;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
use App\UI\Enums\InputType;
use App\UI\Support\Settings\SettingSchema;
enum ApiType: string implements HasLabel
{
use InteractsWithLabels;
case FACULTY = 'faculty';
case STUDY_PROGRAM = 'study_program';
case TERM = 'term';
case ADMISSION_TRACK = 'admission_track';
case ADMISSION_SCHEDULE = 'admission_schedule';
case ACADEMIC_PROGRAM = 'academic_program';
case FEE_TYPE = 'fee_type';
/**
* Centralized Schema Definitions
*/
public function schema(): SettingSchema
{
return SettingSchema::make(InputType::TEXTLINE)->default('-');
}
public function default(): mixed
{
return $this->schema()->default;
}
public function inputAttributes(): array
{
return array_merge($this->schema()->attributes, [
'label' => $this->label(),
'options' => $this->schema()->type->isSelect() ? $this->schema()->options : '',
]);
}
public static function section(): array
{
return [
[
__('domains/system/pages.api.sections.academic') => [
self::FACULTY,
self::STUDY_PROGRAM,
self::TERM,
],
],
[
__('domains/system/pages.api.sections.admission') => [
self::ACADEMIC_PROGRAM,
self::ADMISSION_TRACK,
self::ADMISSION_SCHEDULE,
self::FEE_TYPE,
],
],
];
}
}

17
app/Domains/Channel/Enums/Client/AuthType.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Channel\Enums\Client;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
enum AuthType: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case NONE = 'none';
case BASIC = 'basic';
case TOKEN = 'token';
}

21
app/Domains/Channel/Models/ApiEndpoint.php

@ -0,0 +1,21 @@
<?php
namespace App\Domains\Channel\Models;
use App\Domains\Channel\Enums\Api\ApiType;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable(['key', 'value', 'client_id'])]
class ApiEndpoint extends Model
{
protected $casts = [
'key' => ApiType::class,
];
public function client(): BelongsTo
{
return $this->belongsTo(Client::class);
}
}

5
app/Domains/Channel/Models/Client.php

@ -2,6 +2,7 @@
namespace App\Domains\Channel\Models;
use App\Domains\Channel\Enums\Client\AuthType;
use App\Domains\Channel\Policies\ClientPolicy;
use Database\Factories\Channel\ClientFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['name', 'url', 'code', 'secret'])]
#[Fillable(['name', 'domain', 'webhook', 'auth_type', 'credentials', 'secret'])]
#[UsePolicy(ClientPolicy::class)]
class Client extends Model
{
@ -20,6 +21,8 @@ class Client extends Model
protected $casts = [
'secret' => 'encrypted',
'auth_type' => AuthType::class,
'credentials' => 'encrypted:array',
];
protected static function newFactory(): Factory

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

@ -16,7 +16,7 @@ class RelationshipServiceProvider extends ServiceProvider
* 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.
* between those domains/
*
* Do NOT add route registrations, bindings, event listeners, or any other
* bootstrapping logic here. Keep this file focused on relationships only.

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

@ -15,7 +15,7 @@ class MoneyCurrency implements CastsAttributes
return null;
}
return new Money((int) $value);
return new Money((float) $value);
}
public function set(Model $model, string $key, mixed $value, array $attributes): ?float
@ -25,7 +25,7 @@ class MoneyCurrency implements CastsAttributes
}
if ($value instanceof Money) {
return $value->amount;
return $value->toFloat();
}
if (is_numeric($value)) {

2
app/Domains/Finance/Providers/RelationshipServiceProvider.php

@ -13,7 +13,7 @@ class RelationshipServiceProvider extends ServiceProvider
* 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.
* between those domains/
*
* Do NOT add route registrations, bindings, event listeners, or any other
* bootstrapping logic here. Keep this file focused on relationships only.

9
app/Domains/Finance/Support/ValueObjects/Money.php

@ -8,7 +8,7 @@ use Stringable;
class Money implements Stringable
{
public function __construct(public float $amount)
public function __construct(private readonly float $amount)
{
if ($this->amount < 0) {
throw new InvalidArgumentException('Money cannot be negative.');
@ -16,7 +16,7 @@ class Money implements Stringable
}
/**
* Format the bytes into a human-readable string.
* Format the money into a selected currency string.
*/
public function format(string $currency = 'IDR'): string
{
@ -25,6 +25,11 @@ class Money implements Stringable
return $format->formatCurrency($this->amount, $currency);
}
public function toFloat(): float
{
return $this->amount;
}
/**
* Automatically format when echoed in Blade (e.g., {{ $model->amount }}).
*/

26
app/Domains/Identity/Actions/AccessControl/DefineSystemRole.php

@ -1,26 +0,0 @@
<?php
namespace App\Domains\Identity\Actions\AccessControl;
use App\Domains\Identity\DTOs\AccessControl\CreateRoleDTO;
use App\Domains\Identity\Models\Role;
use Illuminate\Support\Facades\DB;
use Throwable;
class DefineSystemRole
{
/**
* @throws Throwable
*/
public function execute(CreateRoleDTO $dto): bool
{
return DB::transaction(function () use ($dto) {
$role = Role::create([
'name' => $dto->name,
]);
$role->syncPermissions($dto->permissions);
return true;
});
}
}

26
app/Domains/Identity/Actions/AccessControl/DeleteSystemRole.php

@ -1,26 +0,0 @@
<?php
namespace App\Domains\Identity\Actions\AccessControl;
use App\Domains\Identity\Enums\RoleType;
use App\Domains\Identity\Models\Role;
use Exception;
use function in_array;
class DeleteSystemRole
{
/**
* @throws Exception
*/
public function execute(Role $role): void
{
if (in_array($role->name, [RoleType::ADMIN->value, RoleType::SYSTEM_ADMIN->value])) {
throw new Exception(__('domains/identity/messages.exceptions.cannot_remove_system_role'));
}
if ($role->users()->exists()) {
throw new Exception(__('domains/identity/messages.exceptions.role_has_users'));
}
}
}

22
app/Domains/Identity/Actions/Governance/DeleteUser.php

@ -1,22 +0,0 @@
<?php
namespace App\Domains\Identity\Actions\Governance;
use App\Domains\Identity\Models\User;
class DeleteUser
{
public function __construct(
protected PurgeUser $purgeUser,
protected SuspendUser $suspendUser
) {}
public function execute(User $user): void
{
if ($user->status->isActive()) {
$this->suspendUser->execute($user);
} else {
$this->purgeUser->execute($user);
}
}
}

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

@ -34,24 +34,23 @@ enum RoleType: string
'roles' => [self::USER],
],
...self::generatePolicy('role', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('user', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('faculty', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('study-program', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('term', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('client', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('admission-track', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('academic-program', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('admission-schedule', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('academic-program', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('fee-type', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('chart-of-account', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('client', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('invoice', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('product-mapping', [self::SYSTEM_ADMIN, self::ADMIN]),
...self::generatePolicy('payment', [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]),
// System Setting
[
@ -61,6 +60,13 @@ enum RoleType: string
'guard_name' => 'web',
'roles' => [self::SYSTEM_ADMIN, self::ADMIN],
],
[
'name' => 'system-setting.api',
'description' => 'permissions.system-setting.api',
'group' => 'system-setting',
'guard_name' => 'web',
'roles' => [self::SYSTEM_ADMIN, self::ADMIN],
],
// System Backup
[

78
app/Domains/Identity/Enums/UserSettingKey.php

@ -2,72 +2,48 @@
namespace App\Domains\Identity\Enums;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
use App\UI\Enums\Contracts\HasSchema;
use App\UI\Enums\InputType;
use App\UI\Support\Settings\SettingSchema;
use Illuminate\Validation\Rule;
enum UserSettingKey: string
enum UserSettingKey: string implements HasLabel, HasSchema
{
use InteractsWithLabels;
case NOTIFICATION = 'notification';
case LANGUAGE = 'language';
case TIMEZONE = 'timezone';
public function label(): string
{
return __("domains/account/enum.user_settings.{$this->value}");
}
public static function effect(string $settingKey, string|int $value): void
{
match ($settingKey) {
self::LANGUAGE->value => app()->setLocale($value),
self::TIMEZONE->value => date_default_timezone_set($value),
default => null,
};
}
public function default(): mixed
{
return match ($this) {
self::NOTIFICATION => 0,
self::LANGUAGE => 'en',
self::TIMEZONE => 'UTC',
};
}
public function type(): string
public function schema(): SettingSchema
{
return match ($this) {
self::NOTIFICATION,
self::LANGUAGE,
self::TIMEZONE => 'option',
};
}
public function options(): array
{
return match ($this) {
$options = match ($this) {
self::LANGUAGE => [
'en' => __('domains/account/enum.user_settings.options.language.en'),
'id' => __('domains/account/enum.user_settings.options.language.id'),
'en' => __('domains/identity/enum.user_setting_key.options.language.en'),
'id' => __('domains/identity/enum.user_setting_key.options.language.id'),
],
self::TIMEZONE => [
'UTC' => 'UTC',
'Asia/Jakarta' => 'Asia/Jakarta',
'Asia/Makassar' => 'Asia/Makassar',
'Asia/Jayapura' => 'Asia/Jayapura',
'UTC' => __('domains/identity/enum.user_setting_key.options.timezone.UTC'),
'Asia/Jakarta' => __('domains/identity/enum.user_setting_key.options.timezone.Asia/Jakarta'),
'Asia/Makassar' => __('domains/identity/enum.user_setting_key.options.timezone.Asia/Makassar'),
'Asia/Jayapura' => __('domains/identity/enum.user_setting_key.options.timezone.Asia/Jayapura'),
],
self::NOTIFICATION => [
1 => __('domains/account/enum.user_settings.options.notification.on'),
0 => __('domains/account/enum.user_settings.options.notification.off'),
1 => __('domains/identity/enum.user_setting_key.options.notification.on'),
0 => __('domains/identity/enum.user_setting_key.options.notification.off'),
],
default => [],
};
}
public function validation(): array
{
return [
'required',
Rule::in(array_keys($this->options())),
];
$default = match ($this) {
self::NOTIFICATION => 0,
self::LANGUAGE => 'en',
self::TIMEZONE => 'UTC',
};
return SettingSchema::make(InputType::SELECT, ['required', Rule::in(array_keys($options))])
->default($default)
->options($options);
}
}

14
app/Domains/Identity/Enums/UserStatus.php

@ -5,8 +5,13 @@ namespace App\Domains\Identity\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
use App\UI\Enums\Contracts\HasUiBadge;
enum UserStatus: string implements HasLabel
/**
* @method bool isActive()
* @method bool isInactive()
*/
enum UserStatus: string implements HasLabel, HasUiBadge
{
use HasPredicateMethod;
use InteractsWithLabels;
@ -14,12 +19,7 @@ enum UserStatus: string implements HasLabel
case ACTIVE = 'active';
case INACTIVE = 'inactive';
public function label(): string
{
return __('domains/identity/enum.user_status.'.$this->value);
}
public function badge(): string
public function variant(): string
{
return match ($this) {
self::ACTIVE => 'success',

11
app/Domains/Identity/Models/User.php

@ -11,9 +11,9 @@ use Database\Factories\Identity\UserFactory;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Foundation\Auth\User as Authenticatable;
@ -25,13 +25,11 @@ use Spatie\Permission\Traits\HasRoles;
#[Fillable(['name', 'email', 'password', 'status', 'settings'])]
#[Hidden(['password', 'remember_token'])]
#[UsePolicy(UserPolicy::class)]
#[UseFactory(UserFactory::class)]
class User extends Authenticatable implements Auditable, MustVerifyEmail
{
use HasApiTokens;
/** @use HasFactory<UserFactory> */
use HasFactory;
use HasFile;
use HasRoles;
use HasUlids;
@ -59,11 +57,6 @@ class User extends Authenticatable implements Auditable, MustVerifyEmail
'status',
];
protected static function newFactory(): Factory
{
return UserFactory::new();
}
public function sendPasswordResetNotification($token): void
{
// This overrides the default CanResetPassword trait method

2
app/Domains/Identity/Providers/RelationshipServiceProvider.php

@ -15,7 +15,7 @@ class RelationshipServiceProvider extends ServiceProvider
* 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.
* between those domains/
*
* Do NOT add route registrations, bindings, event listeners, or any other
* bootstrapping logic here. Keep this file focused on relationships only.

24
app/Domains/System/Actions/Backup/ArchiveSystemBackup.php

@ -1,24 +0,0 @@
<?php
namespace App\Domains\System\Actions\Backup;
use Exception;
use Illuminate\Support\Facades\Artisan;
class ArchiveSystemBackup
{
public function __construct(protected SyncBackupCatalog $syncBackupCatalog) {}
/**
* @throws Exception
*/
public function execute(): void
{
$artisanResult = Artisan::call('backup:run', []);
if ($artisanResult !== 0) {
throw new Exception(__('domains/system/messages.backup.backup_error'));
}
$this->syncBackupCatalog->execute();
}
}

42
app/Domains/System/Actions/Backup/SystemBackup.php

@ -0,0 +1,42 @@
<?php
namespace App\Domains\System\Actions\Backup;
use App\Domains\System\Models\Backup;
use Exception;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Storage;
class SystemBackup
{
/**
* @throws Exception
*/
public function execute(): Backup
{
$artisanResult = Artisan::call('backup:run', []);
if ($artisanResult !== 0) {
throw new Exception(__('domains/system/messages.backup.backup_error'));
}
$disk = config('backup.backup.destination.disks')[0] ?? 'local';
$backupName = config('backup.backup.name') ?? 'Laravel';
$files = Storage::disk($disk)->allFiles($backupName);
if (empty($files)) {
throw new Exception(__('domains/system/messages.backup.verification_error'));
}
$latestFile = collect($files)->last();
$fileName = $backupName.' '.basename($latestFile);
$sizeInBytes = Storage::disk($disk)->size($latestFile);
return Backup::create([
'file_name' => $fileName,
'disk' => $disk,
'path' => $latestFile,
'size' => $sizeInBytes,
'type' => 'full',
]);
}
}

2
app/Domains/System/Actions/Files/PruneOrphanedFiles.php

@ -35,7 +35,7 @@ class PruneOrphanedFiles
$trackedFiles = File::pluck('path')->toArray();
$settingFiles = collect(SystemSettingKey::cases())
->filter(fn ($key) => $key->inputType() === InputType::FILE)
->filter(fn ($key) => $key->schema()->type === InputType::FILE)
->map(fn ($key) => $this->settingQuery->get($key))
->filter()
->toArray();

4
app/Domains/System/Actions/Settings/UpdateSettings.php

@ -12,7 +12,7 @@ class UpdateSettings
{
$value = $dto->value;
if ($dto->key->isImage()) {
if ($dto->key->schema()->type->isFile()) {
$currentSettings = SystemSettings::where('key', $dto->key->value)->value('value');
if ($currentSettings) {
@ -33,6 +33,6 @@ class UpdateSettings
['value' => $value],
);
cache()->forget('system_settings');
cache()->forget(SystemSettings::$cacheName);
}
}

13
app/Domains/System/Enums/LifecycleStatus.php

@ -5,21 +5,12 @@ namespace App\Domains\System\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
use App\UI\Enums\Contracts\HasUiBadge;
enum LifecycleStatus: string implements HasLabel, HasUiBadge
enum LifecycleStatus: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case ACTIVE = 'active';
case ARCHIVED = 'archived';
public function variant(): string
{
return match ($this) {
self::ACTIVE => 'success',
self::ARCHIVED => 'secondary',
};
}
case INACTIVE = 'inactive';
}

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

@ -2,11 +2,19 @@
namespace App\Domains\System\Enums;
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\Enums\FileType;
use App\UI\Enums\InputType;
use App\UI\Support\Settings\SettingSchema;
enum SystemSettingKey: string
enum SystemSettingKey: string implements HasLabel, HasSchema
{
use HasPredicateMethod;
use InteractsWithLabels;
case WEB_NAME = 'web-name';
case WEB_DESCRIPTION = 'web-description';
case WEB_LOGO = 'web-logo';
@ -14,117 +22,82 @@ enum SystemSettingKey: string
case WEB_PHONE = 'web-phone';
case WEB_EMAIL = 'web-email';
case WEB_ADDRESS = 'web-address';
case DEFAULT_LANGUAGE = 'default_language';
case TIMEZONE = 'timezone';
case GOOGLE_TAG_MANAGER_ID = 'google-tag_manager_id';
case GOOGLE_WEBMASTER_ID = 'google-webmaster_id';
public function label(): string
{
return __('domains/system/field.settings.'.str_replace('-', '.', $this->value));
}
public static function section(): array
/**
* Centralized Schema Definitions
*/
public function schema(): SettingSchema
{
return [
[
__('domains/system/pages.settings.sections.web') => [
self::WEB_NAME,
self::WEB_DESCRIPTION,
self::WEB_ADDRESS,
self::WEB_PHONE,
self::WEB_EMAIL,
self::WEB_LOGO,
self::WEB_FAVICON,
],
],
[
__('domains/system/pages.settings.sections.general') => [
self::DEFAULT_LANGUAGE,
self::TIMEZONE,
],
__('domains/system/pages.settings.sections.webmaster') => [
self::GOOGLE_TAG_MANAGER_ID,
self::GOOGLE_WEBMASTER_ID,
],
],
];
}
public function inputType(): InputType
{
return match ($this) {
self::WEB_LOGO, self::WEB_FAVICON => InputType::FILE,
self::DEFAULT_LANGUAGE, self::TIMEZONE => InputType::SELECT,
self::WEB_DESCRIPTION => InputType::TEXTAREA,
default => InputType::TEXTLINE
};
}
public function inputAttributes(): array
{
$options = match ($this) {
self::WEB_LOGO, self::WEB_FAVICON => [
$imageRules = ['required', 'file', 'mimetypes:'.implode(',', FileType::IMAGE->mimeType()), 'max:1024'];
$imageAttrs = [
'allow-image-crop' => true,
'allow-image-resize' => true,
'allow-image-transform' => true,
'image-crop-aspect-ratio' => '1:1',
'image-resize-target-width' => '500',
'image-resize-target-height' => '500',
],
self::TIMEZONE, self::DEFAULT_LANGUAGE => ['options' => $this->options()],
default => [],
};
$options['label'] = $this->label();
return $options;
}
];
public function validation(): array
{
return match ($this) {
self::WEB_LOGO, self::WEB_FAVICON => ['required', 'file', 'mimetypes:'.implode(',', FileType::IMAGE->mimeType()), 'max:1024'],
default => ['required', 'string'],
self::WEB_NAME => SettingSchema::make(InputType::TEXTLINE)->default('Acme Inc'),
self::WEB_DESCRIPTION => SettingSchema::make(InputType::TEXTAREA),
self::WEB_ADDRESS => SettingSchema::make(InputType::TEXTLINE)->default('123 Main St, Anytown, USA'),
self::WEB_PHONE => SettingSchema::make(InputType::TEXTLINE)->default('+1234567890'),
self::WEB_EMAIL => SettingSchema::make(InputType::TEXTLINE)->default('acme@web.io'),
self::WEB_LOGO,
self::WEB_FAVICON => SettingSchema::make(InputType::FILE, $imageRules)->attributes($imageAttrs),
self::DEFAULT_LANGUAGE => SettingSchema::make(InputType::SELECT)
->default('en')
->options([
'en' => __('domains/system/enum.system_setting_key_options.default_language.en'),
'id' => __('domains/system/enum.system_setting_key_options.default_language.id'),
]),
self::TIMEZONE => SettingSchema::make(InputType::SELECT)
->default('UTC')
->options([
'UTC' => __('domains/system/enum.system_setting_key_options.timezone.UTC'),
'Asia/Jakarta' => __('domains/system/enum.system_setting_key_options.timezone.Asia/Jakarta'),
'Asia/Makassar' => __('domains/system/enum.system_setting_key_options.timezone.Asia/Makassar'),
'Asia/Jayapura' => __('domains/system/enum.system_setting_key_options.timezone.Asia/Jayapura'),
]),
default => SettingSchema::make(InputType::TEXTLINE)->rules(['nullable', 'string']),
};
}
public function default(): ?string
public function default(): mixed
{
return match ($this) {
self::DEFAULT_LANGUAGE => 'en',
self::TIMEZONE => 'UTC',
self::WEB_NAME => 'Acme Inc',
self::WEB_ADDRESS => '123 Main St, Anytown, USA',
self::WEB_PHONE => '+1234567890',
self::WEB_EMAIL => 'acme@web.io',
default => null
};
return $this->schema()->default;
}
public function options(): array
public function inputAttributes(): array
{
return match ($this) {
self::DEFAULT_LANGUAGE => [
'en' => 'English',
'id' => 'Indonesian',
],
self::TIMEZONE => [
'UTC' => 'UTC',
'Asia/Jakarta' => 'Asia/Jakarta',
'Asia/Makassar' => 'Asia/Makassar',
'Asia/Jayapura' => 'Asia/Jayapura',
],
default => [],
};
return array_merge($this->schema()->attributes, [
'label' => $this->label(),
'options' => $this->schema()->type->isSelect() ? $this->schema()->options : '',
]);
}
public function isImage(): bool
public static function section(): array
{
return in_array($this, [
self::WEB_LOGO,
self::WEB_FAVICON,
]);
return [
[
__('domains/system/pages.settings.sections.web') => [
self::WEB_NAME, self::WEB_DESCRIPTION, self::WEB_ADDRESS,
self::WEB_PHONE, self::WEB_EMAIL, self::WEB_LOGO, self::WEB_FAVICON,
],
],
[
__('domains/system/pages.settings.sections.general') => [self::DEFAULT_LANGUAGE, self::TIMEZONE],
__('domains/system/pages.settings.sections.webmaster') => [self::GOOGLE_TAG_MANAGER_ID, self::GOOGLE_WEBMASTER_ID],
],
];
}
}

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

@ -2,27 +2,29 @@
namespace App\Domains\System\Models;
use App\Domains\System\Enums\SystemSettingKey;
use App\UI\Enums\InputType;
use Exception;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['key', 'value'])]
#[Fillable(['key', 'value', 'type'])]
#[WithoutTimestamps]
class SystemSettings extends Model
{
public static string $cacheName = 'system-settings';
/**
* @throws Exception
*/
public function getTranslatedValueAttribute(): ?string
{
$key = SystemSettingKey::tryFrom($this->attributes['key']);
if ($key->inputType() == InputType::FILE && isset($this->attributes['value'])) {
$key = $this->attributes['type']::tryFrom($this->attributes['key']);
$schema = $key->schema();
if ($schema->type->isFile() && isset($this->attributes['value'])) {
return asset_static($this->attributes['value']);
} elseif ($key->inputType() == InputType::SELECT) {
return $key->options()[$this->attributes['value']];
} elseif ($schema->type->isSelect()) {
return $schema->options[$this->attributes['value']];
}
return $this->attributes['value'];

2
app/Domains/System/Providers/RelationshipServiceProvider.php

@ -13,7 +13,7 @@ class RelationshipServiceProvider extends ServiceProvider
* 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.
* between those domains/
*
* Do NOT add route registrations, bindings, event listeners, or any other
* bootstrapping logic here. Keep this file focused on relationships only.

1
app/Domains/System/Providers/SystemServiceProvider.php

@ -32,6 +32,7 @@ class SystemServiceProvider extends ServiceProvider
public function register(): void
{
$this->app->register(RelationshipServiceProvider::class);
$this->app->register(ViewServiceProvider::class);
// Tell Laravel: "Whenever someone asks for GetSystemSettings,
// give them the exact same object instance for the entire request."

4
app/Domains/System/Queries/GetSystemSettings.php

@ -21,8 +21,8 @@ class GetSystemSettings
return $this->settings;
}
$this->settings = Cache::rememberForever('system_settings', function () {
$settings = SystemSettings::pluck('value', 'key')->toArray();
$this->settings = Cache::rememberForever(SystemSettings::$cacheName, function () {
$settings = SystemSettings::where('type', SystemSettingKey::class)->pluck('value', 'key')->toArray();
$finalSettings = [];
foreach (SystemSettingKey::cases() as $key) {
$finalSettings[$key->value] = $settings[$key->value] ?? $key->default();

17
app/Domains/System/Traits/Enum/HasPredicateMethod.php

@ -2,22 +2,29 @@
namespace App\Domains\System\Traits\Enum;
/*
* @mixin \BackedEnum
*/
use BadMethodCallException;
use Illuminate\Support\Str;
trait HasPredicateMethod
{
public function __call(string $method, array $arguments): bool
{
if (str_starts_with($method, 'is')) {
$expectedCase = substr($method, 2);
$expectedCase = strtoupper($expectedCase);
$expectedCase = Str::substr($method, 2);
$expectedCase = Str::upper($expectedCase);
foreach ($this::cases() as $case) {
if ($case->name === $expectedCase) {
if ($case->name === $expectedCase || $case->value === Str::kebab($expectedCase)) {
return $this === $case;
}
}
throw new \BadMethodCallException("Method {$method} does not exist on ".self::class);
throw new BadMethodCallException("Method {$method} does not exist on ".self::class);
}
return true;
throw new BadMethodCallException("Method {$method} does not exist on ".self::class);
}
}

4
app/Domains/System/Traits/Model/HasSlugs.php

@ -13,6 +13,8 @@ trait HasSlugs
public static function bootHasSlugs(): void
{
static::saving(fn (self $model) => $model->slug = str($model->{$model->sluggable()})->slug());
static::creating(function (Model $model) {
$model->slug = str($model->sluggable())->slug();
});
}
}

222
app/Domains/System/Traits/Model/HasTranslation.php

@ -0,0 +1,222 @@
<?php
namespace App\Domains\System\Traits\Model;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/**
* @mixin Model
*/
trait HasTranslation
{
/**
* Internal cache storage for translations retrieved from the DB table.
* Format: ['en' => ['name' => 'Value', 'story' => '...'], 'ar' => [...]]
*/
protected array $translationsCache = [];
/**
* Tracks whether translation records have been read from the database table for this instance.
*/
protected bool $translationsLoaded = false;
/**
* Intercept the standard Eloquent model booting pipeline.
*/
public static function bootHasTranslation(): void
{
// Automatically save any dirty or staged translations when the parent model saves
static::saved(function (self $model) {
$model->saveTranslations();
});
// Automatically drop related database rows when the parent record is deleted
static::deleted(function (self $model) {
$model->purgeTranslations();
});
}
/**
* Magic Getter Interception:
* Intercepts access to properties defined in the $translatable array.
*/
public function __get($key)
{
if ($this->isTranslationAttribute($key)) {
return $this->translate($key, $this->getLocale());
}
return parent::__get($key);
}
/**
* Magic Setter Interception:
* Intercepts updates to properties defined in the $translatable array.
*/
public function __set($key, $value)
{
if ($this->isTranslationAttribute($key)) {
$this->setTranslationValue($key, $this->getLocale(), $value);
return;
}
parent::__set($key, $value);
}
/**
* Magic Isset Interception:
* Handles isset() evaluations for translatable fields.
*/
public function __isset($key)
{
if ($this->isTranslationAttribute($key)) {
return ! is_null($this->translate($key, $this->getLocale()));
}
return parent::__isset($key);
}
/**
* Override the standard Eloquent fill process to support nested language arrays
* Example: $model->fill(['en' => ['name' => 'Adam'], 'color_value' => 123])
*/
public function fill(array $attributes)
{
foreach ($attributes as $key => $value) {
// Check if the key looks like a language code (e.g., 'en', 'ar', 'id')
if (is_array($value) && (strlen($key) === 2 || strlen($key) === 5)) {
foreach ($value as $translatedKey => $translatedValue) {
if ($this->isTranslationAttribute($translatedKey)) {
$this->setTranslationValue($translatedKey, $key, $translatedValue);
}
}
unset($attributes[$key]);
}
}
return parent::fill($attributes);
}
/**
* Public method to retrieve a field value for a specific language locale.
*/
public function translate(string $attribute, ?string $locale = null): ?string
{
// 1. High-Performance Left-Join Optimization:
// If the query used a leftJoin scope, the column exists directly on the primary attributes array.
if (array_key_exists($attribute, $this->attributes)) {
return $this->attributes[$attribute];
}
$locale = $locale ?? $this->getLocale();
// 2. Load translations from the database into the cache array if not already loaded
if (! $this->translationsLoaded && $this->exists) {
$this->loadTranslationsFromTable();
}
return $this->translationsCache[$locale][$attribute] ?? $this->getFallbackTranslation($attribute);
}
/**
* Check if a specific string property name is declared as a translatable variable.
*/
public function isTranslationAttribute(string $key): bool
{
return isset($this->translatable) && in_array($key, $this->translatable);
}
/**
* Load all rows from the target database translation table into local memory cache.
*/
public function loadTranslationsFromTable(): self
{
$records = DB::table($this->getTranslationTableName())
->where($this->getTranslationForeignKey(), $this->getKey())
->get();
foreach ($records as $record) {
foreach ((array) $record as $column => $value) {
if (in_array($column, ['id', $this->getTranslationForeignKey(), 'locale'])) {
continue;
}
$this->translationsCache[$record->locale][$column] = $value;
}
}
$this->translationsLoaded = true;
return $this;
}
/**
* Staged Memory Update: Write property variables straight into the memory array.
*/
protected function setTranslationValue(string $attribute, string $locale, ?string $value): void
{
$this->translationsCache[$locale][$attribute] = $value;
}
/**
* Database Mutation: Write cached memory arrays to the database table.
*/
protected function saveTranslations(): void
{
foreach ($this->translationsCache as $locale => $attributes) {
if (empty($attributes)) {
continue;
}
DB::table($this->getTranslationTableName())->updateOrInsert(
[
$this->getTranslationForeignKey() => $this->getKey(),
'locale' => $locale,
],
$attributes
);
}
}
/**
* Database Purge: Remove records from the database table.
*/
protected function purgeTranslations(): void
{
DB::table($this->getTranslationTableName())
->where($this->getTranslationForeignKey(), $this->getKey())
->delete();
}
/**
* Fallback Logic: Returns the first available translation if the requested language is missing.
*/
protected function getFallbackTranslation(string $attribute): ?string
{
foreach ($this->translationsCache as $locale => $attributes) {
if (! empty($attributes[$attribute])) {
return $attributes[$attribute];
}
}
return null;
}
protected function getLocale(): string
{
return App::getLocale();
}
protected function getTranslationTableName(): string
{
return Str::singular($this->getTable()).'_translations';
}
protected function getTranslationForeignKey(): string
{
return Str::singular($this->getTable()).'_id';
}
}

2
app/Http/Controllers/Web/Academic/FacultyController.php

@ -21,7 +21,7 @@ class FacultyController extends Controller
#[LayoutData(
header: 'domains/academic/seo.faculty.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'ui/menu.dashboard' => 'dashboard',
'domains/academic/seo.faculty.title' => '',
],
)]

12
app/Http/Controllers/Web/Academic/StudyProgramController.php

@ -14,15 +14,15 @@ class StudyProgramController extends Controller
* Handle the incoming request.
*/
#[Seo(
title: 'domains/academic/seo.study-program.title',
description: 'domains/academic/seo.study-program.description',
keywords: 'domains/academic/seo.study-program.keywords'
title: 'domains/academic/seo.study_program.title',
description: 'domains/academic/seo.study_program.description',
keywords: 'domains/academic/seo.study_program.keywords'
)]
#[LayoutData(
header: 'domains/academic/seo.study-program.title',
header: 'domains/academic/seo.study_program.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'domains/academic/seo.study-program.title' => '',
'ui/menu.dashboard' => 'dashboard',
'domains/academic/seo.study_program.title' => '',
],
)]
public function __invoke(StudyProgramDataTable $dataTable)

2
app/Http/Controllers/Web/Academic/TermController.php

@ -21,7 +21,7 @@ class TermController extends Controller
#[LayoutData(
header: 'domains/academic/seo.term.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'ui/menu.dashboard' => 'dashboard',
'domains/academic/seo.term.title' => '',
],
)]

2
app/Http/Controllers/Web/Account/ProfileController.php

@ -20,7 +20,7 @@ class ProfileController extends Controller
#[LayoutData(
header: 'domains/account/seo.profile.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'ui/menu.dashboard' => 'dashboard',
'domains/account/seo.profile.title' => '',
],
)]

12
app/Http/Controllers/Web/Admission/AcademicProgramController.php

@ -14,15 +14,15 @@ class AcademicProgramController extends Controller
* Handle the incoming request.
*/
#[Seo(
title: 'domains/admission/seo.academic-program.title',
description: 'domains/admission/seo.academic-program.description',
keywords: 'domains/admission/seo.academic-program.keywords'
title: 'domains/admission/seo.academic_program.title',
description: 'domains/admission/seo.academic_program.description',
keywords: 'domains/admission/seo.academic_program.keywords'
)]
#[LayoutData(
header: 'domains/admission/seo.academic-program.title',
header: 'domains/admission/seo.academic_program.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'domains/admission/seo.academic-program.title' => '',
'ui/menu.dashboard' => 'dashboard',
'domains/admission/seo.academic_program.title' => '',
],
)]
public function __invoke(AcademicProgramDataTable $dataTable)

12
app/Http/Controllers/Web/Admission/AdmissionScheduleController.php

@ -14,15 +14,15 @@ class AdmissionScheduleController extends Controller
* Handle the incoming request.
*/
#[Seo(
title: 'domains/admission/seo.admission-schedule.title',
description: 'domains/admission/seo.admission-schedule.description',
keywords: 'domains/admission/seo.admission-schedule.keywords'
title: 'domains/admission/seo.admission_schedule.title',
description: 'domains/admission/seo.admission_schedule.description',
keywords: 'domains/admission/seo.admission_schedule.keywords'
)]
#[LayoutData(
header: 'domains/admission/seo.admission-schedule.title',
header: 'domains/admission/seo.admission_schedule.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'domains/admission/seo.admission-schedule.title' => '',
'ui/menu.dashboard' => 'dashboard',
'domains/admission/seo.admission_schedule.title' => '',
],
)]
public function __invoke(AdmissionScheduleDataTable $dataTable)

12
app/Http/Controllers/Web/Admission/AdmissionTrackController.php

@ -14,15 +14,15 @@ class AdmissionTrackController extends Controller
* Handle the incoming request.
*/
#[Seo(
title: 'domains/admission/seo.admission-track.title',
description: 'domains/admission/seo.admission-track.description',
keywords: 'domains/admission/seo.admission-track.keywords'
title: 'domains/admission/seo.admission_track.title',
description: 'domains/admission/seo.admission_track.description',
keywords: 'domains/admission/seo.admission_track.keywords'
)]
#[LayoutData(
header: 'domains/admission/seo.admission-track.title',
header: 'domains/admission/seo.admission_track.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'domains/admission/seo.admission-track.title' => '',
'ui/menu.dashboard' => 'dashboard',
'domains/admission/seo.admission_track.title' => '',
],
)]
public function __invoke(AdmissionTrackDataTable $dataTable)

12
app/Http/Controllers/Web/Admission/FeeTypeController.php

@ -14,15 +14,15 @@ class FeeTypeController extends Controller
* Handle the incoming request.
*/
#[Seo(
title: 'domains/admission/seo.fee-type.title',
description: 'domains/admission/seo.fee-type.description',
keywords: 'domains/admission/seo.fee-type.keywords'
title: 'domains/admission/seo.fee_type.title',
description: 'domains/admission/seo.fee_type.description',
keywords: 'domains/admission/seo.fee_type.keywords'
)]
#[LayoutData(
header: 'domains/admission/seo.fee-type.title',
header: 'domains/admission/seo.fee_type.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'domains/admission/seo.fee-type.title' => '',
'ui/menu.dashboard' => 'dashboard',
'domains/admission/seo.fee_type.title' => '',
],
)]
public function __invoke(FeeTypeDataTable $dataTable)

2
app/Http/Controllers/Web/Channel/ClientController.php

@ -21,7 +21,7 @@ class ClientController extends Controller
#[LayoutData(
header: 'domains/channel/seo.client.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'ui/menu.dashboard' => 'dashboard',
'domains/channel/seo.client.title' => '',
],
)]

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

@ -21,7 +21,7 @@ class ChartOfAccountController extends Controller
#[LayoutData(
header: 'domains/finance/seo.coa.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'ui/menu.dashboard' => 'dashboard',
'domains/finance/seo.coa.title' => '',
],
)]

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

@ -21,7 +21,7 @@ class InvoiceController extends Controller
#[LayoutData(
header: 'domains/finance/seo.invoice.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'ui/menu.dashboard' => 'dashboard',
'domains/finance/seo.invoice.title' => '',
],
)]

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

@ -14,15 +14,15 @@ 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'
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',
header: 'domains/finance/seo.product_mapping.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'domains/finance/seo.product-mapping.title' => '',
'ui/menu.dashboard' => 'dashboard',
'domains/finance/seo.product_mapping.title' => '',
],
)]
public function __invoke(ProductMappingDataTable $dataTable)

2
app/Http/Controllers/Web/Identity/RoleController.php

@ -15,7 +15,7 @@ class RoleController extends Controller
#[LayoutData(
header: 'domains/identity/seo.role.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'ui/menu.dashboard' => 'dashboard',
'domains/identity/seo.role.title' => '',
],
)]

2
app/Http/Controllers/Web/Identity/UserController.php

@ -15,7 +15,7 @@ class UserController extends Controller
#[LayoutData(
header: 'domains/identity/seo.user.title',
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
'ui/menu.dashboard' => 'dashboard',
'domains/identity/seo.user.title' => '',
],
)]

10
app/Http/DataTables/Academic/FacultyDataTable.php

@ -30,9 +30,9 @@ class FacultyDataTable extends DataTable
],
'delete' => [
'url' => null,
'title' => __('ui.button.delete'),
'message' => __('ui.confirmation.delete', ['resource' => __('resources.admission-schedule')]),
'success_message' => __('ui.crud.success.deleted', ['resource' => __('resources.admission-schedule')]),
'title' => __('ui/button.delete'),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.faculty')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.faculty')]),
'permission' => auth()->user()->can('delete', $faculty),
],
'table_name' => 'faculty-table',
@ -80,7 +80,7 @@ class FacultyDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
'fixedColumns' => [
'start' => 2,
@ -115,7 +115,7 @@ class FacultyDataTable extends DataTable
->width(10)
->title(__('domains/academic/field.faculty.code')),
Column::computed('action')
->title(__('ui.label.actions'))
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)

16
app/Http/DataTables/Academic/StudyProgramDataTable.php

@ -34,9 +34,9 @@ class StudyProgramDataTable extends DataTable
],
'delete' => [
'url' => null,
'title' => __('ui.button.delete'),
'message' => __('ui.confirmation.delete', ['resource' => __('resources.admission-schedule')]),
'success_message' => __('ui.crud.success.deleted', ['resource' => __('resources.admission-schedule')]),
'title' => __('ui/button.delete'),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.study_program')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.study_program')]),
'permission' => auth()->user()->can('delete', $studyProgram),
],
'table_name' => 'studyprogram-table',
@ -86,7 +86,7 @@ class StudyProgramDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
'fixedColumns' => [
'start' => 2,
@ -116,17 +116,17 @@ class StudyProgramDataTable extends DataTable
->width(10)
->title('#'),
Column::make('name')
->title(__('domains/academic/field.study-program.name')),
->title(__('domains/academic/field.study_program.name')),
Column::computed('code')
->width(50)
->title(__('domains/academic/field.study-program.code')),
->title(__('domains/academic/field.study_program.code')),
Column::make('faculty_name')
->title(__('domains/academic/field.faculty.name')),
Column::computed('status')
->width(50)
->title(__('domains/academic/field.study-program.status')),
->title(__('domains/academic/field.study_program.status')),
Column::computed('action')
->title(__('ui.label.actions'))
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)

10
app/Http/DataTables/Academic/TermDataTable.php

@ -30,9 +30,9 @@ class TermDataTable extends DataTable
],
'delete' => [
'url' => null,
'title' => __('ui.button.delete'),
'message' => __('ui.confirmation.delete', ['resource' => __('resources.admission-schedule')]),
'success_message' => __('ui.crud.success.deleted', ['resource' => __('resources.admission-schedule')]),
'title' => __('ui/button.delete'),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.term')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.term')]),
'permission' => auth()->user()->can('delete', $term),
],
'table_name' => 'term-table',
@ -80,7 +80,7 @@ class TermDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
'fixedColumns' => [
'start' => 2,
@ -127,7 +127,7 @@ class TermDataTable extends DataTable
->width(150)
->title(__('domains/academic/field.term.date_end')),
Column::computed('action')
->title(__('ui.label.actions'))
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)

16
app/Http/DataTables/Admission/AcademicProgramDataTable.php

@ -34,9 +34,9 @@ class AcademicProgramDataTable extends DataTable
],
'delete' => [
'url' => null,
'title' => __('ui.button.delete'),
'message' => __('ui.confirmation.delete', ['resource' => __('resources.academic_program')]),
'success_message' => __('ui.crud.success.deleted', ['resource' => __('resources.academic_program')]),
'title' => __('ui/button.delete'),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.academic_program')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.academic_program')]),
'permission' => auth()->user()->can('delete', $academicProgram),
],
'table_name' => 'academicprogram-table',
@ -84,7 +84,7 @@ class AcademicProgramDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
'fixedColumns' => [
'start' => 2,
@ -114,14 +114,14 @@ class AcademicProgramDataTable extends DataTable
->width(10)
->title('#'),
Column::make('code')
->title(__('domains/admission/field.academic-program.code')),
->title(__('domains/admission/field.academic_program.code')),
Column::make('name')
->title(__('domains/admission/field.academic-program.name')),
->title(__('domains/admission/field.academic_program.name')),
Column::make('status')
->width(100)
->title(__('domains/admission/field.academic-program.status')),
->title(__('domains/admission/field.academic_program.status')),
Column::computed('action')
->title(__('ui.label.actions'))
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)

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

@ -34,9 +34,9 @@ class AdmissionScheduleDataTable extends DataTable
],
'delete' => [
'url' => null,
'title' => __('ui.button.delete'),
'message' => __('ui.confirmation.delete', ['resource' => __('resources.admission-schedule')]),
'success_message' => __('ui.crud.success.deleted', ['resource' => __('resources.admission-schedule')]),
'title' => __('ui/button.delete'),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.admission_schedule')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.admission_schedule')]),
'permission' => auth()->user()->can('delete', $admissionSchedule),
],
'table_name' => 'admissionschedule-table',
@ -87,7 +87,7 @@ class AdmissionScheduleDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
'fixedColumns' => [
'start' => 2,
@ -118,18 +118,18 @@ class AdmissionScheduleDataTable extends DataTable
->title('#'),
Column::make('admission_track_name')
->width(450)
->title(__('domains/admission/field.admission-schedule.track_name')),
->title(__('domains/admission/field.admission_schedule.track_name')),
Column::make('term_name')
->width(250)
->title(__('domains/admission/field.admission-schedule.term_name')),
->title(__('domains/admission/field.admission_schedule.term_name')),
Column::computed('date_start')
->width(150)
->title(__('domains/admission/field.admission-schedule.date-start')),
->title(__('domains/admission/field.admission_schedule.date_start')),
Column::computed('date_end')
->width(150)
->title(__('domains/admission/field.admission-schedule.date-end')),
->title(__('domains/admission/field.admission_schedule.date_end')),
Column::computed('action')
->title(__('ui.label.actions'))
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->addClass('text-center'),

14
app/Http/DataTables/Admission/AdmissionTrackDataTable.php

@ -34,9 +34,9 @@ class AdmissionTrackDataTable extends DataTable
],
'delete' => [
'url' => null,
'title' => __('ui.button.delete'),
'message' => __('ui.confirmation.delete', ['resource' => __('resources.admission-track')]),
'success_message' => __('ui.crud.success.deleted', ['resource' => __('resources.admission-track')]),
'title' => __('ui/button.delete'),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.admission_track')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.admission_track')]),
'permission' => auth()->user()->can('delete', $admissionTrack),
],
'table_name' => 'admissiontrack-table',
@ -84,7 +84,7 @@ class AdmissionTrackDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
'fixedColumns' => [
'start' => 2,
@ -114,12 +114,12 @@ class AdmissionTrackDataTable extends DataTable
->width(10)
->title('#'),
Column::make('name')
->title(__('domains/admission/field.admission-track.name')),
->title(__('domains/admission/field.admission_track.name')),
Column::computed('status')
->width(50)
->title(__('domains/admission/field.admission-track.status')),
->title(__('domains/admission/field.admission_track.status')),
Column::computed('action')
->title(__('ui.label.actions'))
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(30)

14
app/Http/DataTables/Admission/FeeTypeDataTable.php

@ -27,9 +27,9 @@ class FeeTypeDataTable extends DataTable
'log' => true,
'delete' => [
'url' => null,
'title' => __('ui.button.delete'),
'message' => __('ui.confirmation.delete', ['resource' => __('resources.fee-type')]),
'success_message' => __('ui.crud.success.deleted', ['resource' => __('resources.fee-type')]),
'title' => __('ui/button.delete'),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.fee_type')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.fee_type')]),
'permission' => auth()->user()->can('delete', $feeType),
],
'table_name' => 'feetype-table',
@ -77,7 +77,7 @@ class FeeTypeDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
'fixedColumns' => [
'start' => 2,
@ -107,11 +107,11 @@ class FeeTypeDataTable extends DataTable
->width(10)
->title('#'),
Column::make('name')
->title(__('domains/admission/field.fee-type.name')),
->title(__('domains/admission/field.fee_type.name')),
Column::make('billing_cycle')
->title(__('domains/admission/field.fee-type.billing_cycle')),
->title(__('domains/admission/field.fee_type.billing_cycle')),
Column::computed('action')
->title(__('ui.label.actions'))
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)

27
app/Http/DataTables/Channel/ClientDataTable.php

@ -20,9 +20,10 @@ class ClientDataTable extends DataTable
public function dataTable(QueryBuilder $query): EloquentDataTable
{
return (new EloquentDataTable($query))
->editColumn('auth_type', fn (Client $client) => $client->auth_type->label())
->addColumn(
'action',
fn ($client) => view('components.datatables.action-button', [
fn (Client $client) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'client-form-modal',
@ -30,9 +31,9 @@ class ClientDataTable extends DataTable
],
'delete' => [
'url' => null,
'title' => __('ui.button.delete'),
'message' => __('ui.confirmation.delete', ['resource' => __('resources.client')]),
'success_message' => __('ui.crud.success.deleted', ['resource' => __('resources.client')]),
'title' => __('ui/button.delete'),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.client')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.client')]),
'permission' => auth()->user()->can('delete', $client),
],
'table_name' => 'client-table',
@ -80,7 +81,7 @@ class ClientDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
'fixedColumns' => [
'start' => 2,
@ -94,6 +95,10 @@ class ClientDataTable extends DataTable
->action('$("#client-form-modal").modal("show");')
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make()
->text(svg('tabler-api', ['width' => 16, 'height' => 16])->toHtml())
->action('$("#api-form-modal").modal("show");')
->addClass('btn-sm btn-info'),
Button::make('reload')
->text(svg('tabler-reload', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
@ -111,12 +116,14 @@ class ClientDataTable extends DataTable
->title('#'),
Column::make('name')
->title(__('domains/channel/field.client.name')),
Column::make('code')
->title(__('domains/channel/field.client.code')),
Column::make('url')
->title(__('domains/channel/field.client.url')),
Column::make('domain')
->title(__('domains/channel/field.client.domain')),
Column::make('webhook')
->title(__('domains/channel/field.client.webhook')),
Column::make('auth_type')
->title(__('domains/channel/field.client.auth_type')),
Column::computed('action')
->title(__('ui.label.actions'))
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)

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

@ -25,14 +25,14 @@ class ChartOfAccountDataTable extends DataTable
fn ($coa) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'coa-form-modal',
'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')]),
'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',
@ -80,7 +80,7 @@ class ChartOfAccountDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
'fixedColumns' => [
'start' => 2,
@ -91,7 +91,7 @@ class ChartOfAccountDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#coa-form-modal").modal("show");')
->action('$("#chartofaccount-form-modal").modal("show");')
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')
@ -119,7 +119,7 @@ class ChartOfAccountDataTable extends DataTable
->width(100)
->title(__('domains/finance/field.coa.status')),
Column::computed('action')
->title(__('ui.label.actions'))
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)

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

@ -61,7 +61,7 @@ class InvoiceDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
'fixedColumns' => [
'start' => 2,

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

@ -30,9 +30,9 @@ class ProductMappingDataTable extends DataTable
],
'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')]),
'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',
@ -80,7 +80,7 @@ class ProductMappingDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
'fixedColumns' => [
'start' => 2,
@ -118,7 +118,7 @@ class ProductMappingDataTable extends DataTable
Column::make('client.name')
->title(__('domains/finance/field.product_mapping.client_name')),
Column::computed('action')
->title(__('ui.label.actions'))
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)

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

@ -37,12 +37,12 @@ class RoleDataTable extends DataTable
],
'delete' => [
'url' => null,
'title' => __('ui.button.delete'),
'title' => __('ui/button.delete'),
'permission' => $role->name == RoleType::SYSTEM_ADMIN->value
? false
: auth()->user()->can('delete', $role),
'message' => __('ui.confirmation.delete', ['resource' => __('resources.role')]),
'success_message' => __('ui.crud.success.deleted', ['resource' => __('resources.role')]),
'message' => __('ui/confirmation.delete', ['resource' => __('resources.role')]),
'success_message' => __('ui/crud.success.deleted', ['resource' => __('resources.role')]),
],
'table_name' => 'role-table',
'id' => $role->ulid,
@ -87,7 +87,7 @@ class RoleDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
])
->buttons([
@ -118,7 +118,7 @@ class RoleDataTable extends DataTable
Column::computed('guard_name')
->title(__('domains/identity/field.role.guard_name')),
Column::computed('action')
->title(__('ui.label.actions'))
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)

18
app/Http/DataTables/Identity/UserDataTable.php

@ -46,14 +46,14 @@ class UserDataTable extends DataTable
'delete' => [
'url' => null,
'title' => $user->status->isActive()
? __('ui.button.suspend')
: __('ui.button.delete'),
? __('ui/button.suspend')
: __('ui/button.delete'),
'message' => $user->status->isActive()
? __('ui.confirmation.suspend', ['resource' => __('resources.user')])
: __('ui.confirmation.delete', ['resource' => __('resources.user')]),
? __('ui/confirmation.suspend', ['resource' => __('resources.user')])
: __('ui/confirmation.delete', ['resource' => __('resources.user')]),
'success_message' => $user->status->isActive()
? __('ui.crud.success.suspended', ['resource' => __('resources.user')])
: __('ui.crud.success.deleted', ['resource' => __('resources.user')]),
? __('ui/crud.success.suspended', ['resource' => __('resources.user')])
: __('ui/crud.success.deleted', ['resource' => __('resources.user')]),
'permission' => auth()->user()->can('delete', $user),
],
'table_name' => 'user-table',
@ -135,7 +135,7 @@ class UserDataTable extends DataTable
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui.button.lookup'),
'searchPlaceholder' => __('ui/button.lookup'),
],
'fixedColumns' => [
'start' => 2,
@ -155,7 +155,7 @@ class UserDataTable extends DataTable
->action("Livewire.dispatch('export-excel')"),
Button::make('excel')
->text(svg('tabler-table-import', ['width' => 16, 'height' => 16])->toHtml())
->titleAttr(__('ui.title.import', ['resource' => 'Excel']))
->titleAttr(__('ui/title.import', ['resource' => 'Excel']))
->addClass('btn-sm')
->action("$('#excel-import-modal').modal('show')"),
Button::make('reload')
@ -183,7 +183,7 @@ class UserDataTable extends DataTable
Column::computed('status')
->title(__('domains/identity/field.user.status')),
Column::computed('action')
->title(__('ui.label.actions'))
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)

2
app/Http/Requests/Api/ApiRequest.php

@ -18,7 +18,7 @@ abstract class ApiRequest extends FormRequest
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(response()->json([
'message' => trans('ui.crud.error.validation_failed'),
'message' => trans('ui/crud.error.validation_failed'),
'errors' => $validator->errors(),
], 422));
}

8
app/Livewire/Concerns/WithModal.php

@ -25,7 +25,7 @@ trait WithModal
{
$resource = __('resources.'.$this->resourceName);
return __('ui.title.'.$this->mode, ['resource' => $resource]);
return __('ui/title.'.$this->mode, ['resource' => $resource]);
}
#[Computed]
@ -34,9 +34,9 @@ trait WithModal
$resource = __('resources.'.$this->resourceName);
return match ($this->mode) {
'create' => __('ui.crud.success.created', ['resource' => $resource]),
'update' => __('ui.crud.success.updated', ['resource' => $resource]),
default => __('ui.crud.success.deleted', ['resource' => $resource]),
'create' => __('ui/crud.success.created', ['resource' => $resource]),
'update' => __('ui/crud.success.updated', ['resource' => $resource]),
default => __('ui/crud.success.deleted', ['resource' => $resource]),
};
}
}

4
app/Livewire/Forms/Academic/FacultyForm.php

@ -2,6 +2,7 @@
namespace App\Livewire\Forms\Academic;
use App\Domains\System\Enums\LifecycleStatus;
use Livewire\Attributes\Validate;
use Livewire\Form;
@ -13,6 +14,9 @@ class FacultyForm extends Form
#[Validate('required|string|max:255', as: 'domains/academic/field.faculty.code')]
public string $code = '';
#[Validate('required')]
public LifecycleStatus $status = LifecycleStatus::ACTIVE;
#[Validate('nullable|string|max:255', as: 'domains/academic/field.faculty.external_id')]
public ?string $external_id = null;
}

43
app/Livewire/Forms/Channel/ClientForm.php

@ -2,6 +2,7 @@
namespace App\Livewire\Forms\Channel;
use App\Domains\Channel\Enums\Client\AuthType;
use Livewire\Attributes\Validate;
use Livewire\Form;
@ -10,9 +11,43 @@ class ClientForm extends Form
#[Validate('required|string|max:255', as: 'domains/channel/field.client.name')]
public string $name = '';
#[Validate('required|string|max:255', as: 'domains/channel/field.client.code')]
public string $code = '';
#[Validate('required|max:255', as: 'domains/channel/field.client.domain')]
public string $domain = '';
#[Validate('required|url|max:255', as: 'domains/channel/field.client.url')]
public string $url = '';
#[Validate('required', as: 'domains/channel/field.client.auth_type')]
public AuthType $auth_type = AuthType::NONE;
#[Validate(as: [
'credentials' => 'domains/channel/field.client.credentials',
'credentials.username' => 'domains/channel/field.client.credentials.username',
'credentials.password' => 'domains/channel/field.client.credentials.password',
'credentials.token' => 'domains/channel/field.client.credentials.token',
])]
public array $credentials = [];
#[Validate('required|max:255', as: 'domains/channel/field.client.webhook')]
public string $webhook = '';
public function updatingAuthType(): void
{
$this->reset('credentials');
}
public function rules(): array
{
if ($this->auth_type === AuthType::TOKEN) {
return [
'credentials.token' => 'required',
];
}
if ($this->auth_type == AuthType::BASIC) {
return [
'credentials.username' => 'required',
'credentials.password' => 'required',
];
}
return [];
}
}

19
app/UI/Actions/ApplyLayoutMetadata.php

@ -15,18 +15,11 @@ class ApplyLayoutMetadata
public function execute(LayoutData $attributeInstance, array|object $dataContext): void
{
$contextTarget = $attributeInstance->context;
// Use data_get to extract nested context if dot-notation is used (e.g., 'project.owner')
// Falls back to the entire data context if the target path isn't found or is null.
$resolvedContext = ! empty($contextTarget)
? data_get($dataContext, $contextTarget, $dataContext)
: $dataContext;
$breadcrumbs = [];
foreach ($attributeInstance->breadcrumbs as $labelKey => $routeConfig) {
// 1. Resolve the text label using our resolved nested context
$label = $this->textResolver->execute($labelKey, $resolvedContext, $contextTarget);
// 1. Resolve the text label (Supports translation keys or direct placeholders)
$label = $this->textResolver->execute($labelKey, $dataContext, $contextTarget);
$url = null;
if (! empty($routeConfig)) {
@ -39,8 +32,8 @@ class ApplyLayoutMetadata
$rawParams = $routeConfig[1] ?? [];
foreach ($rawParams as $paramKey => $paramValue) {
// Pass the nested context down to your string resolver
$resolvedValue = $this->textResolver->execute($paramValue, $resolvedContext, $contextTarget);
// Resolve internal variable bindings within parameters (e.g., "{user.id}" -> 4)
$resolvedValue = $this->textResolver->execute($paramValue, $dataContext, $contextTarget);
$routeParams[$paramKey] = $resolvedValue;
}
}
@ -64,10 +57,10 @@ class ApplyLayoutMetadata
];
}
// 3. Resolve layout headline text patterns using our nested context
// 3. Resolve layout headline text patterns
$header = null;
if ($attributeInstance->header) {
$header = $this->textResolver->execute($attributeInstance->header, $resolvedContext, $contextTarget);
$header = $this->textResolver->execute($attributeInstance->header, $dataContext, $contextTarget);
}
if (empty($header) && ! empty($breadcrumbs)) {

2
app/UI/Actions/ResolveDynamicText.php

@ -43,7 +43,7 @@ class ResolveDynamicText
private function resolveExplicitPlaceholders(string $text, array|object $data, ?string $contextTarget): string
{
preg_match_all('/\{([^}]+)\}/', $text, $matches);
preg_match_all('/\{([^}]+)}/', $text, $matches);
if (empty($matches[1])) {
return $text;
}

13
app/UI/Enums/Concerns/InteractsWithLabels.php

@ -19,13 +19,24 @@ trait InteractsWithLabels
{
$fqcn = static::class;
if (Str::startsWith($fqcn, 'App\\Domains\\')) {
$domain = Str::between($fqcn, 'App\\Domains\\', '\\Enums\\');
$domainSlug = Str::kebab($domain);
$className = Str::afterLast($fqcn, '\\');
$enumSlug = Str::kebab($className);
$enumSlug = Str::snake($className);
$translationKey = "domains/{$domainSlug}/enum.{$enumSlug}.{$this->value}";
} elseif (Str::startsWith($fqcn, 'App\\UI\\Enums\\')) {
$className = Str::afterLast($fqcn, '\\');
$enumSlug = Str::snake($className);
$translationKey = "ui/enum.{$enumSlug}.{$this->value}";
} else {
$className = Str::afterLast($fqcn, '\\');
$enumSlug = Str::snake($className);
$translationKey = "enum.{$enumSlug}.{$this->value}";
}
return __($translationKey);
}

10
app/UI/Enums/Contracts/HasSchema.php

@ -0,0 +1,10 @@
<?php
namespace App\UI\Enums\Contracts;
use App\UI\Support\Settings\BaseSchema;
interface HasSchema
{
public function schema(): BaseSchema;
}

6
app/UI/Enums/FileType.php

@ -2,8 +2,12 @@
namespace App\UI\Enums;
enum FileType: string
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
enum FileType: string implements HasLabel
{
use InteractsWithLabels;
case DOCUMENT = 'document';
case IMAGE = 'image';
case AUDIO = 'audio';

21
app/UI/Enums/InputType.php

@ -2,11 +2,26 @@
namespace App\UI\Enums;
enum InputType: string
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
/**
* @method bool isNumber()
* @method bool isTextLine()
* @method bool isTextArea()
* @method bool isSelect()
* @method bool isFile()
* @method bool isCheckbox()
*/
enum InputType: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case NUMBER = 'number';
case TEXTLINE = 'input';
case TEXTAREA = 'textarea';
case TEXTLINE = 'text_line';
case TEXTAREA = 'text_area';
case SELECT = 'select';
case FILE = 'file';
case CHECKBOX = 'checkbox';

5
app/UI/Support/Settings/BaseSchema.php

@ -0,0 +1,5 @@
<?php
namespace App\UI\Support\Settings;
interface BaseSchema {}

44
app/UI/Support/Settings/SettingSchema.php

@ -0,0 +1,44 @@
<?php
namespace App\UI\Support\Settings;
use App\UI\Enums\InputType;
readonly class SettingSchema implements BaseSchema
{
public function __construct(
public InputType $type,
public array $rules = ['required', 'string'],
public mixed $default = null,
public array $options = [],
public array $attributes = []
) {}
public static function make(InputType $type, array $rules = ['required', 'string']): self
{
return new self($type, $rules);
}
public function default(mixed $default): self
{
return new self($this->type, $this->rules, $default, $this->options, $this->attributes);
}
public function rules(array $rules): self
{
return new self($this->type, $rules, $this->default, $this->options, $this->attributes);
}
public function options(array $options): self
{
// If the array is simple (non-associative), we should try to localize the values if they are Enums
// But for now, we just pass it as is, or expect the caller to handle it.
// The common case is ['value' => 'Label']
return new self($this->type, $this->rules, $this->default, $options, $this->attributes);
}
public function attributes(array $attributes): self
{
return new self($this->type, $this->rules, $this->default, $this->options, $attributes);
}
}

959
composer.lock

File diff suppressed because it is too large

1
database/migrations/2026_05_01_172941_create_system_settings_table.php

@ -15,6 +15,7 @@ return new class extends Migration
$table->id();
$table->string('key')->unique();
$table->text('value')->nullable();
$table->text('type');
});
}

8
database/migrations/2026_07_06_031003_create_clients_table.php

@ -15,9 +15,11 @@ return new class extends Migration
$table->id();
$table->ulid();
$table->string('name');
$table->string('url')->nullable();
$table->string('code')->nullable();
$table->string('secret');
$table->string('domain');
$table->text('secret');
$table->text('auth_type');
$table->text('webhook');
$table->longText('credentials');
$table->timestamps();
});
}

30
database/migrations/2026_07_31_035159_create_api_endpoints_table.php

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('api_endpoints', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->string('value');
$table->foreignId('client_id')->constrained();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('api_endpoints');
}
};

10
database/seeders/SystemSettingSeeder.php

@ -2,6 +2,7 @@
namespace Database\Seeders;
use App\Domains\Channel\Enums\Api\ApiType;
use App\Domains\System\Enums\SystemSettingKey;
use App\Domains\System\Models\SystemSettings;
use Illuminate\Database\Seeder;
@ -18,6 +19,15 @@ class SystemSettingSeeder extends Seeder
SystemSettings::create([
'key' => $setting->value,
'value' => $setting->default(),
'type' => SystemSettingKey::class,
]);
}
foreach (ApiType::cases() as $sync) {
SystemSettings::create([
'key' => $sync->value,
'value' => '-',
'type' => ApiType::class,
]);
}
}

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

@ -6,7 +6,7 @@ return [
'code' => 'Faculty Code',
'status' => 'Status',
],
'study-program' => [
'study_program' => [
'name' => 'Study Program Name',
'code' => 'Study Program Code',
'status' => 'Status',

2
lang/en/domains/academic/seo.php

@ -6,7 +6,7 @@ return [
'description' => 'Manage faculty data in the system.',
'keywords' => 'faculty, academic, university',
],
'study-program' => [
'study_program' => [
'title' => 'Study Program Data',
'description' => 'Manage study program data in the system.',
'keywords' => 'study program, academic, university',

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

Loading…
Cancel
Save