Browse Source

refactor: implement dynamic InputSchema UI and Channel API synchronization

- Introduce generic InputSchema and InputSchemaField for dynamic UI management.
- Replace legacy SettingSchema with new schema-based system across multiple domains.
- Add Channel API synchronization logic with SynchronizeWithApi and SendWebhook actions.
- Refactor authentication credentials to use typed DTOs (AuthBasicDTO, AuthBearerDTO).
- Update Finance and System domains to support the new schema architecture.
- Standardize CoreUI attributes and form component bindings.
master
Syarif Ubaidillah 4 weeks ago
parent
commit
79ee6b2e5d
  1. 86
      README.md
  2. 2
      app/Domains/Academic/Queries/Selection/SelectStudyProgramQuery.php
  3. 1
      app/Domains/Admission/Actions/IntakeScheduling/DefineAdmissionSchedule.php
  4. 1
      app/Domains/Admission/DTOs/IntakeScheduling/DefineAdmissionScheduleDTO.php
  5. 2
      app/Domains/Admission/Queries/Selection/SelectFeeTypeQuery.php
  6. 5
      app/Domains/Channel/Actions/Api/AssignEndpointUrl.php
  7. 11
      app/Domains/Channel/Actions/Api/SendWebhook.php
  8. 28
      app/Domains/Channel/Actions/Api/SynchronizeWithApi.php
  9. 2
      app/Domains/Channel/Actions/AppProvisioning/ModifyConsumerAppDetail.php
  10. 3
      app/Domains/Channel/DTOs/AppProvisioning/ModifyConsumerAppDetailDTO.php
  11. 3
      app/Domains/Channel/DTOs/AppProvisioning/ProvisionConsumerAppDTO.php
  12. 27
      app/Domains/Channel/DTOs/AppProvisioning/Type/AuthBasicDTO.php
  13. 24
      app/Domains/Channel/DTOs/AppProvisioning/Type/AuthBearerDTO.php
  14. 10
      app/Domains/Channel/DTOs/AppProvisioning/Type/AuthCredentials.php
  15. 44
      app/Domains/Channel/Enums/Api/ApiType.php
  16. 27
      app/Domains/Channel/Enums/Client/AuthType.php
  17. 2
      app/Domains/Finance/Actions/ProductConfiguration/RegisterProductMapping.php
  18. 16
      app/Domains/Finance/Enums/InvoiceType.php
  19. 77
      app/Domains/Finance/Schemas/Invoice/StudentInvoiceSchema.php
  20. 67
      app/Domains/Finance/Support/Schema/InvoiceSchema.php
  21. 25
      app/Domains/Finance/Support/Schemas/InvoiceSchema.php
  22. 21
      app/Domains/Identity/Enums/UserSettingKey.php
  23. 3
      app/Domains/System/Actions/Settings/UpdateSettings.php
  24. 75
      app/Domains/System/Enums/SystemSettingKey.php
  25. 7
      app/Domains/System/Models/SystemSettings.php
  26. 6
      app/Domains/System/Queries/GetSystemSettings.php
  27. 3
      app/Http/DataTables/Academic/FacultyDataTable.php
  28. 3
      app/Http/DataTables/Academic/StudyProgramDataTable.php
  29. 3
      app/Http/DataTables/Academic/TermDataTable.php
  30. 3
      app/Http/DataTables/Admission/AcademicProgramDataTable.php
  31. 3
      app/Http/DataTables/Admission/AdmissionScheduleDataTable.php
  32. 3
      app/Http/DataTables/Admission/AdmissionTrackDataTable.php
  33. 3
      app/Http/DataTables/Admission/FeeTypeDataTable.php
  34. 6
      app/Http/DataTables/Channel/ClientDataTable.php
  35. 3
      app/Http/DataTables/Finance/ChartOfAccountDataTable.php
  36. 3
      app/Http/DataTables/Finance/InvoiceDataTable.php
  37. 3
      app/Http/DataTables/Finance/ProductMappingDataTable.php
  38. 3
      app/Http/DataTables/Identity/RoleDataTable.php
  39. 6
      app/Http/DataTables/Identity/UserDataTable.php
  40. 6
      app/Livewire/Forms/Channel/ClientForm.php
  41. 43
      app/Livewire/Forms/Finance/InvoiceForm.php
  42. 1
      app/Livewire/Forms/Finance/PaymentForm.php
  43. 5
      app/UI/Enums/Contracts/HasSchema.php
  44. 82
      app/UI/Support/Schemas/AbstractGroupSchema.php
  45. 25
      app/UI/Support/Schemas/AbstractSchema.php
  46. 22
      app/UI/Support/Schemas/BaseSchema.php
  47. 160
      app/UI/Support/Schemas/InputSchema.php
  48. 20
      app/UI/Support/Schemas/InputSchemaField.php
  49. 5
      app/UI/Support/Settings/BaseSchema.php
  50. 122
      app/UI/Support/Settings/SettingSchema.php
  51. 1
      database/migrations/2026_05_01_172941_create_system_settings_table.php
  52. 12
      database/seeders/SystemSettingSeeder.php
  53. 2
      lang/en/domains/channel/enum.php
  54. 6
      lang/en/domains/finance/field.php
  55. 1
      lang/en/ui/button.php
  56. 2
      lang/id/domains/channel/enum.php
  57. 17
      lang/id/domains/finance/field.php
  58. 6
      resources/js/alpine/bs.js
  59. 22
      resources/views/components/datatables/action-button.blade.php
  60. 2
      resources/views/components/form/checkbox.blade.php
  61. 2
      resources/views/components/form/select.blade.php
  62. 5
      resources/views/components/modal.blade.php
  63. 20
      resources/views/pages/account/profile/⚡user-settings/user-settings.php
  64. 3
      resources/views/pages/channel/client/⚡api-form-modal/api-form-modal.blade.php
  65. 12
      resources/views/pages/channel/client/⚡api-form-modal/api-form-modal.php
  66. 6
      resources/views/pages/channel/client/⚡form-modal/form-modal.php
  67. 6
      resources/views/pages/finance/invoice/⚡form-modal/form-modal.blade.php
  68. 5
      resources/views/pages/finance/invoice/⚡form-modal/form-modal.php
  69. 8
      resources/views/pages/system/settings/⚡setting-list/setting-list.blade.php
  70. 6
      resources/views/pages/system/settings/⚡update-setting-modal/update-setting-modal.blade.php
  71. 17
      resources/views/pages/system/settings/⚡update-setting-modal/update-setting-modal.php

86
README.md

@ -168,6 +168,16 @@ app/
│ ├── Listeners/
│ ├── Models/
│ └── Providers/
├── Finance/ <-- Business Concept: Financial Operations
│ ├── Actions/
│ ├── Casts/
│ ├── DTOs/
│ ├── Enums/
│ ├── Models/
│ ├── Policies/
│ ├── Providers/
│ ├── Schemas/
│ └── Support/
└── System/ <-- Business Concept: Cross-cutting Infrastructure
├── Actions/
├── Casts/ <-- Custom Eloquent casts
@ -487,7 +497,81 @@ All critical database mutations are tracked to maintain a compliant historical l
---
## 16. Dynamic UIs & Livewire Interoperability
## 16. Dynamic UIs & Schema Management
To ensure consistent and type-safe form generation across the application, we use a schema-based approach. This separates the definition of UI fields from the rendering logic, primarily used for dynamic settings and complex forms.
### 16.1 Core Schema Components
The system relies on a combination of interfaces and classes to define UI components:
* **`BaseSchema` (Interface)**: The contract for any schema-aware class. It requires a `make()` method and serves as the baseline for both individual field schemas and group schemas.
* **`SettingSchema` (Implementation)**: A versatile class used to define individual field attributes, validation rules, type-specific logic, and options (like dropdown values).
* **Domain Schemas (e.g., `InvoiceSchema`)**: Classes that implement `BaseSchema` to group multiple `SettingSchema` instances into a cohesive business unit (like a form).
### 16.2 Defining a Schema
Schemas are defined using a fluent API that allows for expressive configuration of UI fields.
```php
use App\Domains\Finance\Support\Schema\InvoiceSchema;
use App\UI\Support\Schemas\SettingSchema;
use App\UI\Enums\InputType;
// Define fields within an InvoiceSchema
$schema = InvoiceSchema::make()
->addSchema(
key: 'invoice_number',
label: 'Invoice Number',
schema: SettingSchema::make(InputType::TEXT)
->default('INV-' . date('Ymd'))
->rules(['required', 'string', 'max:20'])
)
->addSchema(
key: 'category_id',
label: 'Category',
schema: SettingSchema::make(InputType::SELECT)
->options(fn($context) => Category::pluck('name', 'id'))
->live() // Triggers Livewire update on change
);
```
### 16.3 SettingSchema Capabilities
The `SettingSchema` provides several methods to tune field behavior:
| Method | Purpose |
| --- | --- |
| `make(InputType $type)` | Static constructor accepting an `InputType` enum. |
| `default(mixed $value)` | Sets the initial/default value for the field. |
| `rules(array $rules)` | Defines Laravel validation rules. |
| `options(array|Closure $options)` | Provides data for select/radio/checkbox fields. Closures receive `$context`. |
| `attributes(array $attr)` | Passes raw HTML attributes to the Blade component. |
| `live()` | Marks the field as "live," useful for dynamic updates in Livewire. |
| `dependsOn(string $key)` | Establishes a dependency on another field for conditional logic. |
| `select2(array $config)` | Helper to quickly configure Select2 JS attributes. |
### 16.4 Schema Resolution & UI Rendering
Once a schema is defined, it is resolved into a standardized object structure that Blade components can iterate over.
```php
// In a Livewire component or Controller
$fields = $schema->withContext(['user_id' => $id])->getSchema();
// In Blade
@foreach($fields as $key => $field)
<x-dynamic-component
:component="$field->schema->type->getComponent()"
:attributes="$field->attributes"
wire:model="form.{{ $key }}"
/>
@endforeach
```
---
## 17. Dynamic UIs & Livewire Interoperability
When building data-driven interfaces (like dynamic settings forms), we utilize the **Renderable Enum** pattern combined with Laravel's native dynamic components.

2
app/Domains/Academic/Queries/Selection/SelectStudyProgramQuery.php

@ -10,7 +10,7 @@ class SelectStudyProgramQuery
public static function fetch(array $context = []): Collection
{
if (! isset($context['faculty'])) {
return new Collection();
return new Collection;
}
return once(fn () => StudyProgram::select('id', 'name')

1
app/Domains/Admission/Actions/IntakeScheduling/DefineAdmissionSchedule.php

@ -4,7 +4,6 @@ namespace App\Domains\Admission\Actions\IntakeScheduling;
use App\Domains\Admission\DTOs\IntakeScheduling\DefineAdmissionScheduleDTO;
use App\Domains\Admission\Enums\AdmissionStatus;
use App\Domains\Admission\Events\IntakeScheduling\AdmissionScheduleDefined;
use App\Domains\Admission\Models\AdmissionSchedule;
class DefineAdmissionSchedule

1
app/Domains/Admission/DTOs/IntakeScheduling/DefineAdmissionScheduleDTO.php

@ -3,7 +3,6 @@
namespace App\Domains\Admission\DTOs\IntakeScheduling;
use Carbon\CarbonImmutable;
use Illuminate\Contracts\Support\Arrayable;
readonly class DefineAdmissionScheduleDTO
{

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

@ -8,6 +8,6 @@ class SelectFeeTypeQuery
{
public function fetch(): Collection
{
return new Collection();
return new Collection;
}
}

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

@ -10,19 +10,20 @@ 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));
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
'value' => $endpoint->endpointUrl,
], $endpointToSave);
ApiEndpoint::upsert($toSave, ['client_id', 'key']);
}

11
app/Domains/Channel/Actions/Api/SendWebhook.php

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

28
app/Domains/Channel/Actions/Api/SynchronizeWithApi.php

@ -0,0 +1,28 @@
<?php
namespace App\Domains\Channel\Actions\Api;
use App\Domains\Channel\Enums\Api\ApiType;
use App\Domains\Channel\Models\ApiEndpoint;
use Illuminate\Support\Facades\Http;
class SynchronizeWithApi
{
public function execute(ApiType $apiType): void
{
$endpoint = ApiEndpoint::query()
->with('client')
->where('key', $apiType)
->first();
$domain = $endpoint->client->domain;
$url = $endpoint->value;
$credentials = $endpoint->client->credentials;
$authType = $endpoint->client->auth_type;
$url = "https://$domain$url";
$header = $authType->buildCredentials($credentials);
$response = Http::withHeaders($header)->get($url);
dd($response);
}
}

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

@ -12,7 +12,7 @@ class ModifyConsumerAppDetail
$client->update([
'webhook' => $dto->webhook_url,
'auth_type' => $dto->auth_type,
'credentials' => $dto->credentials,
'credentials' => $dto->credentials->toArray(),
]);
}
}

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

@ -2,6 +2,7 @@
namespace App\Domains\Channel\DTOs\AppProvisioning;
use App\Domains\Channel\DTOs\AppProvisioning\Type\AuthCredentials;
use App\Domains\Channel\Enums\Client\AuthType;
readonly class ModifyConsumerAppDetailDTO
@ -9,6 +10,6 @@ readonly class ModifyConsumerAppDetailDTO
public function __construct(
public string $webhook_url,
public AuthType $auth_type,
public array $credentials,
public AuthCredentials $credentials,
) {}
}

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

@ -2,6 +2,7 @@
namespace App\Domains\Channel\DTOs\AppProvisioning;
use App\Domains\Channel\DTOs\AppProvisioning\Type\AuthCredentials;
use App\Domains\Channel\Enums\Client\AuthType;
readonly class ProvisionConsumerAppDTO
@ -14,6 +15,6 @@ readonly class ProvisionConsumerAppDTO
public string $domain,
public string $webhook_url,
public AuthType $auth_type,
public array $credentials,
public AuthCredentials $credentials,
) {}
}

27
app/Domains/Channel/DTOs/AppProvisioning/Type/AuthBasicDTO.php

@ -0,0 +1,27 @@
<?php
namespace App\Domains\Channel\DTOs\AppProvisioning\Type;
class AuthBasicDTO implements AuthCredentials
{
public function __construct(
public string $username,
public string $password,
) {}
public static function fromArray(array $data): static
{
return new self(
username: $data['username'],
password: $data['password'],
);
}
public function toArray(): array
{
return [
'username' => $this->username,
'password' => $this->password,
];
}
}

24
app/Domains/Channel/DTOs/AppProvisioning/Type/AuthBearerDTO.php

@ -0,0 +1,24 @@
<?php
namespace App\Domains\Channel\DTOs\AppProvisioning\Type;
class AuthBearerDTO implements AuthCredentials
{
public function __construct(
public string $token,
) {}
public static function fromArray(array $data): static
{
return new self(
token: $data['token'],
);
}
public function toArray(): array
{
return [
'token' => $this->token,
];
}
}

10
app/Domains/Channel/DTOs/AppProvisioning/Type/AuthCredentials.php

@ -0,0 +1,10 @@
<?php
namespace App\Domains\Channel\DTOs\AppProvisioning\Type;
interface AuthCredentials
{
public static function fromArray(array $data): static;
public function toArray(): array;
}

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

@ -5,7 +5,7 @@ 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;
use App\UI\Support\Schemas\InputSchema;
enum ApiType: string implements HasLabel
{
@ -18,46 +18,4 @@ enum ApiType: string implements HasLabel
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,
],
],
];
}
}

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

@ -2,6 +2,9 @@
namespace App\Domains\Channel\Enums\Client;
use App\Domains\Channel\DTOs\AppProvisioning\Type\AuthBasicDTO;
use App\Domains\Channel\DTOs\AppProvisioning\Type\AuthBearerDTO;
use App\Domains\Channel\DTOs\AppProvisioning\Type\AuthCredentials;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
@ -13,5 +16,27 @@ enum AuthType: string implements HasLabel
case NONE = 'none';
case BASIC = 'basic';
case TOKEN = 'token';
case BEARER = 'token';
public function buildCredentials(array $credentials): array
{
return match ($this) {
self::NONE => [],
self::BASIC => [
'Authorization' => 'Basic ' . base64_encode($credentials['username'] . ':' . $credentials['password']),
],
self::BEARER => [
'Authorization' => 'Bearer ' . $credentials['token'] ?? '',
]
};
}
public function transformCredentials(array $credentials): ?AuthCredentials
{
return match ($this) {
self::NONE => null,
self::BASIC => AuthBasicDTO::fromArray($credentials),
self::BEARER => AuthBearerDTO::fromArray($credentials),
};
}
}

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

@ -12,7 +12,7 @@ class RegisterProductMapping
ProductMapping::create([
'name' => $dto->name,
'code' => $dto->code,
'chart_of_account_id' => $dto->coaId
'chart_of_account_id' => $dto->coaId,
]);
}
}

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

@ -5,11 +5,12 @@ namespace App\Domains\Finance\Enums;
use App\Domains\Finance\DTOs\PaymentProcessing\Detail\InvoiceDetailDTO;
use App\Domains\Finance\DTOs\PaymentProcessing\Detail\StudentInvoiceDetailDTO;
use App\Domains\Finance\Schemas\Invoice\StudentInvoiceSchema;
use App\Domains\Finance\Support\Schema\InvoiceSchema;
use App\Domains\Finance\Support\Schemas\InvoiceSchema;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
use App\UI\Enums\Contracts\HasSchema;
use App\UI\Support\Schemas\InputSchemaField;
enum InvoiceType: string implements HasLabel, HasSchema
{
@ -24,7 +25,7 @@ enum InvoiceType: string implements HasLabel, HasSchema
{
return match ($this) {
self::STUDENT_INVOICE => StudentInvoiceSchema::make(),
default => InvoiceSchema::make(true),
default => InvoiceSchema::make()->withCoaVisibility(),
};
}
@ -32,6 +33,17 @@ enum InvoiceType: string implements HasLabel, HasSchema
{
return match ($this) {
self::STUDENT_INVOICE => StudentInvoiceDetailDTO::fromArray($data),
default => false
};
}
/**
* Resolve all field schemas, keyed by their input keys.
*
* @return array<string, InputSchemaField>
*/
public function getSchema(): array
{
return $this->schema()->getSchema();
}
}

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

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

67
app/Domains/Finance/Support/Schema/InvoiceSchema.php

@ -1,67 +0,0 @@
<?php
namespace App\Domains\Finance\Support\Schema;
use App\UI\Enums\InputType;
use App\UI\Support\Settings\BaseSchema;
use App\UI\Support\Settings\SettingSchema;
class InvoiceSchema implements BaseSchema
{
protected array $context = [];
protected string $form = '';
public function __construct(
protected array $uiSchema = [],
protected bool $coaVisibility = false
){}
public static function make(bool $coaVisible = false): static
{
return new self([], $coaVisible);
}
public function addSchema(string$key, string $label, SettingSchema $schema): static
{
$schema = $schema->attributes(['label' => $label]);
$uiSchema = array_merge($this->uiSchema, [[
'key' => $key,
'schema' => $schema
]]);
return new self($uiSchema, $this->coaVisibility);
}
public function withContext(array $context): static
{
$this->context = $context;
return $this;
}
public function withFormString(string $form): static
{
$this->form = $form;
return $this;
}
public function getSchema(): array
{
$fields = [];
foreach($this->uiSchema as $field) {
/** @var SettingSchema $schema */
$schema = $field['schema'];
$attributes['options'] =$schema->type->isSelect() ? $schema->resolveOptions($this->context) : '';
$fields[$field['key']] = (object) [
'attributes' => array_merge($schema->attributes, $attributes),
'schema' => $schema,
];
}
return $fields;
}
public function isCoaVisible(): bool
{
return $this->coaVisibility;
}
}

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

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

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

@ -6,7 +6,8 @@ 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 App\UI\Support\Schemas\InputSchema;
use App\UI\Support\Schemas\InputSchemaField;
use Illuminate\Validation\Rule;
enum UserSettingKey: string implements HasLabel, HasSchema
@ -17,7 +18,7 @@ enum UserSettingKey: string implements HasLabel, HasSchema
case LANGUAGE = 'language';
case TIMEZONE = 'timezone';
public function schema(): SettingSchema
public function schema(): InputSchema
{
$options = match ($this) {
self::LANGUAGE => [
@ -42,8 +43,22 @@ enum UserSettingKey: string implements HasLabel, HasSchema
self::TIMEZONE => 'UTC',
};
return SettingSchema::make(InputType::SELECT, ['required', Rule::in(array_keys($options))])
return InputSchema::make()
->key($this->value)
->label($this->label())
->type(InputType::SELECT)
->rules(['required', Rule::in(array_keys($options))])
->default($default)
->options($options);
}
public function getSchema(): InputSchemaField
{
return $this->schema()->getSchema();
}
public function default(): mixed
{
return $this->getSchema()->default;
}
}

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

@ -11,8 +11,9 @@ class UpdateSettings
public function execute(SystemSetingDTO $dto): void
{
$value = $dto->value;
$schema = $dto->key->getSchema();
if ($dto->key->schema()->type->isFile()) {
if ($schema->type->isFile()) {
$currentSettings = SystemSettings::where('key', $dto->key->value)->value('value');
if ($currentSettings) {

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

@ -8,7 +8,8 @@ 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;
use App\UI\Support\Schemas\InputSchema;
use App\UI\Support\Schemas\InputSchemaField;
enum SystemSettingKey: string implements HasLabel, HasSchema
{
@ -30,7 +31,7 @@ enum SystemSettingKey: string implements HasLabel, HasSchema
/**
* Centralized Schema Definitions
*/
public function schema(): SettingSchema
public function schema(): InputSchema
{
$imageRules = ['required', 'file', 'mimetypes:'.implode(',', FileType::IMAGE->mimeType()), 'max:1024'];
$imageAttrs = [
@ -43,24 +44,50 @@ enum SystemSettingKey: string implements HasLabel, HasSchema
];
return match ($this) {
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_NAME => InputSchema::make()
->type(InputType::TEXTLINE)
->label($this->label())
->default('Acme Inc'),
self::WEB_DESCRIPTION => InputSchema::make()
->label($this->label())
->type(InputType::TEXTAREA),
self::WEB_ADDRESS => InputSchema::make()
->type(InputType::TEXTLINE)
->label($this->label())
->default('123 Main St, Anytown, USA'),
self::WEB_PHONE => InputSchema::make()
->type(InputType::TEXTLINE)
->label($this->label())
->default('+1234567890'),
self::WEB_EMAIL => InputSchema::make()
->type(InputType::TEXTLINE)
->label($this->label())
->default('acme@web.io'),
self::WEB_LOGO,
self::WEB_FAVICON => SettingSchema::make(InputType::FILE, $imageRules)->attributes($imageAttrs),
self::WEB_FAVICON => InputSchema::make()
->type(InputType::FILE)
->label($this->label())
->rules($imageRules)
->attributes($imageAttrs),
self::DEFAULT_LANGUAGE => SettingSchema::make(InputType::SELECT)
self::DEFAULT_LANGUAGE => InputSchema::make()
->type(InputType::SELECT)
->default('en')
->label($this->label())
->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)
self::TIMEZONE => InputSchema::make()
->type(InputType::SELECT)
->default('UTC')
->label($this->label())
->options([
'UTC' => __('domains/system/enum.system_setting_key_options.timezone.UTC'),
'Asia/Jakarta' => __('domains/system/enum.system_setting_key_options.timezone.Asia/Jakarta'),
@ -68,23 +95,13 @@ enum SystemSettingKey: string implements HasLabel, HasSchema
'Asia/Jayapura' => __('domains/system/enum.system_setting_key_options.timezone.Asia/Jayapura'),
]),
default => SettingSchema::make(InputType::TEXTLINE)->rules(['nullable', 'string']),
default => InputSchema::make()
->label($this->label())
->type(InputType::TEXTLINE)
->rules(['nullable', 'string']),
};
}
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 [
@ -100,4 +117,14 @@ enum SystemSettingKey: string implements HasLabel, HasSchema
],
];
}
public function getSchema(): InputSchemaField
{
return $this->schema()->getSchema();
}
public function getValidation(): array
{
return $this->schema()->getSchema()->rules;
}
}

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

@ -2,6 +2,7 @@
namespace App\Domains\System\Models;
use App\Domains\System\Enums\SystemSettingKey;
use Exception;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps;
@ -18,13 +19,13 @@ class SystemSettings extends Model
*/
public function getTranslatedValueAttribute(): ?string
{
$key = $this->attributes['type']::tryFrom($this->attributes['key']);
$key = SystemSettingKey::tryFrom($this->attributes['key']);
$schema = $key->schema();
$schema = $key->getSchema();
if ($schema->type->isFile() && isset($this->attributes['value'])) {
return asset_static($this->attributes['value']);
} elseif ($schema->type->isSelect()) {
return $schema->options[$this->attributes['value']];
return $schema->attributes['options'][$this->attributes['value']];
}
return $this->attributes['value'];

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

@ -12,7 +12,7 @@ class GetSystemSettings
public function get(SystemSettingKey $setting): ?string
{
return $this->fetch()[$setting->value] ?? $setting->default();
return $this->fetch()[$setting->value] ?? $setting->getSchema()->default;
}
public function fetch(): array
@ -22,10 +22,10 @@ class GetSystemSettings
}
$this->settings = Cache::rememberForever(SystemSettings::$cacheName, function () {
$settings = SystemSettings::where('type', SystemSettingKey::class)->pluck('value', 'key')->toArray();
$settings = SystemSettings::pluck('value', 'key')->toArray();
$finalSettings = [];
foreach (SystemSettingKey::cases() as $key) {
$finalSettings[$key->value] = $settings[$key->value] ?? $key->default();
$finalSettings[$key->value] = $settings[$key->value] ?? $key->getSchema()->default;
}
return $finalSettings;

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

@ -91,7 +91,8 @@ class FacultyDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#faculty-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#faculty-form-modal'])
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')

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

@ -97,7 +97,8 @@ class StudyProgramDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#studyprogram-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#studyprogram-form-modal'])
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')

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

@ -91,7 +91,8 @@ class TermDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#term-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#term-form-modal'])
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')

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

@ -95,7 +95,8 @@ class AcademicProgramDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#academicprogram-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#academicprogram-form-modal'])
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')

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

@ -98,7 +98,8 @@ class AdmissionScheduleDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#admissionschedule-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#admissionschedule-form-modal'])
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')

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

@ -95,7 +95,8 @@ class AdmissionTrackDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#admissiontrack-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#admissiontrack-form-modal'])
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')

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

@ -88,7 +88,8 @@ class FeeTypeDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#feetype-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#feetype-form-modal'])
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')

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

@ -92,12 +92,14 @@ class ClientDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#client-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#client-form-modal'])
->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");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#api-form-modal'])
->addClass('btn-sm btn-info'),
Button::make('reload')
->text(svg('tabler-reload', ['width' => 16, 'height' => 16])->toHtml())

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

@ -96,7 +96,8 @@ class ChartOfAccountDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#chartofaccount-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#chartofaccount-form-modal'])
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')

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

@ -72,7 +72,8 @@ class InvoiceDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#invoice-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#invoice-form-modal'])
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')

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

@ -91,7 +91,8 @@ class ProductMappingDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#product-mapping-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#product-mapping-form-modal'])
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')

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

@ -92,7 +92,8 @@ class RoleDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#role-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#role-form-modal'])
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')

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

@ -146,7 +146,8 @@ class UserDataTable extends DataTable
])
->buttons([
Button::make('add')
->action('$("#user-form-modal").modal("show");')
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#user-form-modal'])
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('excel')
@ -157,7 +158,8 @@ class UserDataTable extends DataTable
->text(svg('tabler-table-import', ['width' => 16, 'height' => 16])->toHtml())
->titleAttr(__('ui/title.import', ['resource' => 'Excel']))
->addClass('btn-sm')
->action("$('#excel-import-modal').modal('show')"),
->action('javascript:void(0)')
->attr(['data-coreui-toggle' => 'modal', 'data-coreui-target' => '#excel-import-modal']),
Button::make('reload')
->text(svg('tabler-reload', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),

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

@ -15,7 +15,7 @@ class ClientForm extends Form
public string $domain = '';
#[Validate('required', as: 'domains/channel/field.client.auth_type')]
public AuthType $auth_type = AuthType::NONE;
public ?AuthType $auth_type = null;
#[Validate(as: [
'credentials' => 'domains/channel/field.client.credentials',
@ -28,14 +28,14 @@ class ClientForm extends Form
#[Validate('required|max:255', as: 'domains/channel/field.client.webhook')]
public string $webhook = '';
public function updatingAuthType(): void
public function updatedAuthType(): void
{
$this->reset('credentials');
}
public function rules(): array
{
if ($this->auth_type === AuthType::TOKEN) {
if ($this->auth_type === AuthType::BEARER) {
return [
'credentials.token' => 'required',
];

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

@ -5,7 +5,7 @@ namespace App\Livewire\Forms\Finance;
use App\Domains\Finance\DTOs\PaymentProcessing\IssueInvoiceDTO;
use App\Domains\Finance\Enums\InvoiceStatus;
use App\Domains\Finance\Enums\InvoiceType;
use App\UI\Support\Settings\SettingSchema;
use App\UI\Support\Schemas\InputSchemaField;
use Livewire\Attributes\Validate;
use Livewire\Form;
@ -35,9 +35,9 @@ class InvoiceForm extends Form
$this->resetValidation();
foreach ($formSchema as $key => $schema) {
/** @var SettingSchema $schema */
$schema = $schema->schema;
if ($schema->depends == $prop) {
/** @var InputSchemaField $schema */
$schema = $schema->schema->getSchema();
if ($schema->dependsOn == $prop) {
$this->detail[$key] = '';
}
}
@ -48,9 +48,9 @@ class InvoiceForm extends Form
{
$this->reset('detail');
$schema = $this->type?->schema()?->getSchema() ?? [];
foreach ($schema as $key => $value) {
$this->detail[$key] = '';
$schema = $this->type?->getSchema() ?? [];
foreach ($schema as $value) {
$this->detail[$value->key] = '';
}
$this->resetValidation();
@ -58,44 +58,25 @@ class InvoiceForm extends Form
public function rules(): array
{
$rules = [];
$detailSchema = $this->type
?->schema()
->getSchema() ?? [];
foreach ($detailSchema as $key => $schema) {
$rules = array_merge($rules, [
'detail.'.$key => $schema->schema->rules,
]);
}
return $rules;
return $this->type?->schema()?->getValidationRules('detail');
}
public function validationAttributes(): array
{
$attributes = [];
$detailSchema = $this->type
?->schema()
->getSchema() ?? [];
foreach ($detailSchema as $key => $schema) {
$attributes = array_merge($attributes, [
'detail.'.$key => $schema->schema->attributes['label'],
]);
}
return $attributes;
return $this->type?->schema()?->getValidationAttributes('detail');
}
public function toDto(): IssueInvoiceDTO
{
return new IssueInvoiceDTO(
name: $this->name,
clientRefId: $this->client_id,
detail: $this->type->dtoTransform($this->detail),
type: $this->type,
status: InvoiceStatus::PENDING,
clientId: $this->client_id
amount: $this->amount,
coaId: $this->coa_id,
clientId: $this->client_id,
);
}
}

1
app/Livewire/Forms/Finance/PaymentForm.php

@ -2,7 +2,6 @@
namespace App\Livewire\Forms\Finance;
use Livewire\Attributes\Validate;
use Livewire\Form;
class PaymentForm extends Form

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

@ -2,9 +2,12 @@
namespace App\UI\Enums\Contracts;
use App\UI\Support\Settings\BaseSchema;
use App\UI\Support\Schemas\BaseSchema;
use App\UI\Support\Schemas\InputSchemaField;
interface HasSchema
{
public function schema(): BaseSchema;
public function getSchema(): array|InputSchemaField;
}

82
app/UI/Support/Schemas/AbstractGroupSchema.php

@ -0,0 +1,82 @@
<?php
namespace App\UI\Support\Schemas;
abstract class AbstractGroupSchema extends AbstractSchema
{
/**
* @var array<string, InputSchema>
*/
protected array $fields = [];
/**
* Add one or multiple fields to the container.
*/
public function addInput(InputSchema $input): static
{
$inputSchema = $input->getSchema();
$this->fields[$inputSchema->key] = $input;
return $this;
}
/**
* Pass dynamic context down to all child fields.
*/
public function withContext(array $context): static
{
parent::withContext($context);
foreach ($this->fields as $field) {
$field->withContext($this->context);
}
return $this;
}
/**
* Resolve all field schemas, keyed by their input keys.
*
* @return array<string, InputSchemaField>
*/
public function getSchema(): array
{
return array_map(function ($schema) {
return $schema->getSchema();
}, $this->fields);
}
/**
* Extract validation rules for FormRequest or Livewire.
*
* @return array<string, array>
*/
public function getValidationRules(string $prefix = ''): array
{
$rules = [];
foreach ($this->getSchema() as $key => $schema) {
$fieldKey = $prefix ? "{$prefix}.{$key}" : $key;
$rules[$fieldKey] = $schema->rules;
}
return $rules;
}
/**
* Extract validation rules for FormRequest or Livewire.
*
* @return array<string, array>
*/
public function getValidationAttributes(string $prefix = ''): array
{
$rules = [];
foreach ($this->getSchema() as $key => $schema) {
$fieldKey = $prefix ? "{$prefix}.{$key}" : $key;
$rules[$fieldKey] = $schema->label;
}
return $rules;
}
}

25
app/UI/Support/Schemas/AbstractSchema.php

@ -0,0 +1,25 @@
<?php
namespace App\UI\Support\Schemas;
abstract class AbstractSchema implements BaseSchema
{
protected array $context = [];
/**
* Parameterless default instantiation.
*/
public static function make(): static
{
return new static;
}
public function withContext(array $context): static
{
$this->context = array_merge($this->context, $context);
return $this;
}
abstract public function getSchema(): array|InputSchemaField;
}

22
app/UI/Support/Schemas/BaseSchema.php

@ -0,0 +1,22 @@
<?php
namespace App\UI\Support\Schemas;
interface BaseSchema
{
/**
* Parameterless factory method for fluent API instantiation.
*/
public static function make(): static;
/**
* Pass dynamic context (e.g., form state or Livewire state) into the schema.
*/
public function withContext(array $context): static;
/**
* Resolve the schema payload.
* Returns SchemaField for individual inputs, or array<string, SchemaField> for group containers.
*/
public function getSchema(): array|InputSchemaField;
}

160
app/UI/Support/Schemas/InputSchema.php

@ -0,0 +1,160 @@
<?php
namespace App\UI\Support\Schemas;
use App\UI\Enums\InputType;
use Closure;
use Illuminate\Support\Collection;
class InputSchema extends AbstractSchema
{
protected string $key = '';
protected string $label = '';
protected InputType $type = InputType::TEXTLINE;
protected mixed $default = null;
protected array|Closure $options = [];
protected array $attributes = [];
protected array $rules = ['nullable', 'string'];
protected string $depends = '';
protected array $contextKeys = [];
protected bool $live = false;
/**
* Set the field unique key / name.
*/
public function key(string $key): static
{
$this->key = $key;
return $this;
}
/**
* Set the display label.
*/
public function label(string $label): static
{
$this->label = $label;
return $this;
}
public function type(InputType $type): static
{
$this->type = $type;
return $this;
}
public function default(mixed $default): static
{
$this->default = $default;
return $this;
}
public function rules(array|string $rules): static
{
$this->rules = is_array($rules) ? $rules : explode('|', $rules);
return $this;
}
public function options(array|Closure $options): static
{
$this->options = $options;
return $this;
}
public function attributes(array $attributes): static
{
$this->attributes = array_merge($this->attributes, $attributes);
return $this;
}
public function setContextKeys(array|string $keys): static
{
$this->contextKeys = (array) $keys;
return $this;
}
public function select2(array $config = []): static
{
return $this->attributes(['x-select2' => json_encode($config)]);
}
public function live(): static
{
$this->live = true;
return $this;
}
public function dependsOn(string $depends): static
{
$this->depends = $depends;
return $this;
}
public function isLive(): bool
{
return $this->live;
}
public function resolveOptions(array $extraContext = []): array|Collection
{
if (! $this->options instanceof Closure) {
return $this->options;
}
$mergedContext = array_merge($this->context, $extraContext);
if (! empty($this->contextKeys)) {
$mergedContext = array_intersect_key(
$mergedContext,
array_flip($this->contextKeys)
);
}
return ($this->options)($mergedContext);
}
private function getAttributes(): array
{
$attributes = array_merge(['label' => $this->label], $this->attributes);
if ($this->type->isSelect()) {
$attributes['options'] = $this->resolveOptions();
}
return $attributes;
}
public function getSchema(): InputSchemaField
{
return new InputSchemaField(
key: $this->key,
label: $this->label,
type: $this->type,
attributes: $this->getAttributes(),
rules: $this->rules,
default: $this->default,
isLive: $this->isLive(),
dependsOn: $this->depends,
schema: $this,
);
}
}

20
app/UI/Support/Schemas/InputSchemaField.php

@ -0,0 +1,20 @@
<?php
namespace App\UI\Support\Schemas;
use App\UI\Enums\InputType;
readonly class InputSchemaField
{
public function __construct(
public string $key,
public string $label,
public InputType $type,
public array $attributes,
public array $rules,
public mixed $default,
public bool $isLive,
public string $dependsOn,
public InputSchema $schema,
) {}
}

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

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

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

@ -1,122 +0,0 @@
<?php
namespace App\UI\Support\Settings;
use App\UI\Enums\InputType;
use Closure;
use Illuminate\Support\Collection;
class SettingSchema implements BaseSchema
{
public mixed $default = null;
public array|Closure $options = [];
public array $attributes = [];
public array $rules = ['required', 'string'];
public string $depends = '';
protected array $context = [];
protected array $contextKey = [];
protected bool $live = false;
public function __construct(
public InputType $type,
) {}
public static function make(InputType $type): self
{
return new static($type);
}
public function default(mixed $default): static
{
$this->default = $default;
return $this;
}
public function rules(array $rules): static
{
$this->rules = $rules;
return $this;
}
public function options(array|Closure $options): static
{
$this->options = $options;
return $this;
}
public function attributes(array $attributes): static
{
$this->attributes = array_merge($this->attributes, $attributes);
return $this;
}
public function setContextKey(array|string $contextKey): static
{
$this->contextKey = array_merge($this->contextKey, (array) $contextKey);
return $this;
}
public function withContext(array $context): static
{
$this->context = array_merge($this->context, $context);
return $this;
}
public function select2(array $config = []): static
{
$this->attributes(['x-select2' => json_encode($config)]);
return $this;
}
public function live(): static
{
$this->live = true;
return $this;
}
public function dependsOn(string $depends): static
{
$this->depends = $depends;
return $this;
}
public function isLive(): bool
{
return $this->live;
}
public function resolveOptions(array $extraContext = []): array|Collection
{
if ($this->options instanceof Closure) {
$mergedContext = array_merge($this->context, $extraContext);
if(!empty($this->contextKey)) {
$temporaryContext = [];
foreach($this->contextKey as $key) {
$temporaryContext[$key] = !empty($mergedContext[$key]) ? $mergedContext[$key] : null;
}
$mergedContext = $temporaryContext;
}
return ($this->options)($mergedContext);
}
return $this->options;
}
public function getAttributes(array $extraContext = []): array
{
$attributes = $this->attributes;
if ($this->type->isSelect()) {
$attributes['options'] = $this->resolveOptions($extraContext);
}
return $attributes;
}
}

1
database/migrations/2026_05_01_172941_create_system_settings_table.php

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

12
database/seeders/SystemSettingSeeder.php

@ -2,7 +2,6 @@
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,16 +17,7 @@ class SystemSettingSeeder extends Seeder
foreach ($settings as $setting) {
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,
'value' => $setting->schema()->getSchema()->default,
]);
}
}

2
lang/en/domains/channel/enum.php

@ -16,6 +16,6 @@ return [
'auth_type' => [
AuthType::NONE->value => 'None',
AuthType::BASIC->value => 'Basic',
AuthType::TOKEN->value => 'Token',
AuthType::BEARER->value => 'Bearer',
],
];

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

@ -6,7 +6,7 @@ return [
'name' => 'Account Name',
'classification' => 'Classification',
'status' => 'Status',
'parent' => 'Derived From'
'parent' => 'Derived From',
],
'invoice' => [
'name' => 'Invoice Name',
@ -22,14 +22,14 @@ return [
'student_id' => 'Student ID',
'faculty' => 'Faculty',
'study_program' => 'Study Program',
'term' => 'Study Term',
'term_entry' => 'Term Entry',
'fee_type' => 'Fee Type',
'semester' => 'Semester',
// Vendor Bill
'vendor_name' => 'Vendor Name',
'vendor_id' => 'Vendor ID',
]
],
],
'payment' => [
'virtual_account' => 'Virtual Account',

1
lang/en/ui/button.php

@ -7,6 +7,7 @@ return [
'close' => 'Close',
'create' => 'Create',
'update' => 'Update',
'update-mode' => 'Enter Update Mode',
'upload' => 'Upload',
'edit' => 'Edit',
'delete' => 'Delete',

2
lang/id/domains/channel/enum.php

@ -16,6 +16,6 @@ return [
'auth_type' => [
AuthType::NONE->value => 'Tidak Ada',
AuthType::BASIC->value => 'Dasar',
AuthType::TOKEN->value => 'Token',
AuthType::BEARER->value => 'Bearer',
],
];

17
lang/id/domains/finance/field.php

@ -6,7 +6,7 @@ return [
'name' => 'Nama Akun',
'classification' => 'Klasifikasi',
'status' => 'Status',
'parent' => 'Turunan Dari'
'parent' => 'Turunan Dari',
],
'invoice' => [
'name' => 'Nama Pembayar',
@ -14,6 +14,21 @@ return [
'amount' => 'Jumlah Tagihan',
'client_name' => 'Nama Klien',
'status' => 'Status',
'coa_name' => 'Nama Akun (COA)',
'detail' => [
// Detail for Student
'student_name' => 'Nama Mahasiswa',
'student_id' => 'Nomor Induk Mahasiswa',
'faculty' => 'Fakultas',
'study_program' => 'Program Studi',
'term_entry' => 'Periode Masuk',
'fee_type' => 'Jenis Biaya',
'semester' => 'Semester',
// Vendor Bill
'vendor_name' => 'Nama Vendor',
'vendor_id' => 'Nomor Pengenal Vendor',
],
],
'product_mapping' => [
'code' => 'Kode Produk',

6
resources/js/alpine/bs.js

@ -8,7 +8,7 @@ export default function bs(Alpine) {
}
let wrapHandler = (callback, wrapper) => (e) => wrapper(callback, e)
const eventName = `${modifiers}.bs.${value}`
const eventName = `${modifiers}.coreui.${value}`
handler = wrapHandler(handler, (next, e) => {
next(e);
@ -25,13 +25,13 @@ export default function bs(Alpine) {
get(target, type) {
return {
on(event, callback) {
el.addEventListener(`${event}.bs.${type}`, (e) => {
el.addEventListener(`${event}.coreui.${type}`, (e) => {
callback(e);
});
},
instance(element = undefined) {
const className = type.charAt(0).toUpperCase() + type.slice(1);
return bootstrap[className].getInstance(element ?? el);
return coreui[className].getInstance(element ?? el);
},
updateHTML(attr, html) {
el.querySelector(attr).innerHTML = html

22
resources/views/components/datatables/action-button.blade.php

@ -12,9 +12,9 @@
<button
class="btn btn-sm btn-secondary"
data-id="{{ $id }}"
data-bs-toggle="modal"
data-bs-target="#audit-view-modal"
data-bs-toggle="tooltip"
data-coreui-toggle="modal"
data-coreui-target="#audit-view-modal"
data-coreui-toggle="tooltip"
title="{{ __('ui/button.log') }}"
>
@svg('tabler-logs', ['width' => 16, 'height' => 16])
@ -26,9 +26,9 @@
<button
class="btn btn-sm btn-info"
data-id="{{ $id }}"
data-bs-toggle="modal"
data-bs-target="#{{ $view['modal'] }}"
data-bs-toggle="tooltip"
data-coreui-toggle="modal"
data-coreui-target="#{{ $view['modal'] }}"
data-coreui-toggle="tooltip"
title="{{ __('ui/button.view') }}"
>
@svg('tabler-eye', ['width' => 16, 'height' => 16])
@ -37,7 +37,7 @@
<x-link
href="{{ $view['url'] ?? 'javascript:void(0)' }}"
class="btn btn-sm btn-info"
data-bs-toggle="tooltip"
data-coreui-toggle="tooltip"
title="{{ __('ui/button.view') }}"
icon="tabler-eye"
:icon-config="['width' => 16, 'height' => 16]"
@ -52,9 +52,9 @@
<button
class="btn btn-sm btn-warning"
data-id="{{ $id }}"
data-bs-toggle="modal"
data-bs-target="#{{ $edit['modal'] }}"
data-bs-toggle="tooltip"
data-coreui-toggle="modal"
data-coreui-target="#{{ $edit['modal'] }}"
data-coreui-toggle="tooltip"
title="{{ __('ui/button.edit') }}"
>
@svg('tabler-pencil', ['width' => 16, 'height' => 16])
@ -63,7 +63,7 @@
<x-link
href="{{ $edit['url'] ?? 'javascript:void(0)' }}"
class="btn btn-sm btn-warning"
data-bs-toggle="tooltip"
data-coreui-toggle="tooltip"
title="{{ __('ui/button.edit') }}"
icon="tabler-pencil"
:icon-config="['width' => 16, 'height' => 16]"

2
resources/views/components/form/checkbox.blade.php

@ -1,4 +1,4 @@
@props(['label' => '', 'name' => '', 'feedback' => null, 'id'])
@props(['label' => '', 'name' => '', 'feedback' => null, 'id' => null])
@php($name = $attributes->whereStartsWith('wire:model')->first() ?? $name)

2
resources/views/components/form/select.blade.php

@ -4,6 +4,7 @@
'feedback' => null,
'options' => [],
'noLabel' => false,
'disabled' => false
])
@php($name = $attributes->whereStartsWith('wire:model')->first() ?? $name)
@ -23,6 +24,7 @@
'class' => 'form-select'.($errors->has($name) ? ' is-invalid' : ''),
'name' => $name,
'id' => $attributes->has('id') ? $attributes->get('id') : $name,
'disabled' => $disabled,
'x-bind:class' => $feedback ? "{'is-invalid': error != null }" : false,
])
}}>

5
resources/views/components/modal.blade.php

@ -24,12 +24,13 @@
}}
@if ($livewire)
x-init="
$bs.modal.on('show', (e) => {
$bs.modal.on('shown', (e) => {
let id = e?.relatedTarget?.dataset?.id;
if (id) $wire.show(id);
});
$bs.modal.on('hide', () => {
console.log('hide');
$wire.hide();
});
"
@ -53,7 +54,7 @@
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
data-coreui-dismiss="modal"
@if ($loadingState) x-bind:disabled="{{ $loadingState }}" @endif
aria-label="Close"
></button>

20
resources/views/pages/account/profile/⚡user-settings/user-settings.php

@ -17,17 +17,20 @@ new class extends Component
{
$userSettings = auth('web')->user()->settings ?? collect();
$this->settings = collect(UserSettingKey::cases())->map(function ($key) {
$this->settings = collect(UserSettingKey::cases())->map(function (UserSettingKey $key) {
$field = $key->getSchema(); // Returns InputSchemaField
return [
'key' => $key->value,
'label' => $key->label(),
'type' => $key->schema()->type,
'options' => $key->schema()->options,
'key' => $field->key,
'label' => $field->label,
'type' => $field->type,
'options' => $field->attributes['options'] ?? [],
];
})->toArray();
foreach (UserSettingKey::cases() as $key) {
$this->form[$key->value] = $userSettings->get($key->value, $key->schema()->default);
$field = $key->getSchema();
$this->form[$field->key] = $userSettings->get($field->key, $field->default);
}
}
@ -35,8 +38,9 @@ new class extends Component
{
$rules = [];
foreach (UserSettingKey::cases() as $key) {
if ($key->validation()) {
$rules["form.{$key->value}"] = $key->validation();
$field = $key->getSchema();
if (! empty($field->rules)) {
$rules["form.{$field->key}"] = $field->rules;
}
}

3
resources/views/pages/channel/client/⚡api-form-modal/api-form-modal.blade.php

@ -3,6 +3,9 @@
<div class="d-flex flex-column gap-3">
@foreach($this->endpoints as $key => $endpoint)
<x-card :subtitle="$endpoint['label']">
<x-slot:actions>
<x-button type="button" theme="danger" wire:click="runSynchronization('{{ $key }}')" icon="tabler-player-play" />
</x-slot:actions>
<div class="row">
<div class="col-sm-12 col-md-6">
<x-form.select

12
resources/views/pages/channel/client/⚡api-form-modal/api-form-modal.php

@ -1,6 +1,7 @@
<?php
use App\Domains\Channel\Actions\Api\AssignEndpointUrl;
use App\Domains\Channel\Actions\Api\SynchronizeWithApi;
use App\Domains\Channel\DTOs\Api\EndpointUrlDTO;
use App\Domains\Channel\Enums\Api\ApiType;
use App\Domains\Channel\Models\ApiEndpoint;
@ -68,12 +69,12 @@ new class extends Component
$endpoints[$item->value] = [
'label' => $item->label(),
'value' => $value
'value' => $value,
];
$this->field[$item->value] = [
'type' => $item->value,
'value' => $value,
'client_id' => $client_id
'client_id' => $client_id,
];
}
@ -101,6 +102,13 @@ new class extends Component
$assign->execute($endpointToSave);
$this->mode = 'view';
$this->success($this->message());
}
public function runSynchronization(ApiType $type, SynchronizeWithApi $api): void
{
$api->execute($type);
}
};

6
resources/views/pages/channel/client/⚡form-modal/form-modal.php

@ -37,13 +37,13 @@ new class extends Component
domain: $this->form->domain,
webhook_url: $this->form->webhook,
auth_type: $this->form->auth_type,
credentials: $this->form->credentials,
credentials: $this->form->auth_type->transformCredentials($this->form->credentials),
));
} else {
$modify->execute($this->client, new ModifyConsumerAppDetailDTO(
webhook_url: $this->form->webhook,
auth_type: $this->form->auth_type,
credentials: $this->form->credentials,
credentials: $this->form->auth_type->transformCredentials($this->form->credentials),
));
}
@ -65,7 +65,7 @@ new class extends Component
{
$this->id = $id;
$this->mode = 'update';
$this->form->fill($this->client->only(['name', 'webhook', 'domain', 'auth_type']));
$this->form->fill($this->client->only(['name', 'webhook', 'domain', 'auth_type', 'credentials']));
}
public function hide(): void

6
resources/views/pages/finance/invoice/⚡form-modal/form-modal.blade.php

@ -9,10 +9,10 @@
/>
@forelse ($this->invoiceSchema as $key => $value)
@php($model = $value->schema?->isLive() ? 'wire:model.live' : 'wire:model')
<x-dynamic-component :key="$key"
:component="$value->schema?->type->component()"
<x-dynamic-component :key="$value->key"
:component="$value->type->component()"
:attributes="new Illuminate\View\ComponentAttributeBag($value->attributes)->merge([
$model => 'form.detail.'.$key,
$model => 'form.detail.'.$value->key,
])"
/>
@empty

5
resources/views/pages/finance/invoice/⚡form-modal/form-modal.php

@ -6,7 +6,6 @@ use App\Livewire\Concerns\WithToast;
use App\Livewire\Forms\Finance\InvoiceForm;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Component;
new class extends Component
@ -34,14 +33,12 @@ new class extends Component
#[Computed]
public function invoiceSchema(): array
{
return $this->form
->type
?->schema()
->withContext([
'faculty' => $this->form->detail['faculty'] ?? null,
])
->withFormString('form.detail.')
->getSchema() ?? [];
}
@ -58,6 +55,6 @@ new class extends Component
{
$this->form->reset();
$this->reset('id', 'mode');
$this->resetErrorBag();
$this->resetValidation();
}
};

8
resources/views/pages/system/settings/⚡setting-list/setting-list.blade.php

@ -12,17 +12,17 @@
<x-slot:action>
<button
class="btn btn-sm btn-info"
data-bs-target="#update-setting-modal"
data-bs-toggle="modal"
data-coreui-toggle="modal"
data-coreui-target="#update-setting-modal"
data-id="{{ $field->value }}"
>
@svg('tabler-edit', ['width' => 16, 'height' => 16])
</button>
</x-slot:action>
@if ($field->schema()->type->isFile())
@if ($field->schema()->getSchema()->type->isFile())
<img
class="card-img mt-1 border"
src="{{ $this->settingsValue[$field->value] }}"
src="{{ $this->settingsValue[$field->value] ?? '' }}"
alt=""
/>
@else

6
resources/views/pages/system/settings/⚡update-setting-modal/update-setting-modal.blade.php

@ -2,10 +2,10 @@
<x-modal id="update-setting-modal" :title="$this->title" form wire:submit="save" livewire>
<div class="row">
<div class="col-sm-12" wire:key="{{ $settingKey }}">
@if ($this->settingEnum)
@if ($this->inputField)
<x-dynamic-component
:component="$this->settingEnum?->schema()->type->component()"
:attributes="new Illuminate\View\ComponentAttributeBag($this->settingEnum?->inputAttributes())"
:component="$this->inputField?->type->component()"
:attributes="new Illuminate\View\ComponentAttributeBag($this->inputField?->attributes)"
wire:model="settingValue"
/>
@endif

17
resources/views/pages/system/settings/⚡update-setting-modal/update-setting-modal.php

@ -6,6 +6,7 @@ use App\Domains\System\Enums\SystemSettingKey;
use App\Domains\System\Models\SystemSettings;
use App\Livewire\Concerns\WithModal;
use App\Livewire\Concerns\WithToast;
use App\UI\Support\Schemas\InputSchemaField;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
@ -19,7 +20,7 @@ new class extends Component
use WithToast;
#[Locked]
public string $settingKey = '';
public ?SystemSettingKey $settingKey = null;
#[Validate]
public mixed $settingValue = null;
@ -32,16 +33,16 @@ new class extends Component
public function rules(): array
{
return [
'settingValue' => $this->settingEnum->schema()->rules,
'settingValue' => $this->inputField->rules,
];
}
public function show(int|string $id): void
{
$this->settingKey = $id;
$this->settingKey = SystemSettingKey::tryFrom($id);
$setting = SystemSettings::where('key', $id)
->first();
if ($this->settingEnum->schema()->type->isFile()) {
if ($this->inputField->type->isFile()) {
$this->settingValue = $setting->value;
} else {
$this->settingValue = $setting?->translated_value ?? '-';
@ -49,9 +50,9 @@ new class extends Component
}
#[Computed]
public function settingEnum(): ?SystemSettingKey
public function inputField(): ?InputSchemaField
{
return SystemSettingKey::tryFrom($this->settingKey);
return $this->settingKey?->getSchema();
}
public function save(UpdateSettings $action): void
@ -59,11 +60,11 @@ new class extends Component
$this->validate();
$action->execute(new SystemSetingDTO(
key: $this->settingEnum,
key: $this->settingKey,
value: $this->settingValue,
));
$this->success(__('ui/crud.success.updated', ['resource' => $this->settingEnum->label()]));
$this->success(__('ui/crud.success.updated', ['resource' => $this->settingKey->label()]));
$this->dispatch('hide-update-setting-modal');
$this->dispatch('setting-updated');
}

Loading…
Cancel
Save