diff --git a/README.md b/README.md index 821856f..b79ed55 100644 --- a/README.md +++ b/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` | diff --git a/app/Domains/Academic/Actions/Calendar/ModifyTerm.php b/app/Domains/Academic/Actions/Calendar/ModifyTerm.php deleted file mode 100644 index 75b09b1..0000000 --- a/app/Domains/Academic/Actions/Calendar/ModifyTerm.php +++ /dev/null @@ -1,17 +0,0 @@ -update([ - 'date_start' => $dto->startDate, - 'date_end' => $dto->endDate, - ]); - } -} diff --git a/app/Domains/Academic/DTOs/Calendar/ModifyTermDTO.php b/app/Domains/Academic/DTOs/Calendar/ModifyTermDTO.php deleted file mode 100644 index 8c1c1da..0000000 --- a/app/Domains/Academic/DTOs/Calendar/ModifyTermDTO.php +++ /dev/null @@ -1,13 +0,0 @@ - 'date:d/M/Y', + 'date_end' => 'date:d/M/Y', + ]; + protected static function newFactory(): Factory { return TermFactory::new(); diff --git a/app/Domains/Admission/Actions/AdmissionPricing/SetUpAdmissionFeeRates.php b/app/Domains/Admission/Actions/AdmissionPricing/SetUpAdmissionFeeRates.php index 7ad7d92..5ae23b5 100644 --- a/app/Domains/Admission/Actions/AdmissionPricing/SetUpAdmissionFeeRates.php +++ b/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, + ]); } } diff --git a/app/Domains/Admission/Actions/IntakeScheduling/PublishAdmissionSchedule.php b/app/Domains/Admission/Actions/IntakeScheduling/DefineAdmissionSchedule.php similarity index 65% rename from app/Domains/Admission/Actions/IntakeScheduling/PublishAdmissionSchedule.php rename to app/Domains/Admission/Actions/IntakeScheduling/DefineAdmissionSchedule.php index cdcbdbc..f1a5578 100644 --- a/app/Domains/Admission/Actions/IntakeScheduling/PublishAdmissionSchedule.php +++ b/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, ]); diff --git a/app/Domains/Admission/DTOs/AdmissionPricing/AdmissionFeeRatesDTO.php b/app/Domains/Admission/DTOs/AdmissionPricing/AdmissionFeeRatesDTO.php index f97bb72..73c9c5d 100644 --- a/app/Domains/Admission/DTOs/AdmissionPricing/AdmissionFeeRatesDTO.php +++ b/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, ) {} } diff --git a/app/Domains/Admission/DTOs/AdmissionPricing/FeeTypeDTO.php b/app/Domains/Admission/DTOs/AdmissionPricing/FeeTypeDTO.php index f24705b..11ec677 100644 --- a/app/Domains/Admission/DTOs/AdmissionPricing/FeeTypeDTO.php +++ b/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 ) {} } diff --git a/app/Domains/Admission/DTOs/IntakeScheduling/PublishAdmissionScheduleDTO.php b/app/Domains/Admission/DTOs/IntakeScheduling/DefineAdmissionScheduleDTO.php similarity index 74% rename from app/Domains/Admission/DTOs/IntakeScheduling/PublishAdmissionScheduleDTO.php rename to app/Domains/Admission/DTOs/IntakeScheduling/DefineAdmissionScheduleDTO.php index 79e079d..30d6450 100644 --- a/app/Domains/Admission/DTOs/IntakeScheduling/PublishAdmissionScheduleDTO.php +++ b/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 ) {} diff --git a/app/Domains/Admission/Integration/Mappers/AcademicProgramDataMapper.php b/app/Domains/Admission/Integration/Mappers/AcademicProgramDataMapper.php new file mode 100644 index 0000000..0cfce5a --- /dev/null +++ b/app/Domains/Admission/Integration/Mappers/AcademicProgramDataMapper.php @@ -0,0 +1,31 @@ + $rawData['name'], + 'billing_cycle' => $rawData['billing_cycle'], + ]; + } + + /** + * @param FeeType|null $model + */ + public function updateOrCreateDomainState(array $payload, ?Model $model = null): void + { + // + } +} diff --git a/app/Domains/Admission/Models/AcademicProgram.php b/app/Domains/Admission/Models/AcademicProgram.php index 687a9bb..8f47058 100644 --- a/app/Domains/Admission/Models/AcademicProgram.php +++ b/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'; + } } diff --git a/app/Domains/Admission/Models/AdmissionSchedule.php b/app/Domains/Admission/Models/AdmissionSchedule.php index 521d2bb..b0dfd16 100644 --- a/app/Domains/Admission/Models/AdmissionSchedule.php +++ b/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, ]; diff --git a/app/Domains/Admission/Models/AdmissionTrack.php b/app/Domains/Admission/Models/AdmissionTrack.php index 4ae4247..6dbd943 100644 --- a/app/Domains/Admission/Models/AdmissionTrack.php +++ b/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, diff --git a/app/Domains/Identity/Enums/UserStatus.php b/app/Domains/Identity/Enums/UserStatus.php index 368e4b3..863206e 100644 --- a/app/Domains/Identity/Enums/UserStatus.php +++ b/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 { diff --git a/app/Domains/System/Actions/Backup/ArchiveSystemBackup.php b/app/Domains/System/Actions/Backup/ArchiveSystemBackup.php index 2864a56..940e8e8 100644 --- a/app/Domains/System/Actions/Backup/ArchiveSystemBackup.php +++ b/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 { diff --git a/app/Domains/System/Enums/LifecycleStatus.php b/app/Domains/System/Enums/LifecycleStatus.php index 081d43e..cd9bc34 100644 --- a/app/Domains/System/Enums/LifecycleStatus.php +++ b/app/Domains/System/Enums/LifecycleStatus.php @@ -14,7 +14,7 @@ enum LifecycleStatus: string implements HasLabel, HasUiBadge case ACTIVE = 'active'; case ARCHIVED = 'archived'; - + public function variant(): string { return match ($this) { diff --git a/app/Domains/System/Traits/Enum/HasPredicateMethod.php b/app/Domains/System/Traits/Enum/HasPredicateMethod.php index 9a7add4..1092b7d 100644 --- a/app/Domains/System/Traits/Enum/HasPredicateMethod.php +++ b/app/Domains/System/Traits/Enum/HasPredicateMethod.php @@ -6,16 +6,16 @@ trait HasPredicateMethod { public function __call(string $method, array $arguments): bool { - if(str_starts_with($method, 'is')) { + if (str_starts_with($method, 'is')) { $expectedCase = substr($method, 2); $expectedCase = strtoupper($expectedCase); - foreach($this::cases() as $case) { - if($case->name === $expectedCase) { + foreach ($this::cases() as $case) { + if ($case->name === $expectedCase) { return $this === $case; } } - throw new \BadMethodCallException("Method {$method} does not exist on " . self::class); + throw new \BadMethodCallException("Method {$method} does not exist on ".self::class); } return true; diff --git a/app/Domains/System/Traits/Model/HasSlugs.php b/app/Domains/System/Traits/Model/HasSlugs.php index 239c474..5fc4f03 100644 --- a/app/Domains/System/Traits/Model/HasSlugs.php +++ b/app/Domains/System/Traits/Model/HasSlugs.php @@ -13,6 +13,6 @@ trait HasSlugs public static function bootHasSlugs(): void { - static::saving(fn(self $model) => $model->slug = str($model->{$model->sluggable()})->slug()); + static::saving(fn (self $model) => $model->slug = str($model->{$model->sluggable()})->slug()); } } diff --git a/app/Http/DataTables/Academic/FacultyDataTable.php b/app/Http/DataTables/Academic/FacultyDataTable.php index c0b1fc7..d65fb5e 100644 --- a/app/Http/DataTables/Academic/FacultyDataTable.php +++ b/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')) diff --git a/app/Http/DataTables/Academic/StudyProgramDataTable.php b/app/Http/DataTables/Academic/StudyProgramDataTable.php index 9302247..2417af5 100644 --- a/app/Http/DataTables/Academic/StudyProgramDataTable.php +++ b/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')) diff --git a/app/Http/DataTables/Academic/TermDataTable.php b/app/Http/DataTables/Academic/TermDataTable.php index 0f67fe7..157fef7 100644 --- a/app/Http/DataTables/Academic/TermDataTable.php +++ b/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')) diff --git a/app/Http/DataTables/Admission/AdmissionScheduleDataTable.php b/app/Http/DataTables/Admission/AdmissionScheduleDataTable.php index 9ee19c2..85c1b41 100644 --- a/app/Http/DataTables/Admission/AdmissionScheduleDataTable.php +++ b/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'), ]; } diff --git a/app/Http/DataTables/Admission/AdmissionTrackDataTable.php b/app/Http/DataTables/Admission/AdmissionTrackDataTable.php index beca23e..7aa1ffd 100644 --- a/app/Http/DataTables/Admission/AdmissionTrackDataTable.php +++ b/app/Http/DataTables/Admission/AdmissionTrackDataTable.php @@ -20,7 +20,7 @@ class AdmissionTrackDataTable extends DataTable public function dataTable(QueryBuilder $query): EloquentDataTable { return (new EloquentDataTable($query)) - ->editColumn('status', fn(AdmissionTrack $track) => view('components.badge', [ + ->editColumn('status', fn (AdmissionTrack $track) => view('components.badge', [ 'label' => $track->status->label(), 'variant' => $track->status->variant(), ])) @@ -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')) diff --git a/app/Http/DataTables/Admission/FeeTypeDataTable.php b/app/Http/DataTables/Admission/FeeTypeDataTable.php index d4ee61a..89d2665 100644 --- a/app/Http/DataTables/Admission/FeeTypeDataTable.php +++ b/app/Http/DataTables/Admission/FeeTypeDataTable.php @@ -20,7 +20,7 @@ class FeeTypeDataTable extends DataTable public function dataTable(QueryBuilder $query): EloquentDataTable { return (new EloquentDataTable($query)) - ->editColumn('billing_cycle', fn(FeeType $feeType) => $feeType->billing_cycle->label()) + ->editColumn('billing_cycle', fn (FeeType $feeType) => $feeType->billing_cycle->label()) ->addColumn( 'action', fn ($feeType) => view('components.datatables.action-button', [ diff --git a/app/Livewire/Forms/Admission/AdmissionScheduleForm.php b/app/Livewire/Forms/Admission/AdmissionScheduleForm.php index b967457..b6d636e 100644 --- a/app/Livewire/Forms/Admission/AdmissionScheduleForm.php +++ b/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; diff --git a/app/Livewire/Forms/Admission/FeeTypeForm.php b/app/Livewire/Forms/Admission/FeeTypeForm.php index 5376696..2c40a5f 100644 --- a/app/Livewire/Forms/Admission/FeeTypeForm.php +++ b/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; } diff --git a/app/UI/Actions/ApplyLayoutMetadata.php b/app/UI/Actions/ApplyLayoutMetadata.php index 6ea4c64..7586dc9 100644 --- a/app/UI/Actions/ApplyLayoutMetadata.php +++ b/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)) { diff --git a/app/UI/Actions/ResolveDynamicText.php b/app/UI/Actions/ResolveDynamicText.php index cf745ad..982f225 100644 --- a/app/UI/Actions/ResolveDynamicText.php +++ b/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 ''; - } + // Normalize final output value to a string format + $output = is_scalar($resolved) ? (string) $resolved : ''; - /** - * Reflects and extracts object contexts ONCE, caching the output in memory. - */ - private function extractInspectableObjects(object $component): array - { - $objects = []; - $reflection = new ReflectionClass($component); + // Cache the final resolved string output + $this->resolvedPathCache[$contextKey][$cachePathKey] = $output; - // Scan Public Properties - foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $prop) { - $value = $prop->getValue($component); - $objects[$prop->getName()] = $value; - } + return $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')) { - - // 💡 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; - } - } + } catch (Throwable $e) { + // Graceful fallback if anything unexpected misfires during dynamic resolution + return ''; } - - return $objects; } } diff --git a/app/UI/Enums/Concerns/InteractsWithLabels.php b/app/UI/Enums/Concerns/InteractsWithLabels.php index cc9a1c1..e7bcbf9 100644 --- a/app/UI/Enums/Concerns/InteractsWithLabels.php +++ b/app/UI/Enums/Concerns/InteractsWithLabels.php @@ -32,7 +32,7 @@ trait InteractsWithLabels public static function options(): array { - $mappedArray = array_map(fn(self $self) => [ + $mappedArray = array_map(fn (self $self) => [ $self->value => $self->label(), ], self::cases()); diff --git a/composer.json b/composer.json index 812e424..b5445c5 100644 --- a/composer.json +++ b/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": { diff --git a/composer.lock b/composer.lock index e7b34e1..0e6395e 100644 --- a/composer.lock +++ b/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" diff --git a/database/migrations/2026_07_06_031133_create_academic_programs_table.php b/database/migrations/2026_07_06_031133_create_academic_programs_table.php index 7a1dc4e..8e573dd 100644 --- a/database/migrations/2026_07_06_031133_create_academic_programs_table.php +++ b/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(); diff --git a/database/migrations/2026_07_06_031445_create_admission_schedules_table.php b/database/migrations/2026_07_06_031445_create_admission_schedules_table.php index b80bf51..c2d6445 100644 --- a/database/migrations/2026_07_06_031445_create_admission_schedules_table.php +++ b/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(); }); } diff --git a/lang/en/domains/admission/seo.php b/lang/en/domains/admission/seo.php index e9d2ff3..eba5118 100644 --- a/lang/en/domains/admission/seo.php +++ b/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.', diff --git a/lang/id/domains/admission/seo.php b/lang/id/domains/admission/seo.php index de57225..a701a61 100644 --- a/lang/id/domains/admission/seo.php +++ b/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.', diff --git a/resources/scss/plugin/datatables.scss b/resources/scss/plugin/datatables.scss index d2dc5ba..9019747 100644 --- a/resources/scss/plugin/datatables.scss +++ b/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)); - } } } } diff --git a/resources/scss/style.css b/resources/scss/style.css index 3158a5d..bfa710f 100644 --- a/resources/scss/style.css +++ b/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; +} diff --git a/resources/views/components/container/accordion-item.blade.php b/resources/views/components/container/accordion-item.blade.php new file mode 100644 index 0000000..967fb3a --- /dev/null +++ b/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))]) +
+

+ +

+
+
+ {{ $content ?? $slot }} +
+
+
diff --git a/resources/views/components/container/accordion.blade.php b/resources/views/components/container/accordion.blade.php new file mode 100644 index 0000000..f12bed1 --- /dev/null +++ b/resources/views/components/container/accordion.blade.php @@ -0,0 +1,5 @@ +@props(['id' => md5(microtime(true))]) + +
+ {{ $slot }} +
diff --git a/resources/views/pages/academic/study-program/⚡form-modal/form-modal.blade.php b/resources/views/pages/academic/study-program/⚡form-modal/form-modal.blade.php index 5daefe7..c2937d2 100644 --- a/resources/views/pages/academic/study-program/⚡form-modal/form-modal.blade.php +++ b/resources/views/pages/academic/study-program/⚡form-modal/form-modal.blade.php @@ -1,17 +1,17 @@
- + - + - + - +
diff --git a/resources/views/pages/academic/study-program/⚡form-modal/form-modal.php b/resources/views/pages/academic/study-program/⚡form-modal/form-modal.php index 1206199..9034571 100644 --- a/resources/views/pages/academic/study-program/⚡form-modal/form-modal.php +++ b/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; diff --git a/resources/views/pages/admission/academic-program/⚡form-modal/form-modal.php b/resources/views/pages/admission/academic-program/⚡form-modal/form-modal.php index 6d3865e..be3e5f6 100644 --- a/resources/views/pages/admission/academic-program/⚡form-modal/form-modal.php +++ b/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'); diff --git a/resources/views/pages/admission/admission-schedule/⚡detail-view/detail-view.blade.php b/resources/views/pages/admission/admission-schedule/⚡detail-view/detail-view.blade.php new file mode 100644 index 0000000..9bbe123 --- /dev/null +++ b/resources/views/pages/admission/admission-schedule/⚡detail-view/detail-view.blade.php @@ -0,0 +1,130 @@ +
+
+
+ +
+
+ {{ __('domains/admission/field.admission-schedule.track_name') }} +
+
+ {{ $this->admissionSchedule->admissionTrack->name }} +
+ +
+ {{ __('domains/admission/field.admission-schedule.term_name') }} +
+
+ {{ $this->admissionSchedule->term->name }} +
+ +
+ {{ __('domains/admission/field.admission-schedule.date-start') }} +
+
+ {{ $this->admissionSchedule->date_start->format('d/M/Y') }} +
+ +
+ {{ __('domains/admission/field.admission-schedule.date-end') }} +
+
+ {{ $this->admissionSchedule->date_end->format('d/M/Y') }} +
+ +
+ {{ __('domains/admission/field.admission-schedule.re-registration-date-start') }} +
+
+ {{ $this->admissionSchedule->re_registration_date_start->format('d/M/Y') }} +
+ +
+ {{ __('domains/admission/field.admission-schedule.re-registration-date-end') }} +
+
+ {{ $this->admissionSchedule->re_registration_date_end->format('d/M/Y') }} +
+
+
+
+
+ + + + + + + @foreach($this->studyPrograms as $study) + + + + {{ $study->name }} + + +
+ + + + + @foreach($this->feeTypes as $fee) + + @endforeach + + + + @foreach($this->academicPrograms as $program) + + + @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') + + @endforeach + + @endforeach + +
{{ $fee->name }}
{{ $program->name }} + +
+
+
+ @endforeach +
+
+
+
+ + @script + + @endscript +
diff --git a/resources/views/pages/admission/admission-schedule/⚡detail-view/detail-view.php b/resources/views/pages/admission/admission-schedule/⚡detail-view/detail-view.php new file mode 100644 index 0000000..afb74b1 --- /dev/null +++ b/resources/views/pages/admission/admission-schedule/⚡detail-view/detail-view.php @@ -0,0 +1,88 @@ + '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; + } +}; diff --git a/resources/views/pages/admission/admission-schedule/⚡form-modal/form-modal.blade.php b/resources/views/pages/admission/admission-schedule/⚡form-modal/form-modal.blade.php index 8fac024..0ecdf59 100644 --- a/resources/views/pages/admission/admission-schedule/⚡form-modal/form-modal.blade.php +++ b/resources/views/pages/admission/admission-schedule/⚡form-modal/form-modal.blade.php @@ -5,8 +5,6 @@ - - diff --git a/resources/views/pages/admission/admission-schedule/⚡form-modal/form-modal.php b/resources/views/pages/admission/admission-schedule/⚡form-modal/form-modal.php index 13e88e3..1eba4e9 100644 --- a/resources/views/pages/admission/admission-schedule/⚡form-modal/form-modal.php +++ b/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, )); diff --git a/resources/views/pages/admission/admission-track/⚡form-modal/form-modal.php b/resources/views/pages/admission/admission-track/⚡form-modal/form-modal.php index 624a4d0..c31f464 100644 --- a/resources/views/pages/admission/admission-track/⚡form-modal/form-modal.php +++ b/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; diff --git a/resources/views/pages/system/backups/⚡backup-list/backup-list.php b/resources/views/pages/system/backups/⚡backup-list/backup-list.php index c93668f..8d90565 100644 --- a/resources/views/pages/system/backups/⚡backup-list/backup-list.php +++ b/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; diff --git a/routes/web.php b/routes/web.php index 295e44f..c289b0d 100644 --- a/routes/web.php +++ b/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'); diff --git a/tests/Feature/Domains/Identity/Actions/Governance/DeleteUserTest.php b/tests/Feature/Domains/Identity/Actions/Governance/DeleteUserTest.php index 35d6de8..dc6ef7a 100644 --- a/tests/Feature/Domains/Identity/Actions/Governance/DeleteUserTest.php +++ b/tests/Feature/Domains/Identity/Actions/Governance/DeleteUserTest.php @@ -1,7 +1,7 @@