Browse Source

refactor: update models, DTOs, and DataTables to standardize naming and improve relationships; enhance casting and formatting for dates; clean up unused dependencies.

master
Syarif Ubaidillah 1 month ago
parent
commit
d7778c5e62
  1. 2
      README.md
  2. 17
      app/Domains/Academic/Actions/Calendar/ModifyTerm.php
  3. 13
      app/Domains/Academic/DTOs/Calendar/ModifyTermDTO.php
  4. 5
      app/Domains/Academic/Models/Term.php
  5. 14
      app/Domains/Admission/Actions/AdmissionPricing/SetUpAdmissionFeeRates.php
  6. 9
      app/Domains/Admission/Actions/IntakeScheduling/DefineAdmissionSchedule.php
  7. 8
      app/Domains/Admission/DTOs/AdmissionPricing/AdmissionFeeRatesDTO.php
  8. 4
      app/Domains/Admission/DTOs/AdmissionPricing/FeeTypeDTO.php
  9. 4
      app/Domains/Admission/DTOs/IntakeScheduling/DefineAdmissionScheduleDTO.php
  10. 31
      app/Domains/Admission/Integration/Mappers/AcademicProgramDataMapper.php
  11. 31
      app/Domains/Admission/Integration/Mappers/AdmissionScheduleDataMapper.php
  12. 31
      app/Domains/Admission/Integration/Mappers/AdmissionTrackDataMapper.php
  13. 38
      app/Domains/Admission/Integration/Mappers/FeeTypeDataMapper.php
  14. 7
      app/Domains/Admission/Models/AcademicProgram.php
  15. 10
      app/Domains/Admission/Models/AdmissionSchedule.php
  16. 4
      app/Domains/Admission/Models/AdmissionTrack.php
  17. 1
      app/Domains/Identity/Enums/UserStatus.php
  18. 2
      app/Domains/System/Actions/Backup/ArchiveSystemBackup.php
  19. 14
      app/Http/DataTables/Academic/FacultyDataTable.php
  20. 24
      app/Http/DataTables/Academic/StudyProgramDataTable.php
  21. 25
      app/Http/DataTables/Academic/TermDataTable.php
  22. 37
      app/Http/DataTables/Admission/AdmissionScheduleDataTable.php
  23. 2
      app/Http/DataTables/Admission/AdmissionTrackDataTable.php
  24. 3
      app/Livewire/Forms/Admission/AdmissionScheduleForm.php
  25. 2
      app/Livewire/Forms/Admission/FeeTypeForm.php
  26. 20
      app/UI/Actions/ApplyLayoutMetadata.php
  27. 104
      app/UI/Actions/ResolveDynamicText.php
  28. 3
      composer.json
  29. 509
      composer.lock
  30. 1
      database/migrations/2026_07_06_031133_create_academic_programs_table.php
  31. 1
      database/migrations/2026_07_06_031445_create_admission_schedules_table.php
  32. 5
      lang/en/domains/admission/seo.php
  33. 5
      lang/id/domains/admission/seo.php
  34. 8
      resources/scss/plugin/datatables.scss
  35. 4
      resources/scss/style.css
  36. 17
      resources/views/components/container/accordion-item.blade.php
  37. 5
      resources/views/components/container/accordion.blade.php
  38. 8
      resources/views/pages/academic/study-program/⚡form-modal/form-modal.blade.php
  39. 1
      resources/views/pages/academic/study-program/⚡form-modal/form-modal.php
  40. 4
      resources/views/pages/admission/academic-program/⚡form-modal/form-modal.php
  41. 130
      resources/views/pages/admission/admission-schedule/⚡detail-view/detail-view.blade.php
  42. 88
      resources/views/pages/admission/admission-schedule/⚡detail-view/detail-view.php
  43. 2
      resources/views/pages/admission/admission-schedule/⚡form-modal/form-modal.blade.php
  44. 10
      resources/views/pages/admission/admission-schedule/⚡form-modal/form-modal.php
  45. 1
      resources/views/pages/admission/admission-track/⚡form-modal/form-modal.php
  46. 2
      resources/views/pages/system/backups/⚡backup-list/backup-list.php
  47. 42
      routes/web.php
  48. 2
      tests/Feature/Domains/Identity/Actions/Governance/DeleteUserTest.php
  49. 1
      tests/Feature/Domains/System/Actions/Backup/ArchiveSystemBackupTest.php

2
README.md

@ -269,7 +269,7 @@ To keep domain actions aligned with authentic business intent rather than techni
| --- | --- | --- |
| **`Initialize`** / **`Register`** | Declares the setup of an operational business track or engine. | `InitializeAdmissionTrack`, `RegisterStudyProgram` |
| **`Draft`** | Spawns a new entity row but locks it out of live visibility as a pending draft. | `DraftAdmissionSchedule` |
| **`Publish`** / **`Activate`** | Handles state transition to shift an existing record to a live production state. | `PublishAdmissionSchedule` |
| **`Publish`** / **`Activate`** | Handles state transition to shift an existing record to a live production state. | `DefineAdmissionSchedule` |
| **`Define`** | Configures static lookups or structural reference dictionary elements. | `DefineAcademicProgram`, `DefineFeeType` |
| **`Adjust`** / **`Modify`** | Performs precise partial modifications or data tuning on active records. | `AdjustAdmissionScheduleDuration` |
| **`Replace`** / **`Overwrite`** | Performs a full destructive replacement of an entry's total data layout. | `ReplaceTrackSpecification` |

17
app/Domains/Academic/Actions/Calendar/ModifyTerm.php

@ -1,17 +0,0 @@
<?php
namespace App\Domains\Academic\Actions\Calendar;
use App\Domains\Academic\DTOs\Calendar\ModifyTermDTO;
use App\Domains\Academic\Models\Term;
class ModifyTerm
{
public function execute(Term $term, ModifyTermDTO $dto): void
{
$term->update([
'date_start' => $dto->startDate,
'date_end' => $dto->endDate,
]);
}
}

13
app/Domains/Academic/DTOs/Calendar/ModifyTermDTO.php

@ -1,13 +0,0 @@
<?php
namespace App\Domains\Academic\DTOs\Calendar;
use Carbon\CarbonImmutable;
readonly class ModifyTermDTO
{
public function __construct(
public CarbonImmutable $startDate,
public CarbonImmutable $endDate,
) {}
}

5
app/Domains/Academic/Models/Term.php

@ -18,6 +18,11 @@ class Term extends Model
use HasFactory;
use HasUlids;
protected $casts = [
'date_start' => 'date:d/M/Y',
'date_end' => 'date:d/M/Y',
];
protected static function newFactory(): Factory
{
return TermFactory::new();

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

@ -2,10 +2,20 @@
namespace App\Domains\Admission\Actions\AdmissionPricing;
use App\Domains\Admission\DTOs\AdmissionPricing\AdmissionFeeRatesDTO;
use App\Domains\Admission\Models\FeeRate;
class SetUpAdmissionFeeRates
{
public function execute(): void
public function execute(AdmissionFeeRatesDTO $dto): void
{
//
FeeRate::updateOrCreate([
'academic_program_id' => $dto->academicProgramId,
'admission_schedule_id' => $dto->admissionScheduleId,
'study_program_id' => $dto->studyProgramId,
'fee_type_id' => $dto->feeTypeId,
], [
'amount' => $dto->amount,
]);
}
}

9
app/Domains/Admission/Actions/IntakeScheduling/PublishAdmissionSchedule.php → app/Domains/Admission/Actions/IntakeScheduling/DefineAdmissionSchedule.php

@ -2,19 +2,20 @@
namespace App\Domains\Admission\Actions\IntakeScheduling;
use App\Domains\Admission\DTOs\IntakeScheduling\PublishAdmissionScheduleDTO;
use App\Domains\Admission\DTOs\IntakeScheduling\DefineAdmissionScheduleDTO;
use App\Domains\Admission\Enums\AdmissionStatus;
use App\Domains\Admission\Models\AdmissionSchedule;
class PublishAdmissionSchedule
class DefineAdmissionSchedule
{
public function execute(PublishAdmissionScheduleDTO $dto): void
public function execute(DefineAdmissionScheduleDTO $dto): void
{
AdmissionSchedule::create([
'date_start' => $dto->startData,
'date_end' => $dto->endData,
're_registration_date_start' => $dto->reRegisterStartData,
're_registration_date_end' => $dto->reRegisterEndData,
'status' => $dto->status,
'status' => AdmissionStatus::DRAFT,
'term_id' => $dto->termId,
'admission_track_id' => $dto->admissionTrackId,
]);

8
app/Domains/Admission/DTOs/AdmissionPricing/AdmissionFeeRatesDTO.php

@ -5,10 +5,10 @@ namespace App\Domains\Admission\DTOs\AdmissionPricing;
readonly class AdmissionFeeRatesDTO
{
public function __construct(
public int $termId,
public int $academicProgramId,
public int $admissionTrackId,
public int $feeTypeId,
public string $academicProgramId,
public string $admissionScheduleId,
public string $studyProgramId,
public string $feeTypeId,
public float $amount,
) {}
}

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

@ -2,10 +2,12 @@
namespace App\Domains\Admission\DTOs\AdmissionPricing;
use App\Domains\Admission\Enums\BillingCycle;
readonly class FeeTypeDTO
{
public function __construct(
public string $name,
public string $billingCycle
public BillingCycle $billingCycle
) {}
}

4
app/Domains/Admission/DTOs/IntakeScheduling/PublishAdmissionScheduleDTO.php → app/Domains/Admission/DTOs/IntakeScheduling/DefineAdmissionScheduleDTO.php

@ -2,17 +2,15 @@
namespace App\Domains\Admission\DTOs\IntakeScheduling;
use App\Domains\Admission\Enums\AdmissionStatus;
use Carbon\CarbonImmutable;
readonly class PublishAdmissionScheduleDTO
readonly class DefineAdmissionScheduleDTO
{
public function __construct(
public CarbonImmutable $startData,
public CarbonImmutable $endData,
public CarbonImmutable $reRegisterStartData,
public CarbonImmutable $reRegisterEndData,
public AdmissionStatus $status,
public int $termId,
public int $admissionTrackId
) {}

31
app/Domains/Admission/Integration/Mappers/AcademicProgramDataMapper.php

@ -0,0 +1,31 @@
<?php
namespace App\Domains\Admission\Integration\Mappers;
use App\Domains\System\Support\Integration\DataPayloadMapper;
use Illuminate\Database\Eloquent\Model;
class AcademicProgramDataMapper implements DataPayloadMapper
{
public function __construct()
{
// Inject required Domain and Cross-Domain Actions via constructor composition
}
public function getLookupKey(): string
{
// Return the unique string key used to identify existing records (e.g., 'email')
return 'id';
}
public function transform(array $rawData): array
{
// Normalize incoming data array formats into an internal domain-safe layout
return $rawData;
}
public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void
{
// Allocate mapped payloads into strict DTOs and fire internal/external Domain Actions
}
}

31
app/Domains/Admission/Integration/Mappers/AdmissionScheduleDataMapper.php

@ -0,0 +1,31 @@
<?php
namespace App\Domains\Admission\Integration\Mappers;
use App\Domains\System\Support\Integration\DataPayloadMapper;
use Illuminate\Database\Eloquent\Model;
class AdmissionScheduleDataMapper implements DataPayloadMapper
{
public function __construct()
{
// Inject required Domain and Cross-Domain Actions via constructor composition
}
public function getLookupKey(): string
{
// Return the unique string key used to identify existing records (e.g., 'email')
return 'id';
}
public function transform(array $rawData): array
{
// Normalize incoming data array formats into an internal domain-safe layout
return $rawData;
}
public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void
{
// Allocate mapped payloads into strict DTOs and fire internal/external Domain Actions
}
}

31
app/Domains/Admission/Integration/Mappers/AdmissionTrackDataMapper.php

@ -0,0 +1,31 @@
<?php
namespace App\Domains\Admission\Integration\Mappers;
use App\Domains\System\Support\Integration\DataPayloadMapper;
use Illuminate\Database\Eloquent\Model;
class AdmissionTrackDataMapper implements DataPayloadMapper
{
public function __construct()
{
// Inject required Domain and Cross-Domain Actions via constructor composition
}
public function getLookupKey(): string
{
// Return the unique string key used to identify existing records (e.g., 'email')
return 'id';
}
public function transform(array $rawData): array
{
// Normalize incoming data array formats into an internal domain-safe layout
return $rawData;
}
public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void
{
// Allocate mapped payloads into strict DTOs and fire internal/external Domain Actions
}
}

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

@ -0,0 +1,38 @@
<?php
namespace App\Domains\Admission\Integration\Mappers;
use App\Domains\Admission\Models\FeeType;
use App\Domains\System\Support\Integration\DataPayloadMapper;
use Illuminate\Database\Eloquent\Model;
class FeeTypeDataMapper implements DataPayloadMapper
{
public function __construct()
{
// Inject required Domain and Cross-Domain Actions via constructor composition
}
public function getLookupKey(): string
{
// Return the unique string key used to identify existing records (e.g., 'email')
return 'id';
}
public function transform(array $rawData): array
{
// Normalize incoming data array formats into an internal domain-safe layout
return [
'name' => $rawData['name'],
'billing_cycle' => $rawData['billing_cycle'],
];
}
/**
* @param FeeType|null $model
*/
public function updateOrCreateDomainState(array $payload, ?Model $model = null): void
{
//
}
}

7
app/Domains/Admission/Models/AcademicProgram.php

@ -4,6 +4,7 @@ namespace App\Domains\Admission\Models;
use App\Domains\Admission\Policies\AcademicProgramPolicy;
use App\Domains\System\Enums\LifecycleStatus;
use App\Domains\System\Traits\Model\HasSlugs;
use Database\Factories\Academic\AcademicProgramFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
@ -17,6 +18,7 @@ use Illuminate\Database\Eloquent\Model;
class AcademicProgram extends Model
{
use HasFactory;
use HasSlugs;
use HasUlids;
protected $casts = [
@ -32,4 +34,9 @@ class AcademicProgram extends Model
{
return ['ulid'];
}
public function sluggable(): string
{
return 'name';
}
}

10
app/Domains/Admission/Models/AdmissionSchedule.php

@ -2,8 +2,8 @@
namespace App\Domains\Admission\Models;
use App\Domains\Admission\Policies\AdmissionSchedulePolicy;
use App\Domains\Admission\Enums\AdmissionStatus;
use App\Domains\Admission\Policies\AdmissionSchedulePolicy;
use Database\Factories\Admission\AdmissionScheduleFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
@ -29,10 +29,10 @@ class AdmissionSchedule extends Model
use HasUlids;
protected $casts = [
'date_start' => 'date',
'date_end' => 'date',
're_registration_date_start' => 'date',
're_registration_date_end' => 'date',
'date_start' => 'date:d/M/Y',
'date_end' => 'date:d/M/Y',
're_registration_date_start' => 'date:d/M/Y',
're_registration_date_end' => 'date:d/M/Y',
'status' => AdmissionStatus::class,
];

4
app/Domains/Admission/Models/AdmissionTrack.php

@ -13,13 +13,13 @@ use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['name', 'slug', 'status'])]
#[Fillable(['name', 'status'])]
#[UsePolicy(AdmissionTrackPolicy::class)]
class AdmissionTrack extends Model
{
use HasFactory;
use HasUlids;
use HasSlugs;
use HasUlids;
protected $casts = [
'status' => LifecycleStatus::class,

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

@ -5,7 +5,6 @@ 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 Illuminate\Foundation\Testing\Concerns\InteractsWithAuthentication;
enum UserStatus: string implements HasLabel
{

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

@ -2,10 +2,8 @@
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 ArchiveSystemBackup
{

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

@ -22,21 +22,21 @@ class FacultyDataTable extends DataTable
return (new EloquentDataTable($query))
->addColumn(
'action',
fn ($admissionSchedule) => view('components.datatables.action-button', [
fn (Faculty $faculty) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'admissionschedule-form-modal',
'permission' => auth()->user()->can('update', $admissionSchedule),
'modal' => 'faculty-form-modal',
'permission' => auth()->user()->can('update', $faculty),
],
'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')]),
'permission' => auth()->user()->can('delete', $admissionSchedule),
'permission' => auth()->user()->can('delete', $faculty),
],
'table_name' => 'admissionschedule-table',
'id' => $admissionSchedule->ulid,
'table_name' => 'faculty-table',
'id' => $faculty->ulid,
])
)
->rawColumns(['action'])
@ -112,7 +112,7 @@ class FacultyDataTable extends DataTable
Column::make('name')
->title(__('domains/academic/field.faculty.name')),
Column::make('code')
->width(100)
->width(10)
->title(__('domains/academic/field.faculty.code')),
Column::computed('action')
->title(__('ui.label.actions'))

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

@ -22,25 +22,25 @@ class StudyProgramDataTable extends DataTable
return (new EloquentDataTable($query))
->editColumn('status', fn (StudyProgram $studyProgram) => view('components.badge', [
'label' => $studyProgram->status->label(),
'variant' => $studyProgram->status->color(),
'variant' => $studyProgram->status->variant(),
]))
->addColumn(
'action',
fn ($admissionSchedule) => view('components.datatables.action-button', [
fn (StudyProgram $studyProgram) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'studyprogram-form-modal',
'permission' => auth()->user()->can('update', $admissionSchedule),
'permission' => auth()->user()->can('update', $studyProgram),
],
'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')]),
'permission' => auth()->user()->can('delete', $admissionSchedule),
'permission' => auth()->user()->can('delete', $studyProgram),
],
'table_name' => 'studyprogram-table',
'id' => $admissionSchedule->ulid,
'id' => $studyProgram->ulid,
])
)
->rawColumns(['action'])
@ -54,7 +54,9 @@ class StudyProgramDataTable extends DataTable
*/
public function query(StudyProgram $model): QueryBuilder
{
return $model->with('faculty')->newQuery();
return $model->newQuery()
->join('faculties', 'study_programs.faculty_id', '=', 'faculties.id')
->select('study_programs.*', 'faculties.name as faculty_name');
}
/**
@ -115,13 +117,13 @@ class StudyProgramDataTable extends DataTable
->title('#'),
Column::make('name')
->title(__('domains/academic/field.study-program.name')),
Column::make('code')
->width(100)
Column::computed('code')
->width(50)
->title(__('domains/academic/field.study-program.code')),
Column::make('faculty.name')
Column::make('faculty_name')
->title(__('domains/academic/field.faculty.name')),
Column::make('status')
->width(100)
Column::computed('status')
->width(50)
->title(__('domains/academic/field.study-program.status')),
Column::computed('action')
->title(__('ui.label.actions'))

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

@ -22,21 +22,21 @@ class TermDataTable extends DataTable
return (new EloquentDataTable($query))
->addColumn(
'action',
fn ($admissionSchedule) => view('components.datatables.action-button', [
fn (Term $term) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'admissionschedule-form-modal',
'permission' => auth()->user()->can('update', $admissionSchedule),
'modal' => 'term-form-modal',
'permission' => auth()->user()->can('update', $term),
],
'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')]),
'permission' => auth()->user()->can('delete', $admissionSchedule),
'permission' => auth()->user()->can('delete', $term),
],
'table_name' => 'admissionschedule-table',
'id' => $admissionSchedule->ulid,
'table_name' => 'term-table',
'id' => $term->ulid,
])
)
->rawColumns(['action'])
@ -111,15 +111,20 @@ class TermDataTable extends DataTable
->title('#'),
Column::make('name')
->title(__('domains/academic/field.term.name')),
Column::make('code')
Column::computed('code')
->width(150)
->title(__('domains/academic/field.term.code')),
Column::make('year_study')
->width(150)
->title(__('domains/academic/field.term.year_study')),
Column::make('semester')
Column::computed('semester')
->width(150)
->title(__('domains/academic/field.term.semester')),
Column::make('date_start')
Column::computed('date_start')
->width(150)
->title(__('domains/academic/field.term.date_start')),
Column::make('date_end')
Column::computed('date_end')
->width(150)
->title(__('domains/academic/field.term.date_end')),
Column::computed('action')
->title(__('ui.label.actions'))

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

@ -20,10 +20,14 @@ class AdmissionScheduleDataTable extends DataTable
public function dataTable(QueryBuilder $query): EloquentDataTable
{
return (new EloquentDataTable($query))
->addColumn(
->editColumn(
'action',
fn ($admissionSchedule) => view('components.datatables.action-button', [
fn (AdmissionSchedule $admissionSchedule) => view('components.datatables.action-button', [
'log' => true,
'view' => [
'url' => route('admission-schedules.view', ['id' => $admissionSchedule->ulid]),
'permission' => auth()->user()->can('view', $admissionSchedule),
],
'edit' => [
'modal' => 'admissionschedule-form-modal',
'permission' => auth()->user()->can('update', $admissionSchedule),
@ -50,7 +54,10 @@ class AdmissionScheduleDataTable extends DataTable
*/
public function query(AdmissionSchedule $model): QueryBuilder
{
return $model->newQuery();
return $model->newQuery()
->join('admission_tracks', 'admission_tracks.id', '=', 'admission_schedules.admission_track_id')
->join('terms', 'terms.id', '=', 'admission_schedules.term_id')
->select('admission_schedules.*', 'admission_tracks.name as admission_track_name', 'terms.name as term_name');
}
/**
@ -109,26 +116,22 @@ class AdmissionScheduleDataTable extends DataTable
Column::computed('DT_RowIndex')
->width(10)
->title('#'),
Column::make('date_start')
->title(__('domains/admission/field.admission-schedule.date-start')),
Column::make('date_end')
->title(__('domains/admission/field.admission-schedule.date-end')),
Column::make('re_registration_date_start')
->title(__('domains/admission/field.admission-schedule.re-registration-date-start')),
Column::make('re_registration_date_end')
->title(__('domains/admission/field.admission-schedule.re-registration-date-end')),
Column::make('admission_track.name')
Column::make('admission_track_name')
->width(450)
->title(__('domains/admission/field.admission-schedule.track_name')),
Column::make('status')
->width(100)
->title(__('domains/admission/field.admission-schedule.status')),
Column::make('term.name')
Column::make('term_name')
->width(250)
->title(__('domains/admission/field.admission-schedule.term_name')),
Column::computed('date_start')
->width(150)
->title(__('domains/admission/field.admission-schedule.date-start')),
Column::computed('date_end')
->width(150)
->title(__('domains/admission/field.admission-schedule.date-end')),
Column::computed('action')
->title(__('ui.label.actions'))
->exportable(false)
->printable(false)
->width(60)
->addClass('text-center'),
];
}

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

@ -116,7 +116,7 @@ class AdmissionTrackDataTable extends DataTable
Column::make('name')
->title(__('domains/admission/field.admission-track.name')),
Column::computed('status')
->width(100)
->width(50)
->title(__('domains/admission/field.admission-track.status')),
Column::computed('action')
->title(__('ui.label.actions'))

3
app/Livewire/Forms/Admission/AdmissionScheduleForm.php

@ -19,9 +19,6 @@ class AdmissionScheduleForm extends Form
#[Validate('required|date|after_or_equal:re_registration_date_start')]
public string $re_registration_date_end = '';
#[Validate('required|string')]
public string $status = 'draft';
#[Validate('required|integer')]
public ?int $term_id = null;

2
app/Livewire/Forms/Admission/FeeTypeForm.php

@ -12,5 +12,5 @@ class FeeTypeForm extends Form
public string $name = '';
#[Validate('required', as: 'domains/admission/field.fee-type.billing_cycle')]
public BillingCycle $billing_cycle;
public BillingCycle $billing_cycle = BillingCycle::ONCE;
}

20
app/UI/Actions/ApplyLayoutMetadata.php

@ -15,12 +15,20 @@ 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 (Supports translation keys or direct placeholders)
$label = $this->textResolver->execute($labelKey, $dataContext, $contextTarget);
// 1. Resolve the text label using our resolved nested context
$label = $this->textResolver->execute($labelKey, $resolvedContext, $contextTarget);
$url = null;
if (! empty($routeConfig)) {
$routeName = '';
$routeParams = [];
@ -31,8 +39,8 @@ class ApplyLayoutMetadata
$rawParams = $routeConfig[1] ?? [];
foreach ($rawParams as $paramKey => $paramValue) {
// Resolve internal variable bindings within parameters (e.g., "{user.id}" -> 4)
$resolvedValue = $this->textResolver->execute($paramValue, $dataContext, $contextTarget);
// Pass the nested context down to your string resolver
$resolvedValue = $this->textResolver->execute($paramValue, $resolvedContext, $contextTarget);
$routeParams[$paramKey] = $resolvedValue;
}
}
@ -56,10 +64,10 @@ class ApplyLayoutMetadata
];
}
// 3. Resolve layout headline text patterns
// 3. Resolve layout headline text patterns using our nested context
$header = null;
if ($attributeInstance->header) {
$header = $this->textResolver->execute($attributeInstance->header, $dataContext, $contextTarget);
$header = $this->textResolver->execute($attributeInstance->header, $resolvedContext, $contextTarget);
}
if (empty($header) && ! empty($breadcrumbs)) {

104
app/UI/Actions/ResolveDynamicText.php

@ -3,18 +3,15 @@
namespace App\UI\Actions;
use Illuminate\Support\Facades\Lang;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use Throwable;
class ResolveDynamicText
{
/**
* Cache store to prevent duplicate reflection lookups and redundant query executions
* Structure: [object_hash_or_array_id => [context_key => resolved_object_or_scalar]]
* Cache store indexed by object hash and string path to prevent duplicate evaluation
* Structure: [context_hash => [path_string => resolved_scalar_string]]
*/
private array $resolvedContextCache = [];
private array $resolvedPathCache = [];
public function execute(?string $value, array|object $context, ?string $contextTarget = null): ?string
{
@ -37,14 +34,7 @@ class ResolveDynamicText
$bindings = [];
if (! empty($matches[1])) {
foreach ($matches[1] as $variableName) {
$targetProperty = $variableName;
$objectKey = $contextTarget;
$bindings[$variableName] = $this->resolveNestedValue(
$objectKey ?? $variableName,
$objectKey ? $targetProperty : null,
$data
);
$bindings[$variableName] = $this->resolveNestedValue($data, $variableName, $contextTarget);
}
}
@ -59,88 +49,52 @@ class ResolveDynamicText
}
foreach ($matches[1] as $placeholder) {
if (str_contains($placeholder, '.')) {
[$objectName, $property] = explode('.', $placeholder, 2);
} else {
$objectName = $contextTarget ?? $placeholder;
$property = $contextTarget ? $placeholder : null;
}
$replacementValue = $this->resolveNestedValue($objectName, $property, $data);
$replacementValue = $this->resolveNestedValue($data, $placeholder, $contextTarget);
$text = str_replace("{{$placeholder}}", $replacementValue, $text);
}
return $text;
}
private function resolveNestedValue(string $objectName, ?string $property, array|object $data): string
private function resolveNestedValue(array|object $data, string $path, ?string $contextTarget = null): string
{
// 1. Generate a unique cache signature for the current payload state
// 1. Generate a stable cache key representing the current base context state
$contextKey = is_object($data) ? spl_object_hash($data) : md5(serialize($data));
$cachePathKey = "{$contextTarget}.{$path}";
// 2. Warm up the context cache exactly once per component/request
if (! isset($this->resolvedContextCache[$contextKey])) {
$this->resolvedContextCache[$contextKey] = is_object($data)
? $this->extractInspectableObjects($data)
: $data;
// 2. Return early if this exact path combination has already been evaluated on this request
if (isset($this->resolvedPathCache[$contextKey][$cachePathKey])) {
return $this->resolvedPathCache[$contextKey][$cachePathKey];
}
$cachedContext = $this->resolvedContextCache[$contextKey];
$target = null;
try {
$resolved = null;
// 3. Look up the cached target context safely without firing new database calls
if (isset($cachedContext[$objectName])) {
$target = $cachedContext[$objectName];
} elseif ($property === null && isset($cachedContext[$objectName]) && is_scalar($cachedContext[$objectName])) {
return (string) $cachedContext[$objectName];
}
// SCENARIO A: Look for the context wrapper object first (e.g. 'admissionSchedule')
// Using data_get directly on the base container honors custom dynamic getters/methods natively
$targetObject = ! empty($contextTarget) ? data_get($data, $contextTarget) : null;
if ($property === null) {
return $target && is_scalar($target) ? (string) $target : '';
if ($targetObject !== null) {
// Read the relation path directly from the live object instance (triggers magic getters/relations)
$resolved = data_get($targetObject, $path);
}
if ($target && is_object($target) && isset($target->{$property})) {
return (string) $target->{$property};
}
if ($target && is_array($target) && isset($target[$property])) {
return (string) $target[$property];
// SCENARIO B: Fallback to evaluating from the root container context directly
if ($resolved === null || $resolved === '') {
$resolved = data_get($data, $path);
}
return '';
}
/**
* Reflects and extracts object contexts ONCE, caching the output in memory.
*/
private function extractInspectableObjects(object $component): array
{
$objects = [];
$reflection = new ReflectionClass($component);
// Normalize final output value to a string format
$output = is_scalar($resolved) ? (string) $resolved : '';
// Scan Public Properties
foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $prop) {
$value = $prop->getValue($component);
$objects[$prop->getName()] = $value;
}
// Cache the final resolved string output
$this->resolvedPathCache[$contextKey][$cachePathKey] = $output;
// Scan Public Methods (e.g. Livewire 4 #[Computed] properties)
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
if ($method->getNumberOfParameters() === 0 && ! $method->isStatic()) {
try {
$name = $method->getName();
// Ignore internal framework overrides and lifecycle names
if (! str_starts_with($name, '__') && ! str_starts_with($name, 'get') && ! str_contains($name, 'rendering')) {
return $output;
// 💡 THIS WAS THE CULPRIT: Invoking this on every string match triggered query loops.
// Now it runs exactly once per request render pass, and the model is stored in memory.
$objects[$name] = $component->{$name};
}
} catch (Throwable $e) {
continue;
}
}
// Graceful fallback if anything unexpected misfires during dynamic resolution
return '';
}
return $objects;
}
}

3
composer.json

@ -39,7 +39,8 @@
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^4.4",
"pestphp/pest-plugin-laravel": "^4.1"
"pestphp/pest-plugin-laravel": "^4.1",
"tomasvotruba/class-leak": "^2.1"
},
"autoload": {
"psr-4": {

509
composer.lock

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8c3eee9bbc408cbf3152e1d1c9adf948",
"content-hash": "35f66afde94d1745c82bbd632692f3dc",
"packages": [
{
"name": "artesaos/seotools",
@ -1199,6 +1199,56 @@
],
"time": "2025-03-06T22:45:56+00:00"
},
{
"name": "entropy/entropy",
"version": "0.4.6",
"source": {
"type": "git",
"url": "https://github.com/TomasVotruba/entropy.git",
"reference": "415f69258150409db7c36aa747923e28aa53e9f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/TomasVotruba/entropy/zipball/415f69258150409db7c36aa747923e28aa53e9f6",
"reference": "415f69258150409db7c36aa747923e28aa53e9f6",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
"php": "^8.4",
"webmozart/assert": "^2.4"
},
"require-dev": {
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.5",
"rector/jack": "^1.0",
"rector/rector": "^2.4",
"rector/swiss-knife": "^2.4",
"shipmonk/composer-dependency-analyser": "^1.8",
"symplify/easy-coding-standard": "^13.1",
"symplify/phpstan-rules": "^14.10",
"tomasvotruba/type-coverage": "^2.2",
"tomasvotruba/unused-public": "^2.2",
"tracy/tracy": "^2.12"
},
"type": "library",
"autoload": {
"psr-4": {
"Entropy\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"proprietary"
],
"description": "Entropy framework",
"support": {
"issues": "https://github.com/TomasVotruba/entropy/issues",
"source": "https://github.com/TomasVotruba/entropy/tree/0.4.6"
},
"time": "2026-06-20T10:04:38+00:00"
},
{
"name": "fruitcake/php-cors",
"version": "v1.4.0",
@ -6751,89 +6801,6 @@
],
"time": "2026-05-29T05:06:50+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "141046a8f9477948ff284fa65be2095baafb94f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
"reference": "141046a8f9477948ff284fa65be2095baafb94f2",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.38.1",
@ -7003,176 +6970,6 @@
],
"time": "2026-05-25T15:22:23+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.38.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b",
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's Normalizer class and related functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"intl",
"normalizer",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-25T13:48:31+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.38.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
"reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-27T06:59:30+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.37.0",
@ -8510,6 +8307,75 @@
},
"time": "2025-12-02T11:56:42+00:00"
},
{
"name": "tomasvotruba/class-leak",
"version": "2.1.8",
"source": {
"type": "git",
"url": "https://github.com/TomasVotruba/class-leak.git",
"reference": "3ebe4d7bebfb40ab3a85a36431821367b0ac57ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/TomasVotruba/class-leak/zipball/3ebe4d7bebfb40ab3a85a36431821367b0ac57ab",
"reference": "3ebe4d7bebfb40ab3a85a36431821367b0ac57ab",
"shasum": ""
},
"require": {
"entropy/entropy": "^0.4.6",
"nette/utils": "^4.1",
"nikic/php-parser": "^5.7",
"php": ">=8.4",
"symfony/finder": "^7.4|^8.0",
"webmozart/assert": "^2.0"
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-intl-normalizer": "*",
"symfony/polyfill-mbstring": "*"
},
"require-dev": {
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.2",
"phpunit/phpunit": "^13.0",
"rector/jack": "^1.0",
"rector/rector": "^2.4",
"symplify/easy-coding-standard": "^13.0",
"symplify/phpstan-extensions": "^12",
"tomasvotruba/unused-public": "^2.2",
"tracy/tracy": "^2.12"
},
"bin": [
"bin/class-leak",
"bin/class-leak.php"
],
"type": "library",
"autoload": {
"psr-4": {
"TomasVotruba\\ClassLeak\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Detect leaking classes",
"support": {
"issues": "https://github.com/TomasVotruba/class-leak/issues",
"source": "https://github.com/TomasVotruba/class-leak/tree/2.1.8"
},
"funding": [
{
"url": "https://www.paypal.me/rectorphp",
"type": "custom"
},
{
"url": "https://github.com/tomasvotruba",
"type": "github"
}
],
"time": "2026-06-20T16:19:43+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.6.3",
@ -8668,6 +8534,72 @@
],
"time": "2026-04-26T05:33:54+00:00"
},
{
"name": "webmozart/assert",
"version": "2.4.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/9007ea6f45ecf352a9422b36644e4bfc039b9155",
"reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-date": "*",
"ext-filter": "*",
"php": "^8.2"
},
"suggest": {
"ext-intl": "",
"ext-simplexml": "",
"ext-spl": ""
},
"type": "library",
"extra": {
"psalm": {
"pluginClass": "Webmozart\\Assert\\PsalmPlugin"
},
"branch-alias": {
"dev-master": "2.0-dev",
"dev-feature/2-0": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/2.4.0"
},
"time": "2026-05-20T13:07:01+00:00"
},
{
"name": "yajra/laravel-datatables",
"version": "v13.0.0",
@ -13180,72 +13112,6 @@
}
],
"time": "2025-12-08T11:19:18+00:00"
},
{
"name": "webmozart/assert",
"version": "2.4.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/9007ea6f45ecf352a9422b36644e4bfc039b9155",
"reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-date": "*",
"ext-filter": "*",
"php": "^8.2"
},
"suggest": {
"ext-intl": "",
"ext-simplexml": "",
"ext-spl": ""
},
"type": "library",
"extra": {
"psalm": {
"pluginClass": "Webmozart\\Assert\\PsalmPlugin"
},
"branch-alias": {
"dev-master": "2.0-dev",
"dev-feature/2-0": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/2.4.0"
},
"time": "2026-05-20T13:07:01+00:00"
}
],
"aliases": [],
@ -13257,7 +13123,8 @@
"prefer-lowest": false,
"platform": {
"php": "^8.4",
"ext-zip": "*"
"ext-zip": "*",
"ext-intl": "*"
},
"platform-dev": {},
"plugin-api-version": "2.9.0"

1
database/migrations/2026_07_06_031133_create_academic_programs_table.php

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

1
database/migrations/2026_07_06_031445_create_admission_schedules_table.php

@ -22,6 +22,7 @@ return new class extends Migration
->constrained()
->restrictOnDelete();
$table->unsignedBigInteger('term_id')->index();
$table->string('status');
$table->timestamps();
});
}

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

@ -6,6 +6,11 @@ return [
'description' => 'Manage new student admission schedules.',
'keywords' => 'admission schedule, admission, new student',
],
'admission-schedule-detail' => [
'title' => 'Admission Schedule Detail',
'description' => 'View details of the admission schedule.',
'keywords' => 'admission schedule detail, admission, schedule',
],
'admission-track' => [
'title' => 'Admission Track Data',
'description' => 'Manage new student admission tracks.',

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

@ -6,6 +6,11 @@ return [
'description' => 'Kelola jadwal penerimaan mahasiswa baru.',
'keywords' => 'jadwal penerimaan, penerimaan, mahasiswa baru',
],
'admission-schedule-detail' => [
'title' => 'Detail Jadwal Penerimaan',
'description' => 'Lihat detail jadwal penerimaan.',
'keywords' => 'detail jadwal penerimaan, penerimaan, jadwal',
],
'admission-track' => [
'title' => 'Data Jalur Penerimaan',
'description' => 'Kelola jalur penerimaan mahasiswa baru.',

8
resources/scss/plugin/datatables.scss

@ -3,6 +3,9 @@
@import "datatables.net-bs5/css/dataTables.bootstrap5.min.css";
// DataTables Theme Overrides
:root {
--bs-table-bg: var(--cui-body-bg);
}
table.dataTable {
font-family: var(--cui-font-sans-serif);
font-size: var(--cui-body-font-size);
@ -90,11 +93,6 @@ table.dataTable {
&.table-striped {
> tbody > tr:nth-child(odd) > td {
background-color: var(--cui-table-striped-bg);
&.dtfc-fixed-left,
&.dtfc-fixed-right {
background-color: color-mix(in srgb, var(--cui-tertiary-bg) 60%, var(--cui-body-bg));
}
}
}
}

4
resources/scss/style.css

@ -1616,3 +1616,7 @@ html[data-theme="dark"] {
border-top-color: var(--admin-sidebar-border);
color: var(--admin-sidebar-text);
}
.cursor-pointer {
cursor: pointer;
}

17
resources/views/components/container/accordion-item.blade.php

@ -0,0 +1,17 @@
@props(['itemId' => md5(microtime(true)), 'header' => 'Accordion', 'content' => null])
@aware(['id' => md5(microtime(true))])
<div class="accordion-item">
<h2 class="accordion-header">
<button type="button" data-coreui-toggle="collapse" data-coreui-target="#{{ $itemId }}"
@if(!is_string($header)) {{ $header->attributes->merge(['class' => 'accordion-button collapsed']) }}
@else class="accordion-button collapsed" @endif
aria-expanded="false" aria-controls="{{ $itemId }}">
{{ $header }}
</button>
</h2>
<div id="{{ $itemId }}" class="accordion-collapse collapse" data-coreui-parent="#{{ $id }}">
<div class="accordion-body">
{{ $content ?? $slot }}
</div>
</div>
</div>

5
resources/views/components/container/accordion.blade.php

@ -0,0 +1,5 @@
@props(['id' => md5(microtime(true))])
<div class="accordion" id="{{ $id }}">
{{ $slot }}
</div>

8
resources/views/pages/academic/study-program/⚡form-modal/form-modal.blade.php

@ -1,17 +1,17 @@
<x-modal id="studyprogram-form-modal" :title="$this->title" wire:submit="save" wire:loading form livewire>
<div class="d-flex flex-column gap-3">
<x-form.input name="form.name" wire:model="form.name" :label="__('domains/academic/field.study-program.name')" />
<x-form.input name="form.name" wire:model="form.name" :disabled="$mode == 'update'" :label="__('domains/academic/field.study-program.name')" />
<x-form.input name="form.code" wire:model="form.code" :label="__('domains/academic/field.study-program.code')" />
<x-form.input name="form.code" wire:model="form.code" :disabled="$mode == 'update'" :label="__('domains/academic/field.study-program.code')" />
<x-form.input name="form.level" wire:model="form.level" label="Level" />
<x-form.select name="form.status" wire:model="form.status" label="Status" :options="['active' => 'Active', 'archived' => 'Archived']" />
<x-form.select name="form.faculty_id" wire:model="form.faculty_id" :label="__('domains/academic/field.faculty.name')" :options="$this->faculties->pluck('name', 'id')" placeholder="Select Faculty" />
<x-form.select name="form.faculty_id" wire:model="form.faculty_id" :disabled="$mode == 'update'" :label="__('domains/academic/field.faculty.name')" :options="$this->faculties->pluck('name', 'id')" placeholder="Select Faculty" />
<x-form.input type="number" name="form.external_id" wire:model="form.external_id" label="External ID" />
<x-form.input type="number" name="form.external_id" :disabled="$mode == 'update'" wire:model="form.external_id" label="External ID" />
</div>
<x-slot:footer>

1
resources/views/pages/academic/study-program/⚡form-modal/form-modal.php

@ -6,7 +6,6 @@ use App\Domains\Academic\DTOs\Curriculum\AdjustStudyProgramDTO;
use App\Domains\Academic\DTOs\Curriculum\RegisterStudyProgramDTO;
use App\Domains\Academic\Models\Faculty;
use App\Domains\Academic\Models\StudyProgram;
use App\Domains\System\Enums\LifecycleStatus;
use App\Livewire\Concerns\WithModal;
use App\Livewire\Concerns\WithToast;
use App\Livewire\Forms\Academic\StudyProgramForm;

4
resources/views/pages/admission/academic-program/⚡form-modal/form-modal.php

@ -44,8 +44,8 @@ new class extends Component
}
$this->success($this->message);
$this->dispatch('hide-academic-program-form-modal');
$this->js("LaravelDataTables['academic-program-table'].ajax.reload(null, false)");
$this->dispatch('hide-academicprogram-form-modal');
$this->js("LaravelDataTables['academicprogram-table'].ajax.reload(null, false)");
$this->form->reset();
$this->form->resetValidation();
$this->reset('id', 'mode');

130
resources/views/pages/admission/admission-schedule/⚡detail-view/detail-view.blade.php

@ -0,0 +1,130 @@
<div>
<div class="row">
<div class="col-sm-12 col-md-4">
<x-card :title="__('domains/admission/section.admission-schedule-detail.schedule-detail')">
<div class="row gy-3">
<div class="col-12 col-md-6 font-bold">
{{ __('domains/admission/field.admission-schedule.track_name') }}
</div>
<div class="col-12 col-md-6 text-muted">
{{ $this->admissionSchedule->admissionTrack->name }}
</div>
<div class="col-12 col-md-6 font-bold">
{{ __('domains/admission/field.admission-schedule.term_name') }}
</div>
<div class="col-12 col-md-6 text-muted">
{{ $this->admissionSchedule->term->name }}
</div>
<div class="col-12 col-md-6 font-bold">
{{ __('domains/admission/field.admission-schedule.date-start') }}
</div>
<div class="col-12 col-md-6 text-muted">
{{ $this->admissionSchedule->date_start->format('d/M/Y') }}
</div>
<div class="col-12 col-md-6 font-bold">
{{ __('domains/admission/field.admission-schedule.date-end') }}
</div>
<div class="col-12 col-md-6 text-muted">
{{ $this->admissionSchedule->date_end->format('d/M/Y') }}
</div>
<div class="col-12 col-md-6 font-bold">
{{ __('domains/admission/field.admission-schedule.re-registration-date-start') }}
</div>
<div class="col-12 col-md-6 text-muted">
{{ $this->admissionSchedule->re_registration_date_start->format('d/M/Y') }}
</div>
<div class="col-12 col-md-6 font-bold">
{{ __('domains/admission/field.admission-schedule.re-registration-date-end') }}
</div>
<div class="col-12 col-md-6 text-muted">
{{ $this->admissionSchedule->re_registration_date_end->format('d/M/Y') }}
</div>
</div>
</x-card>
</div>
<div class="col-sm-12 col-md-8">
<x-card :title="__('domains/admission/section.admission-schedule-detail.fee-detail')">
<x-slot:actions>
<x-button type="button" theme="primary" size="sm" icon="tabler-plus" icon-only />
</x-slot:actions>
<x-container.accordion id="fee-detail">
@foreach($this->studyPrograms as $study)
<x-container.accordion-item>
<x-slot:header class="d-flex gap-3">
<x-badge variant="danger" label="Not Set" />
<span>{{ $study->name }}</span>
</x-slot:header>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th></th>
@foreach($this->feeTypes as $fee)
<th>{{ $fee->name }}</th>
@endforeach
</tr>
</thead>
<tbody>
@foreach($this->academicPrograms as $program)
<tr>
<td><strong>{{ $program->name }}</strong></td>
@foreach($this->feeTypes as $fee)
@php($feeRate = $this->feeRates
->where('study_program_id', $study->id)
->where('academic_program_id', $program->id)
->where('fee_type_id', $fee->id)->first()?->amount ?? '0')
<td>
<x-badge data-fee-type-id="{{ $fee->ulid }}" data-program-id="{{ $program->ulid }}"
data-study-program-id="{{ $study->ulid }}" data-fee-type="{{ $feeRate }}"
class="cursor-pointer" x-on:click="$js.ask" :label="$feeRate" variant="danger" />
</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>
</div>
</x-container.accordion-item>
@endforeach
</x-container.accordion>
</x-card>
</div>
</div>
@script
<script>
this.$js.ask = function(self) {
var dataset = self.target.dataset
window.Swal.fire({
text: "You won't be able to revert this!",
input: 'text',
showCancelButton: true,
showLoaderOnConfirm: true,
inputValidator: (value) => {
if (!value) {
return 'You need to write something!'
}
},
preConfirm: (value) => {
return new Promise((resolve, reject) => {
$wire.updateFee(
dataset.feeTypeId,
dataset.programId,
dataset.studyProgramId,
value
)
resolve()
})
}
})
}
</script>
@endscript
</div>

88
resources/views/pages/admission/admission-schedule/⚡detail-view/detail-view.php

@ -0,0 +1,88 @@
<?php
use App\Attributes\LayoutData;
use App\Attributes\Seo;
use App\Domains\Academic\Models\StudyProgram;
use App\Domains\Admission\Actions\AdmissionPricing\SetUpAdmissionFeeRates;
use App\Domains\Admission\DTOs\AdmissionPricing\AdmissionFeeRatesDTO;
use App\Domains\Admission\Models\AcademicProgram;
use App\Domains\Admission\Models\AdmissionSchedule;
use App\Domains\Admission\Models\FeeRate;
use App\Domains\Admission\Models\FeeType;
use App\Livewire\Concerns\HasLayoutDataAttributes;
use App\Livewire\Concerns\HasSeoAttributes;
use App\Livewire\Concerns\WithToast;
use Illuminate\Support\Collection;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Component;
new #[Layout('components.layouts.app')]
#[Seo(title: 'domains/admission/seo.admission-schedule-detail.title', description: 'domains/admission/seo.admission-schedule-detail.description', keywords: 'domains/admission/seo.admission-schedule-detail.keywords')]
#[LayoutData(header: 'domains/admission/seo.admission-schedule.title', breadcrumbs: ['ui.menu.dashboard' => 'dashboard', 'domains/admission/seo.admission-schedule.title' => 'admission-schedules.index', '{admissionTrack.name} {term.name}' => ''], context: 'admissionSchedule')]
class extends Component
{
use HasLayoutDataAttributes;
use HasSeoAttributes;
use WithToast;
#[Locked]
public string $id;
public function mount($id): void
{
$this->id = $id;
}
#[Computed]
public function admissionSchedule(): AdmissionSchedule
{
return AdmissionSchedule::with(['admissionTrack', 'term'])
->where('ulid', $this->id)->first();
}
#[Computed]
public function studyPrograms(): Collection
{
return StudyProgram::all(['id', 'name', 'ulid']);
}
#[Computed]
public function academicPrograms(): Collection
{
return AcademicProgram::all(['id', 'name', 'ulid']);
}
#[Computed]
public function feeTypes(): Collection
{
return FeeType::all(['id', 'name', 'ulid']);
}
#[Computed]
public function feeRates(): Collection
{
return FeeRate::where('admission_schedule_id', $this->admissionSchedule->id)
->get();
}
public function updateFee(string $feeTypeId, string $programId, string $studyId, int $value, SetUpAdmissionFeeRates $setUpAdmissionFeeRates): true
{
$feeType = $this->feeTypes->where('ulid', $feeTypeId)->first();
$program = $this->academicPrograms->where('ulid', $programId)->first();
$study = $this->studyPrograms->where('ulid', $studyId)->first();
$setUpAdmissionFeeRates->execute(new AdmissionFeeRatesDTO(
academicProgramId: $program->id,
admissionScheduleId: $this->admissionSchedule->id,
studyProgramId: $study->id,
feeTypeId: $feeType->id,
amount: $value
));
$this->success(__('ui.crud.success.updated', ['resource' => __('ui.fee_type')]));
$this->dispatch('$refresh');
return true;
}
};

2
resources/views/pages/admission/admission-schedule/⚡form-modal/form-modal.blade.php

@ -5,8 +5,6 @@
<x-form.select name="form.admission_track_id" wire:model="form.admission_track_id" :label="__('domains/admission/field.admission-schedule.track_name')" :options="$this->tracks->pluck('name', 'id')" placeholder="Select Track" :disabled="$id !== null" />
<x-form.select name="form.status" wire:model="form.status" :label="__('domains/admission/field.admission-schedule.status')" :options="['draft' => 'Draft', 'published' => 'Published', 'archived' => 'Archived']" :disabled="$id !== null" />
<x-form.input type="date" name="form.date_start" wire:model="form.date_start" :label="__('domains/admission/field.admission-schedule.date-start')" />
<x-form.input type="date" name="form.date_end" wire:model="form.date_end" :label="__('domains/admission/field.admission-schedule.date-end')" />

10
resources/views/pages/admission/admission-schedule/⚡form-modal/form-modal.php

@ -2,10 +2,9 @@
use App\Domains\Academic\Models\Term;
use App\Domains\Admission\Actions\IntakeScheduling\AdjustAdmissionScheduleDuration;
use App\Domains\Admission\Actions\IntakeScheduling\PublishAdmissionSchedule;
use App\Domains\Admission\Actions\IntakeScheduling\DefineAdmissionSchedule;
use App\Domains\Admission\DTOs\IntakeScheduling\AdjustAdmissionScheduleDurationDTO;
use App\Domains\Admission\DTOs\IntakeScheduling\PublishAdmissionScheduleDTO;
use App\Domains\Admission\Enums\AdmissionStatus;
use App\Domains\Admission\DTOs\IntakeScheduling\DefineAdmissionScheduleDTO;
use App\Domains\Admission\Models\AdmissionSchedule;
use App\Domains\Admission\Models\AdmissionTrack;
use App\Livewire\Concerns\WithModal;
@ -44,7 +43,7 @@ new class extends Component
return AdmissionTrack::all(['id', 'name']);
}
public function save(PublishAdmissionSchedule $create, AdjustAdmissionScheduleDuration $update): void
public function save(DefineAdmissionSchedule $create, AdjustAdmissionScheduleDuration $update): void
{
$this->form->validate();
@ -54,12 +53,11 @@ new class extends Component
$reRegisterEndData = CarbonImmutable::parse($this->form->re_registration_date_end);
if ($this->mode === 'create') {
$create->execute(new PublishAdmissionScheduleDTO(
$create->execute(new DefineAdmissionScheduleDTO(
startData: $startData,
endData: $endData,
reRegisterStartData: $reRegisterStartData,
reRegisterEndData: $reRegisterEndData,
status: AdmissionStatus::from($this->form->status),
termId: (int) $this->form->term_id,
admissionTrackId: (int) $this->form->admission_track_id,
));

1
resources/views/pages/admission/admission-track/⚡form-modal/form-modal.php

@ -3,7 +3,6 @@
use App\Domains\Admission\Actions\IntakeSourcing\InitializeAdmissionTrack;
use App\Domains\Admission\DTOs\IntakeSourcing\InitializeAdmissionTrackDTO;
use App\Domains\Admission\Models\AdmissionTrack;
use App\Domains\System\Enums\LifecycleStatus;
use App\Livewire\Concerns\WithModal;
use App\Livewire\Concerns\WithToast;
use App\Livewire\Forms\Admission\AdmissionTrackForm;

2
resources/views/pages/system/backups/⚡backup-list/backup-list.php

@ -2,8 +2,8 @@
use App\Attributes\LayoutData;
use App\Attributes\Seo;
use App\Domains\System\Actions\Backup\DeleteBackup;
use App\Domains\System\Actions\Backup\ArchiveSystemBackup;
use App\Domains\System\Actions\Backup\DeleteBackup;
use App\Domains\System\Actions\Backup\SystemRestore;
use App\Domains\System\Models\Backup;
use App\Livewire\Concerns\HasLayoutDataAttributes;

42
routes/web.php

@ -30,24 +30,46 @@ Route::middleware(['web', 'auth', 'verified', 'seo', 'layouts'])->group(function
breadcrumbs: [
'ui.menu.dashboard' => 'dashboard',
],
))->name('dashboard');
))->can('dashboard.index')
->name('dashboard');
Route::livewire('/user/{user_id}', 'pages::identity.users.detail-view')->name('users.view');
Route::get('/users', UserController::class)->name('users.index');
Route::get('/roles', RoleController::class)->name('roles.index');
Route::get('/faculties', FacultyController::class)->name('faculties.index');
Route::get('/study-programs', StudyProgramController::class)->name('study-programs.index');
Route::get('/faculties', FacultyController::class)
->can('faculty.viewAny')
->name('faculties.index');
Route::get('/study-programs', StudyProgramController::class)
->can('study-program.viewAny')
->name('study-programs.index');
Route::get('/terms', TermController::class)->name('terms.index');
Route::get('/admission-tracks', AdmissionTrackController::class)->name('admission-tracks.index');
Route::get('/academic-programs', AcademicProgramController::class)->name('academic-programs.index');
Route::get('/fee-types', FeeTypeController::class)->name('fee-types.index');
Route::get('/admission-schedules', AdmissionScheduleController::class)->name('admission-schedules.index');
Route::get('/admission-tracks', AdmissionTrackController::class)
->can('admission-track.viewAny')
->name('admission-tracks.index');
Route::get('/academic-programs', AcademicProgramController::class)
->can('academic-program.viewAny')
->name('academic-programs.index');
Route::get('/fee-types', FeeTypeController::class)
->can('fee-type.viewAny')
->name('fee-types.index');
Route::get('/admission-schedules', AdmissionScheduleController::class)
->can('admission-schedule.viewAny')
->name('admission-schedules.index');
Route::livewire('/admission-schedules/{id}', 'pages::admission.admission-schedule.detail-view')
->can('admission-schedule.view')
->name('admission-schedules.view');
Route::get('/invoices', InvoiceController::class)->name('invoices.index');
Route::get('/chart-of-accounts', ChartOfAccountController::class)->name('chart-of-accounts.index');
Route::get('/product-mappings', ProductMappingController::class)->name('product-mappings.index');
Route::get('/invoices', InvoiceController::class)
->can('invoice.viewAny')
->name('invoices.index');
Route::get('/chart-of-accounts', ChartOfAccountController::class)
->can('chart-of-account.viewAny')
->name('chart-of-accounts.index');
Route::get('/product-mappings', ProductMappingController::class)
->can('product-mapping.viewAny')
->name('product-mappings.index');
Route::middleware('password.confirm')->group(function () {
Route::livewire('/system/settings', 'pages::system.settings.setting-list')->name('system-setting.index');

2
tests/Feature/Domains/Identity/Actions/Governance/DeleteUserTest.php

@ -1,7 +1,7 @@
<?php
use App\Domains\Identity\Actions\Governance\PurgeUser;
use App\Domains\Identity\Actions\Governance\DeleteUser;
use App\Domains\Identity\Actions\Governance\PurgeUser;
use App\Domains\Identity\Actions\Governance\SuspendUser;
use App\Domains\Identity\Enums\UserStatus;
use App\Domains\Identity\Models\User;

1
tests/Feature/Domains/System/Actions/Backup/ArchiveSystemBackupTest.php

@ -4,7 +4,6 @@ namespace Tests\Feature\Domains\System\Actions\Backup;
use App\Domains\System\Actions\Backup\ArchiveSystemBackup;
use App\Domains\System\Actions\Backup\SyncBackupCatalog;
use App\Domains\System\Models\Backup;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Config;

Loading…
Cancel
Save