Compare commits
No commits in common. "dev" and "master" have entirely different histories.
@ -1,80 +0,0 @@
|
||||
---
|
||||
name: socialite-development
|
||||
description: "Manages OAuth social authentication with Laravel Socialite. Activate when adding social login providers; configuring OAuth redirect/callback flows; retrieving authenticated user details; customizing scopes or parameters; setting up community providers; testing with Socialite fakes; or when the user mentions social login, OAuth, Socialite, or third-party authentication."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Socialite Authentication
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Socialite patterns and documentation (installation, configuration, routing, callbacks, testing, scopes, stateless auth).
|
||||
|
||||
## Available Providers
|
||||
|
||||
Built-in: `facebook`, `twitter`, `twitter-oauth-2`, `linkedin`, `linkedin-openid`, `google`, `github`, `gitlab`, `bitbucket`, `slack`, `slack-openid`, `twitch`
|
||||
|
||||
Community: 150+ additional providers at [socialiteproviders.com](https://socialiteproviders.com). For provider-specific setup, use `WebFetch` on `https://socialiteproviders.com/{provider-name}`.
|
||||
|
||||
Configuration key in `config/services.php` must match the driver name exactly — note the hyphenated keys: `twitter-oauth-2`, `linkedin-openid`, `slack-openid`.
|
||||
|
||||
Twitter/X: Use `twitter-oauth-2` (OAuth 2.0) for new projects. The legacy `twitter` driver is OAuth 1.0. Driver names remain unchanged despite the platform rebrand.
|
||||
|
||||
Community providers differ from built-in providers in the following ways:
|
||||
- Installed via `composer require socialiteproviders/{name}`
|
||||
- Must register via event listener — NOT auto-discovered like built-in providers
|
||||
- Use `search-docs` for the registration pattern
|
||||
|
||||
## Adding a Provider
|
||||
|
||||
### 1. Configure the provider
|
||||
|
||||
Add the provider's `client_id`, `client_secret`, and `redirect` to `config/services.php`. The config key must match the driver name exactly.
|
||||
|
||||
### 2. Create redirect and callback routes
|
||||
|
||||
Two routes are needed: one that calls `Socialite::driver('provider')->redirect()` to send the user to the OAuth provider, and one that calls `Socialite::driver('provider')->user()` to receive the callback and retrieve user details.
|
||||
|
||||
### 3. Authenticate and store the user
|
||||
|
||||
In the callback, use `updateOrCreate` to find or create a user record from the provider's response (`id`, `name`, `email`, `token`, `refreshToken`), then call `Auth::login()`.
|
||||
|
||||
### 4. Customize the redirect (optional)
|
||||
|
||||
- `scopes()` — merge additional scopes with the provider's defaults
|
||||
- `setScopes()` — replace all scopes entirely
|
||||
- `with()` — pass optional parameters (e.g., `['hd' => 'example.com']` for Google)
|
||||
- `asBotUser()` — Slack only; generates a bot token (`xoxb-`) instead of a user token (`xoxp-`). Must be called before both `redirect()` and `user()`. Only the `token` property will be hydrated on the user object.
|
||||
- `stateless()` — for API/SPA contexts where session state is not maintained
|
||||
|
||||
### 5. Verify
|
||||
|
||||
1. Config key matches driver name exactly (check the list above for hyphenated names)
|
||||
2. `client_id`, `client_secret`, and `redirect` are all present
|
||||
3. Redirect URL matches what is registered in the provider's OAuth dashboard
|
||||
4. Callback route handles denied grants (when user declines authorization)
|
||||
|
||||
Use `search-docs` for complete code examples of each step.
|
||||
|
||||
## Additional Features
|
||||
|
||||
Use `search-docs` for usage details on: `enablePKCE()`, `userFromToken($token)`, `userFromTokenAndSecret($token, $secret)` (OAuth 1.0), retrieving user details.
|
||||
|
||||
User object: `getId()`, `getName()`, `getEmail()`, `getAvatar()`, `getNickname()`, `token`, `refreshToken`, `expiresIn`, `approvedScopes`
|
||||
|
||||
## Testing
|
||||
|
||||
Socialite provides `Socialite::fake()` for testing redirects and callbacks. Use `search-docs` for faking redirects, callback user data, custom token properties, and assertion methods.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Config key must match driver name exactly — hyphenated drivers need hyphenated keys (`linkedin-openid`, `slack-openid`, `twitter-oauth-2`). Mismatch silently fails.
|
||||
- Every provider needs `client_id`, `client_secret`, and `redirect` in `config/services.php`. Missing any one causes cryptic errors.
|
||||
- `scopes()` merges with defaults; `setScopes()` replaces all scopes entirely.
|
||||
- Missing `stateless()` in API/SPA contexts causes `InvalidStateException`.
|
||||
- Redirect URL in `config/services.php` must exactly match the provider's OAuth dashboard (including trailing slashes and protocol).
|
||||
- Do not pass `state`, `response_type`, `client_id`, `redirect_uri`, or `scope` via `with()` — these are reserved.
|
||||
- Community providers require event listener registration via `SocialiteWasCalled`.
|
||||
- `user()` throws when the user declines authorization. Always handle denied grants.
|
||||
@ -8,10 +8,6 @@ APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
GOOGLE_REDIRECT_URI="${APP_URL}/auth/google/callback"
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
|
||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@ -26,8 +26,10 @@ jobs:
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Setup Application
|
||||
run: composer setup
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -19,7 +19,8 @@
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
bun-debug.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/auth.json
|
||||
/.fleet
|
||||
/.idea
|
||||
|
||||
@ -14,7 +14,6 @@ ## Foundational Context
|
||||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v13
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/socialite (SOCIALITE) - v5
|
||||
- laravel/wayfinder (WAYFINDER) - v0
|
||||
- larastan/larastan (LARASTAN) - v3
|
||||
- laravel/boost (BOOST) - v2
|
||||
@ -52,7 +51,7 @@ ## Application Structure & Architecture
|
||||
|
||||
## Frontend Bundling
|
||||
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `bun run build`, `bun run dev`, or `composer run dev`. Ask them.
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
||||
|
||||
## Documentation Files
|
||||
|
||||
@ -175,7 +174,7 @@ ## Testing
|
||||
|
||||
## Vite Error
|
||||
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `bun run build` or ask the user to run `bun run dev` or `composer run dev`.
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
|
||||
|
||||
=== wayfinder/core rules ===
|
||||
|
||||
|
||||
354
BEST_PRACTICE.md
354
BEST_PRACTICE.md
@ -1,354 +0,0 @@
|
||||
# Best Practice Laravel
|
||||
|
||||
Panduan konvensi kode untuk **semua project** dengan tech stack: **Laravel 13 (PHP 8.3)**, **Inertia + React + TypeScript**, **Laravel Wayfinder**, **Spatie Permission**, **Pest**, **Larastan**, **Laravel Pint**.
|
||||
|
||||
Dokumen ini bersifat reusable antar-project — jangan isi dengan nama domain/fitur spesifik satu project saja. Kalau sebuah project punya pengecualian dari aturan di sini, catat pengecualiannya di `CLAUDE.md`/README project tersebut, bukan mengubah dokumen ini.
|
||||
|
||||
Semua kode baru (controller, service, request, model, enum, komponen React) **wajib** mengikuti dokumen ini. Saat mengubah kode lama yang menyimpang, samakan dengan konvensi di sini sekalian (boy scout rule), kecuali perubahan di luar scope task yang sedang dikerjakan.
|
||||
|
||||
---
|
||||
|
||||
## 1. Konsistensi Struktur & Penamaan
|
||||
|
||||
- **Nama method harus konsisten** untuk fungsi yang setara. Jangan sampai controller A punya `store()` tapi controller B untuk hal yang sama pakai `save()`.
|
||||
- **Urutan method harus konsisten** di semua Controller, Service, Model, dan class lain. Urutan baku untuk resource controller:
|
||||
1. `index()`
|
||||
2. `create()` (jika ada, hanya untuk non-Inertia modal-less form)
|
||||
3. `store()`
|
||||
4. `show()` (jika ada)
|
||||
5. `edit()` (jika ada)
|
||||
6. `update()`
|
||||
7. `destroy()`
|
||||
8. Method kustom lain (`updateStatus()`, `resetPassword()`, dll) **selalu ditaruh setelah** ketujuh method di atas, bukan disisipkan di tengah.
|
||||
|
||||
Contoh salah — method kustom nyempil di antara `update()` dan `destroy()`:
|
||||
```php
|
||||
index(), create(), store(), edit(), update(), resetPassword(), destroy()
|
||||
```
|
||||
Contoh benar:
|
||||
```php
|
||||
index(), create(), store(), edit(), update(), destroy(), resetPassword()
|
||||
```
|
||||
- **Organisasi Controller/Request/Service by domain.** Kelompokkan berdasarkan domain bisnis project yang bersangkutan, konsisten di ketiga layer:
|
||||
```
|
||||
app/Http/Controllers/{Domain}/{Name}Controller.php
|
||||
app/Http/Requests/{Domain}/{Name}Request.php
|
||||
app/Services/{Domain}/{Name}Service.php
|
||||
```
|
||||
Domain ditentukan oleh kebutuhan project (mis. `Manage`, `Master`, `Finances`, `Users`), bukan nama fitur individual. Class baru yang berhubungan masuk ke domain yang sudah ada; jangan bikin domain baru untuk satu fitur kecil.
|
||||
- Ikuti konvensi penamaan Laravel community (PSR-12 + konvensi umum):
|
||||
|
||||
| Yang di-nama-i | Gaya | Contoh benar | Contoh salah |
|
||||
|---|---|---|---|
|
||||
| Controller | singular | `ArticleController` | `ArticlesController` |
|
||||
| Route (URI) | plural | `articles/1` | `article/1` |
|
||||
| Route name | snake_case + dot | `users.show_active` | `users.show-active` |
|
||||
| Model | singular | `User` | `Users` |
|
||||
| Relasi hasOne/belongsTo | singular | `articleComment` | `articleComments` |
|
||||
| Relasi lain (hasMany, dll) | plural | `articleComments` | `articleComment` |
|
||||
| Tabel | plural, snake_case | `article_comments` | `articleComments` |
|
||||
| Tabel pivot | singular, alfabetis | `article_user` | `user_article` |
|
||||
| Kolom tabel | snake_case, tanpa nama model | `meta_title` | `article_meta_title` |
|
||||
| Foreign key | singular model + `_id` | `article_id` | `id_article` |
|
||||
| Primary key | - | `id` | `custom_id` |
|
||||
| Migration | deskriptif | `2017_01_01_000000_create_articles_table` | `2017_01_01_000000_articles` |
|
||||
| Method | camelCase | `getAll()` | `get_all()` |
|
||||
| Method resource controller | verba standar | `store()` | `saveArticle()` |
|
||||
| Method test (Pest) | deskriptif, `it(...)`/`test(...)` | `it('rejects guest from viewing article')` | — |
|
||||
| Variable | camelCase | `$articlesWithAuthor` | `$articles_with_author` |
|
||||
| Collection | deskriptif, plural | `$activeUsers` | `$data` |
|
||||
| Object tunggal | deskriptif, singular | `$activeUser` | `$obj` |
|
||||
| File config/lang index | snake_case | `articles_enabled` | `ArticlesEnabled` |
|
||||
| File view Blade | kebab-case | `show-filtered.blade.php` | `showFiltered.blade.php` |
|
||||
| Komponen React (Inertia page/props) | PascalCase file, camelCase prop | `EditForm.tsx`, `isOpen` | — |
|
||||
| Contract (interface) | adjective/noun, tanpa prefix `I` | `AuthenticationInterface` | `IAuthentication` |
|
||||
| Trait | adjective | `Notifiable` | `NotificationTrait` |
|
||||
| Enum | singular | `UserType` | `UserTypeEnum` |
|
||||
| FormRequest | singular + `Request` | `UpdateUserRequest` | `UserFormRequest` |
|
||||
| Seeder | singular | `UserSeeder` | `UsersSeeder` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Enum
|
||||
|
||||
- Semua data opsi yang sudah pasti/tetap valuenya **wajib** pakai native PHP Enum (`enum ... : string`), jangan pakai konstanta class atau string mentah.
|
||||
- Setiap Enum yang punya representasi UI **wajib** menyediakan method `label(): string` berbahasa Indonesia:
|
||||
```php
|
||||
enum OrderStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Completed = 'completed';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Pending => 'Menunggu',
|
||||
self::Completed => 'Selesai',
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
- Kalau project punya banyak Enum, buat trait bantu (mis. `HasValues`) untuk method umum seperti `values()`/`options()` supaya tidak duplikat logika di tiap enum.
|
||||
- Validasi enum di FormRequest pakai `Rule::enum(XxxEnum::class)`, bukan `Rule::in([...])` manual — termasuk kalau ditulis sebagai `Rule::in(XxxEnum::values())`, itu tetap salah karena tidak mendapat pesan error bawaan enum dan gampang lolos review karena polanya mirip valid:
|
||||
```php
|
||||
// Bad
|
||||
'status' => ['required', 'string', Rule::in(OrderStatus::values())],
|
||||
|
||||
// Good
|
||||
'status' => ['required', 'string', Rule::enum(OrderStatus::class)],
|
||||
```
|
||||
`Rule::in([...])` tetap sah dipakai untuk daftar nilai tetap yang **belum** dijadikan Enum (mis. sedang dipertimbangkan atau memang bukan kandidat Enum) — tapi begitu ada Enum untuk field itu, wajib pindah ke `Rule::enum()`.
|
||||
- Kirim enum ke frontend sebagai `value` + `label`, jangan kirim instance PHP mentah ke Inertia props — gunakan `->value` dan `->label()` secara eksplisit atau resource/mapper kecil.
|
||||
|
||||
---
|
||||
|
||||
## 3. Model & Eloquent
|
||||
|
||||
- Gunakan `#[Guarded(['id'])]` (attribute), **bukan** `protected $guarded` atau `#[Fillable]`. Tambahkan kolom lain ke `Guarded` hanya jika memang tidak boleh diisi lewat mass assignment (mis. `last_login_at`).
|
||||
- **Hapus model/class yang sudah tidak dipakai**, jangan dibiarkan menumpuk. Cek dengan grep nama class-nya (bukan cuma teks labelnya) sebelum menyimpulkan tidak terpakai — model kosong tanpa `#[Guarded]` dan tanpa referensi pemakaian adalah tanda kuat dead code.
|
||||
- **Selalu buat relasi dua arah.** Kalau `Order belongsTo Customer`, maka `Customer` juga harus punya relasi baliknya (`hasOne`/`hasMany` sesuai kardinalitas). Jangan biarkan relasi hanya berjalan satu arah.
|
||||
- **Jangan asumsikan foreign key default Eloquent tanpa mengecek migration.** Sebelum menulis `hasMany`/`hasOne`/`belongsTo` tanpa parameter FK eksplisit, pastikan kolom hasil konvensi (`{model}_id`) memang ada di tabel terkait. Kalau kolomnya berbeda (mis. relasi menyeberang lewat model perantara), gunakan FK eksplisit atau `hasManyThrough`, bukan dibiarkan salah diam-diam.
|
||||
- **Urutkan method relasi dalam satu model per kelompok tipe, lalu alfabetis di dalam tiap kelompok**, dengan urutan kelompok: `belongsTo` → `hasOne` → `hasMany` → `belongsToMany` → `hasOneThrough`/`hasManyThrough` → relasi morph (`morphTo`/`morphMany`/`morphToMany`). Method non-relasi (`casts()`, accessor, method bisnis custom) tetap di posisi semula relatif terhadap blok relasi.
|
||||
```php
|
||||
// Good — dikelompokkan per tipe, lalu alfabetis
|
||||
public function department(): BelongsTo { ... } // belongsTo
|
||||
public function user(): BelongsTo { ... }
|
||||
|
||||
public function profile(): HasOne { ... } // hasOne
|
||||
|
||||
public function attendances(): HasMany { ... } // hasMany
|
||||
public function submissions(): HasMany { ... }
|
||||
|
||||
public function departments(): BelongsToMany { ... } // belongsToMany
|
||||
```
|
||||
- Eloquent-first, hindari raw query/`DB::` kecuali untuk kasus performa spesifik yang benar-benar butuh (agregasi berat, bulk update) — dan beri komentar alasannya.
|
||||
- **Cegah N+1**: selalu eager-load relasi yang dipakai di view/Inertia props dengan `with()`/`load()`. Saat memakai partial eager load (`with('term:id,start_date,end_date')`), pastikan kolom yang dipakai untuk cast/accessor ikut disertakan, atau serialisasi akan error.
|
||||
- Mass assignment lewat relasi, bukan set atribut manual satu-satu:
|
||||
```php
|
||||
// Bad
|
||||
$article = new Article;
|
||||
$article->title = $request->title;
|
||||
$article->category_id = $category->id;
|
||||
$article->save();
|
||||
|
||||
// Good
|
||||
$category->articles()->create($request->validated());
|
||||
```
|
||||
- Untuk data besar (export, batch update, notifikasi massal), gunakan `chunk()`/`chunkById()`/`cursor()`, jangan `get()` lalu `foreach` penuh di memori:
|
||||
```php
|
||||
// Bad
|
||||
foreach (User::all() as $user) { ... }
|
||||
|
||||
// Good
|
||||
User::chunkById(500, function ($users) {
|
||||
foreach ($users as $user) { ... }
|
||||
});
|
||||
```
|
||||
- Query builder singkat & ekspresif:
|
||||
|
||||
| Panjang | Singkat |
|
||||
|---|---|
|
||||
| `->where('column', '=', 1)` | `->where('column', 1)` |
|
||||
| `->orderBy('created_at', 'desc')` | `->latest()` |
|
||||
| `->orderBy('created_at', 'asc')` | `->oldest()` |
|
||||
| `->select('id', 'name')->get()` | `->get(['id', 'name'])` |
|
||||
| `->first()->name` | `->value('name')` |
|
||||
|
||||
Pola `->get([...])`/`->paginate($perPage, [...])` ini berlaku juga walau ada `with()`/`when()`/`join()`/dll di antara — Eloquent tidak peduli di posisi mana `select()` dipanggil relatif ke klausa lain, cuma peduli klausa itu ada sebelum eksekusi. **Kecuali** kalau query yang sama pakai `withCount()`/`withSum()`/`withAvg()`/`withMax()`/`withMin()`: method-method itu diam-diam menyuntik `select(table.*)` kalau belum ada `select()` eksplisit sebelumnya, dan Eloquent hanya menerapkan kolom dari `get($columns)`/`paginate($perPage, $columns)` kalau belum ada `select()` — begitu `withCount()` lebih dulu mengisi kolom, argumen kolom di method terminal **diabaikan diam-diam tanpa error** (balik jadi `select(*)`, bocor semua kolom). Jadi kalau ada `withCount()`/`withSum()`/dst di query, `select([...])` eksplisit **wajib** tetap dipertahankan sebelum pemanggilan `withCount()`/dst, jangan dipindah ke method terminal.
|
||||
|
||||
---
|
||||
|
||||
## 4. Controller, Request, dan Service
|
||||
|
||||
- **Controller** hanya mengatur alur request → response (validasi input dipanggil, service dipanggil, redirect/Inertia render dikembalikan). **Tidak boleh** ada query Eloquent kompleks atau business logic langsung di controller.
|
||||
- **Selalu gunakan FormRequest**, sekecil apapun validasinya — jangan validasi inline di controller dengan `$request->validate()`.
|
||||
- Business logic (kalkulasi, orkestrasi antar model, side effect seperti notifikasi/log) **disimpan di Service**, bukan di controller atau model.
|
||||
- **Kalau method Service butuh user yang sedang login untuk scoping/filtering** (mis. dosen cuma lihat kelasnya sendiri, mahasiswa cuma lihat jurusannya sendiri), terima sebagai parameter eksplisit `User $user` (non-nullable, tanpa default) di **posisi pertama** — jangan panggil `auth()->user()` langsung di dalam Service. Controller yang menyuplainya lewat `$request->user()`. Ini soal testability (Service tidak bergantung diam-diam ke global state) dan konsistensi lintas Service.
|
||||
```php
|
||||
// Bad
|
||||
public function paginated(int $perPage = 25): LengthAwarePaginator
|
||||
{
|
||||
$user = auth()->user();
|
||||
// ...
|
||||
}
|
||||
|
||||
// Good
|
||||
public function paginated(User $user, int $perPage = 25): LengthAwarePaginator
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- Otorisasi berbasis permission (Spatie) dicek di dua tempat:
|
||||
- Route-level: middleware `permission:create-xxx` dipasang per-route/per-group.
|
||||
- Object-level (mis. user hanya boleh mengubah record miliknya sendiri): di `authorize()` milik FormRequest, kombinasikan `$this->user()->can('permission-name')` dengan pengecekan kepemilikan record. Untuk route tanpa FormRequest (mis. `destroy()` yang tidak butuh validasi input), pengecekan yang sama dilakukan inline dengan `abort_if()`/`abort_unless()` di Controller.
|
||||
- **Predikat kepemilikan itu sendiri ditaruh sebagai method di Model (`User`, atau model pemilik lain yang relevan), bukan didefinisikan ulang di tiap Controller/FormRequest yang butuh.** Satu aturan bisnis harus punya satu sumber kebenaran — supaya konsisten dan gampang di-test. Penamaan: `is<Peran>Of($target)` untuk peran yang punya nama (mis. `isAdvisorOf`), atau `canManage<Model>($target)` untuk gate umum "boleh mengubah/menghapus record ini".
|
||||
```php
|
||||
// Bad — predikat yang sama diulang di FormRequest DAN Controller
|
||||
// (FormRequest)
|
||||
public function authorize(): bool
|
||||
{
|
||||
$assignment = $this->route('assignment');
|
||||
return $this->user()->can('update-assignments')
|
||||
&& (! $this->user()->hasRole('dosen') || $assignment->courseClass->lecturer_id === $this->user()->lecturer?->id);
|
||||
}
|
||||
// (Controller::destroy(), butuh predikat yang sama karena tidak ada FormRequest)
|
||||
private function abortUnlessLecturerOwnsAssignment(Assignment $assignment): void
|
||||
{
|
||||
$user = request()->user();
|
||||
abort_if($user->hasRole('dosen') && $assignment->courseClass?->lecturer_id !== $user->lecturer?->id, 403);
|
||||
}
|
||||
|
||||
// Good — predikat di Model, dipakai dari FormRequest maupun Controller
|
||||
// (User model)
|
||||
public function canManageAssignment(Assignment $assignment): bool
|
||||
{
|
||||
if ($this->hasRole(UserRole::Dosen->value)) {
|
||||
return $assignment->courseClass->lecturer_id === $this->lecturer?->id;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// (FormRequest)
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('update-assignments')
|
||||
&& $this->user()->canManageAssignment($this->route('assignment'));
|
||||
}
|
||||
// (Controller::destroy())
|
||||
abort_unless(request()->user()->canManageAssignment($assignment), 403);
|
||||
```
|
||||
- Constructor injection untuk dependency (Service, Model), jangan `new Xxx` langsung di dalam method:
|
||||
```php
|
||||
// Bad
|
||||
$user = new User;
|
||||
$user->create($request->validated());
|
||||
|
||||
// Good
|
||||
public function __construct(protected UserService $userService) {}
|
||||
|
||||
$this->userService->create($request->validated());
|
||||
```
|
||||
- **Single Responsibility** — satu method cuma ngerjain satu hal:
|
||||
```php
|
||||
// Bad
|
||||
public function update(Request $request): string
|
||||
{
|
||||
$validated = $request->validate([...]);
|
||||
foreach ($request->events as $event) {
|
||||
$date = $this->carbon->parse($event['date'])->toString();
|
||||
$this->logger->log('Update event ' . $date);
|
||||
}
|
||||
$this->event->updateGeneralEvent($request->validated());
|
||||
return back();
|
||||
}
|
||||
|
||||
// Good
|
||||
public function update(UpdateEventRequest $request): RedirectResponse
|
||||
{
|
||||
$this->logService->logEvents($request->events);
|
||||
$this->eventService->updateGeneralEvent($request->validated());
|
||||
return back();
|
||||
}
|
||||
```
|
||||
- Pecah method yang melakukan banyak hal jadi beberapa method kecil dengan nama deskriptif:
|
||||
```php
|
||||
// Bad
|
||||
public function getFullNameAttribute(): string
|
||||
{
|
||||
if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) {
|
||||
return 'Mr. ' . $this->first_name . ' ' . $this->last_name;
|
||||
}
|
||||
return $this->first_name[0] . '. ' . $this->last_name;
|
||||
}
|
||||
|
||||
// Good
|
||||
public function getFullNameAttribute(): string
|
||||
{
|
||||
return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
|
||||
}
|
||||
|
||||
public function isVerifiedClient(): bool { ... }
|
||||
public function getFullNameLong(): string { ... }
|
||||
public function getFullNameShort(): string { ... }
|
||||
```
|
||||
- Return type selalu dideklarasikan secara eksplisit (`: Response`, `: RedirectResponse`, `: Collection`, dll).
|
||||
- Untuk operasi yang menyentuh >1 tabel sekaligus (mis. buat record + update counter terkait), bungkus dengan `DB::transaction()` di dalam Service, supaya atomik.
|
||||
|
||||
---
|
||||
|
||||
## 5. Kode Ringkas & Idiomatis Laravel
|
||||
|
||||
Gunakan helper singkat yang sudah tersedia, jangan syntax panjang manual:
|
||||
|
||||
| Syntax panjang | Syntax ringkas |
|
||||
|---|---|
|
||||
| `Session::get('cart')` | `session('cart')` |
|
||||
| `$request->session()->get('cart')` | `session('cart')` |
|
||||
| `Session::put('cart', $data)` | `session(['cart' => $data])` |
|
||||
| `$request->input('name')` | `$request->name` / `request('name')` |
|
||||
| `return Redirect::back()` | `return back()` |
|
||||
| `is_null($obj->relation) ? null : $obj->relation->id` | `$obj->relation?->id` |
|
||||
| `return view('index')->with('title', $t)->with('client', $c)` | `return view('index', compact('title', 'client'))` |
|
||||
| `$request->has('value') ? $request->value : 'default'` | `$request->get('value', 'default')` |
|
||||
| `Carbon::now()`, `Carbon::today()` | `now()`, `today()` |
|
||||
| `App::make('Class')` | `app('Class')` |
|
||||
|
||||
- **Jangan panggil `env()` di luar file config.** Simpan dulu ke `config/*.php`, lalu akses lewat `config('nama.key')`. Ini berlaku juga untuk kredensial/flag baru yang ditambahkan.
|
||||
- **DocBlock boleh dipakai kalau memang dibutuhkan dan penting** — bukan default yang dipasang di semua method. Untuk method standar (CRUD biasa, atau apa pun yang sudah jelas maksudnya dari nama method + return/param type PHP), **jangan** tambah DocBlock — itu pemborosan. DocBlock baru layak ditulis untuk kasus yang memang penting, misalnya:
|
||||
1. **Generic type** yang tidak bisa diekspresikan native PHP, mis. `@return Collection<int, Student>`, `@param array<int, string>`.
|
||||
2. **Perilaku non-obvious** yang bisa bikin pembaca salah paham kalau tidak dijelaskan: aturan bisnis tersembunyi, constraint yang tidak kelihatan dari nama method, workaround keterbatasan interface/library.
|
||||
|
||||
```php
|
||||
// Bad — DocBlock generik yang cuma mengulang nama method, tidak nambah informasi
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array { ... }
|
||||
|
||||
// Good — tanpa DocBlock sama sekali, karena nama + return type sudah cukup jelas
|
||||
public function rules(): array { ... }
|
||||
|
||||
// Good — DocBlock dipertahankan karena memang menjelaskan hal yang tidak
|
||||
// kelihatan dari signature method (fallback logic yang bisa mengejutkan)
|
||||
/**
|
||||
* Falls back to the one with the latest start date if none is explicitly active.
|
||||
*/
|
||||
public function getActive(): ?AcademicTerm { ... }
|
||||
```
|
||||
- Komentar kode hanya untuk menjelaskan **kenapa**, bukan **apa** — kalau nama variabel/method sudah jelas, jangan tambah komentar.
|
||||
|
||||
---
|
||||
|
||||
## 6. Frontend (Inertia + React + TypeScript)
|
||||
|
||||
- **Selalu jalankan `php artisan wayfinder:generate --with-form`** setelah menambah/mengubah route, controller, atau FormRequest — flag `--with-form` wajib, tanpa itu halaman auth/settings gagal `tsc`.
|
||||
- Panggil route lewat Wayfinder helper yang di-generate (`import { store } from '@/routes/...'`), **jangan** hardcode string URL di komponen React.
|
||||
- Props yang dikirim dari controller ke halaman Inertia harus sudah dalam bentuk final untuk UI (enum sudah di-`value`/`label`, tanggal sudah diformat/di-ISO-kan) — jangan lempar model Eloquent mentah tanpa transformasi eksplisit.
|
||||
- Komponen React: file `PascalCase.tsx`, satu komponen per file untuk komponen yang di-export dan dipakai di tempat lain; komponen kecil khusus halaman boleh co-located di file yang sama.
|
||||
- Tipe untuk data dari backend didefinisikan eksplisit (interface/type), jangan `any`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing
|
||||
|
||||
- Test ditulis dengan **Pest**, bukan gaya PHPUnit class-based, kecuali menyentuh kode lama yang belum dimigrasikan.
|
||||
- Nama test deskriptif dan menyatakan perilaku, bukan nama method: `it('rejects guest from updating another user's profile')`.
|
||||
- Untuk endpoint yang dilindungi permission, test **minimal** mencakup: pengguna dengan permission (berhasil), pengguna tanpa permission (403), dan — kalau ada object-level authorization — kasus user lain yang mencoba mengakses record bukan miliknya (ditolak).
|
||||
- Gunakan factory (`Model::factory()`), jangan insert manual ke DB di test.
|
||||
- Jalankan `php artisan test` (atau `vendor/bin/pest`) sebelum menganggap task selesai jika ada perubahan pada Service/Controller/Request.
|
||||
|
||||
---
|
||||
|
||||
## 8. Kualitas Kode & Tooling
|
||||
|
||||
- Jalankan **Laravel Pint** (`vendor/bin/pint --parallel`) sebelum commit — style harus konsisten, jangan format manual.
|
||||
- Jalankan **Larastan** (`vendor/bin/phpstan analyse`) untuk perubahan yang menyentuh tipe/return value; perbaiki temuannya, jangan suppress kecuali benar-benar false positive dan beri alasan.
|
||||
- Jangan gunakan `@phpstan-ignore` / `@ts-ignore` sebagai jalan pintas — perbaiki akar masalah tipe-nya.
|
||||
|
||||
---
|
||||
|
||||
## 9. Keamanan
|
||||
|
||||
- Validasi selalu di FormRequest (lihat §4), termasuk validasi kepemilikan record lewat `Rule::exists()` yang di-scope, bukan hanya cek ID ada di tabel mana pun.
|
||||
- Jangan expose data sensitif (password hash, token) lewat Inertia props atau API resource — pastikan `Guarded`/`hidden` pada model sudah benar dan props controller hanya kirim field yang dibutuhkan.
|
||||
- Upload file lewat satu service upload terpusat per project, jangan tulis logic upload baru per fitur — validasi mime/size selalu di FormRequest terkait.
|
||||
- Permission baru yang ditambahkan harus terdaftar di seeder permission (Spatie) dan dipasang di route lewat `permission:` middleware.
|
||||
@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Fortify;
|
||||
|
||||
class AuthenticateUser
|
||||
{
|
||||
/**
|
||||
* Resolve the user attempting to log in, rejecting inactive accounts.
|
||||
*/
|
||||
public function __invoke(Request $request): ?User
|
||||
{
|
||||
$user = User::where(Fortify::username(), $request->input(Fortify::username()))->first();
|
||||
|
||||
if (! $user || ! Hash::check($request->password, $user->password)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $user->is_active) {
|
||||
throw ValidationException::withMessages([
|
||||
Fortify::username() => 'Akun Anda tidak aktif. Silakan hubungi administrator.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,11 @@ class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
use PasswordValidationRules, ProfileValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and create a newly registered user.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
@ -20,7 +25,7 @@ public function create(array $input): User
|
||||
])->validate();
|
||||
|
||||
return User::create([
|
||||
'username' => $input['username'],
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'password' => $input['password'],
|
||||
]);
|
||||
|
||||
@ -2,40 +2,40 @@
|
||||
|
||||
namespace App\Concerns;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
trait ProfileValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate user profiles.
|
||||
*
|
||||
* @return array<string, array<int, ValidationRule|array<mixed>|string>>
|
||||
*/
|
||||
protected function profileRules(?int $userId = null): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->usernameRules($userId),
|
||||
'name' => $this->nameRules(),
|
||||
'email' => $this->emailRules($userId),
|
||||
'full_name' => ['required', 'string', 'max:150'],
|
||||
'phone_number' => ['required', 'string', 'max:20'],
|
||||
'address' => ['required', 'string'],
|
||||
'gender' => ['required', 'string', Rule::in(Gender::values())],
|
||||
'birth_date' => ['required', 'date'],
|
||||
'birth_place' => ['required', 'string', 'max:100'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function usernameRules(?int $userId = null): array
|
||||
/**
|
||||
* Get the validation rules used to validate user names.
|
||||
*
|
||||
* @return array<int, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
protected function nameRules(): array
|
||||
{
|
||||
return [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
'min:3',
|
||||
'alpha_dash',
|
||||
$userId === null
|
||||
? Rule::unique(User::class)
|
||||
: Rule::unique(User::class)->ignore($userId),
|
||||
];
|
||||
return ['required', 'string', 'max:255'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules used to validate user emails.
|
||||
*
|
||||
* @return array<int, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
protected function emailRules(?int $userId = null): array
|
||||
{
|
||||
return [
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum AssignmentStatus: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Open = 'open';
|
||||
case Closed = 'closed';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Open => 'Dibuka',
|
||||
self::Closed => 'Ditutup',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum AttendanceStatus: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Present = 'present';
|
||||
case Absent = 'absent';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Present => 'Hadir',
|
||||
self::Absent => 'Tidak Hadir',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum ClassMethod: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Online = 'online';
|
||||
case Offline = 'offline';
|
||||
case Hybrid = 'hybrid';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Online => 'Daring',
|
||||
self::Offline => 'Luring',
|
||||
self::Hybrid => 'Hybrid',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Concerns;
|
||||
|
||||
trait HasValues
|
||||
{
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{value: string, label: string}>
|
||||
*/
|
||||
public static function options(): array
|
||||
{
|
||||
return array_map(fn (self $case) => [
|
||||
'value' => $case->value,
|
||||
'label' => $case->label(),
|
||||
], self::cases());
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum DayOfWeek: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Monday = 'Monday';
|
||||
case Tuesday = 'Tuesday';
|
||||
case Wednesday = 'Wednesday';
|
||||
case Thursday = 'Thursday';
|
||||
case Friday = 'Friday';
|
||||
case Saturday = 'Saturday';
|
||||
case Sunday = 'Sunday';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Monday => 'Senin',
|
||||
self::Tuesday => 'Selasa',
|
||||
self::Wednesday => 'Rabu',
|
||||
self::Thursday => 'Kamis',
|
||||
self::Friday => 'Jumat',
|
||||
self::Saturday => 'Sabtu',
|
||||
self::Sunday => 'Minggu',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum DegreeLevel: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case D3 = 'D3';
|
||||
case D4 = 'D4';
|
||||
case S1 = 'S1';
|
||||
case S2 = 'S2';
|
||||
case S3 = 'S3';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::D3 => 'D3 (Diploma Tiga)',
|
||||
self::D4 => 'D4 (Diploma Empat)',
|
||||
self::S1 => 'S1 (Sarjana)',
|
||||
self::S2 => 'S2 (Magister)',
|
||||
self::S3 => 'S3 (Doktor)',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum FeedbackStatus: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Submitted = 'submitted';
|
||||
case InReview = 'in_review';
|
||||
case Resolved = 'resolved';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Submitted => 'Terkirim',
|
||||
self::InReview => 'Ditinjau',
|
||||
self::Resolved => 'Selesai',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum FeedbackType: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Kritik = 'kritik';
|
||||
case Saran = 'saran';
|
||||
case Aduan = 'aduan';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Kritik => 'Kritik',
|
||||
self::Saran => 'Saran',
|
||||
self::Aduan => 'Aduan',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum Gender: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Male = 'male';
|
||||
case Female = 'female';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Male => 'Laki-laki',
|
||||
self::Female => 'Perempuan',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum LetterStatus: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Submitted = 'submitted';
|
||||
case InProcess = 'in_process';
|
||||
case Completed = 'completed';
|
||||
case Rejected = 'rejected';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Submitted => 'Diajukan',
|
||||
self::InProcess => 'Diproses',
|
||||
self::Completed => 'Selesai',
|
||||
self::Rejected => 'Ditolak',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum PaymentMethod: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Transfer = 'transfer';
|
||||
case Cash = 'cash';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Transfer => 'Transfer',
|
||||
self::Cash => 'Tunai',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum PaymentStatus: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Unpaid = 'unpaid';
|
||||
case Partial = 'partial';
|
||||
case Paid = 'paid';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Unpaid => 'Belum Bayar',
|
||||
self::Partial => 'Sebagian',
|
||||
self::Paid => 'Lunas',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum RegistrationStatus: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Submitted = 'submitted';
|
||||
case Approved = 'approved';
|
||||
case Rejected = 'rejected';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Submitted => 'Diajukan',
|
||||
self::Approved => 'Disetujui',
|
||||
self::Rejected => 'Ditolak',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum Semester: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case odd = 'odd';
|
||||
case even = 'even';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::odd => 'Ganjil',
|
||||
self::even => 'Genap',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum StudentStatus: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Active = 'active';
|
||||
case OnLeave = 'on_leave';
|
||||
case Graduated = 'graduated';
|
||||
case DroppedOut = 'dropped_out';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Active => 'Aktif',
|
||||
self::OnLeave => 'Cuti',
|
||||
self::Graduated => 'Lulus',
|
||||
self::DroppedOut => 'Drop Out',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum SubmissionStatus: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case NotSubmitted = 'not_submitted';
|
||||
case Submitted = 'submitted';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::NotSubmitted => 'Belum Mengumpulkan',
|
||||
self::Submitted => 'Sudah Mengumpulkan',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Concerns\HasValues;
|
||||
|
||||
enum UserRole: string
|
||||
{
|
||||
use HasValues;
|
||||
|
||||
case Mahasiswa = 'mahasiswa';
|
||||
case Dosen = 'dosen';
|
||||
case Kaprodi = 'kaprodi';
|
||||
case StaffAdmin = 'staff-admin';
|
||||
case StaffKeuangan = 'staff-keuangan';
|
||||
case Developer = 'developer';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Mahasiswa => 'Mahasiswa',
|
||||
self::Dosen => 'Dosen',
|
||||
self::Kaprodi => 'Ketua Program Studi',
|
||||
self::StaffAdmin => 'Staff Admin',
|
||||
self::StaffKeuangan => 'Staff Keuangan',
|
||||
self::Developer => 'Developer',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports\Concerns;
|
||||
|
||||
use Maatwebsite\Excel\Events\AfterSheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
trait StyledHeadingRow
|
||||
{
|
||||
abstract public function title(): string;
|
||||
|
||||
public function styles(Worksheet $sheet): array
|
||||
{
|
||||
return [
|
||||
1 => [
|
||||
'font' => ['bold' => true, 'color' => ['rgb' => '733E0A']],
|
||||
'fill' => [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => 'FDC700'],
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_LEFT,
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function registerEvents(): array
|
||||
{
|
||||
return [
|
||||
AfterSheet::class => function (AfterSheet $event) {
|
||||
$sheet = $event->sheet->getDelegate();
|
||||
$lastColumn = $sheet->getHighestColumn();
|
||||
|
||||
$sheet->insertNewRowBefore(1, 1);
|
||||
$sheet->mergeCells("A1:{$lastColumn}1");
|
||||
$sheet->setCellValue('A1', $this->title().' '.config('app.name'));
|
||||
$sheet->getRowDimension(1)->setRowHeight(24);
|
||||
|
||||
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
|
||||
$sheet->getStyle('A1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
$sheet->getStyle('A1')->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Exports\Concerns\StyledHeadingRow;
|
||||
use App\Models\User;
|
||||
use App\Services\Admin\Users\LecturerService;
|
||||
use Illuminate\Support\Enumerable;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||
|
||||
class LecturersExport implements FromCollection, ShouldAutoSize, WithEvents, WithHeadings, WithMapping, WithStyles
|
||||
{
|
||||
use StyledHeadingRow;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $search = '',
|
||||
private readonly ?string $gender = null,
|
||||
private readonly ?int $departmentId = null,
|
||||
) {}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return 'Data Dosen';
|
||||
}
|
||||
|
||||
public function collection(): Enumerable
|
||||
{
|
||||
return app(LecturerService::class)->forExport($this->search, $this->gender, $this->departmentId);
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return [
|
||||
'NIDN',
|
||||
'Nama Lengkap',
|
||||
'Username',
|
||||
'Email',
|
||||
'Nomor Telepon',
|
||||
'Jurusan',
|
||||
'Jenis Kelamin',
|
||||
'Tempat Lahir',
|
||||
'Tanggal Lahir',
|
||||
'Alamat',
|
||||
];
|
||||
}
|
||||
|
||||
public function map($user): array
|
||||
{
|
||||
/** @var User $user */
|
||||
return [
|
||||
$user->lecturer?->lecturer_number ?? '-',
|
||||
$user->profile?->full_name ?? '-',
|
||||
$user->username,
|
||||
$user->email,
|
||||
$user->profile?->phone_number ?? '-',
|
||||
$user->lecturer?->departments->pluck('name')->join(', ') ?: '-',
|
||||
$user->profile?->gender?->label() ?? '-',
|
||||
$user->profile?->birth_place ?? '-',
|
||||
$user->profile?->birth_date?->format('d M Y') ?? '-',
|
||||
$user->profile?->address ?? '-',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Exports\Concerns\StyledHeadingRow;
|
||||
use App\Models\User;
|
||||
use App\Services\Admin\Users\StudentService;
|
||||
use Illuminate\Support\Enumerable;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||
|
||||
class StudentsExport implements FromCollection, ShouldAutoSize, WithEvents, WithHeadings, WithMapping, WithStyles
|
||||
{
|
||||
use StyledHeadingRow;
|
||||
|
||||
public function __construct(
|
||||
private readonly User $user,
|
||||
private readonly string $search = '',
|
||||
private readonly ?string $gender = null,
|
||||
private readonly ?int $departmentId = null,
|
||||
private readonly ?string $status = null,
|
||||
private readonly ?int $enrollmentYear = null,
|
||||
) {}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return 'Data Mahasiswa';
|
||||
}
|
||||
|
||||
public function collection(): Enumerable
|
||||
{
|
||||
return app(StudentService::class)->forExport($this->user, $this->search, $this->gender, $this->departmentId, $this->status, $this->enrollmentYear);
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return [
|
||||
'NIM',
|
||||
'Nama Lengkap',
|
||||
'Username',
|
||||
'Email',
|
||||
'Nomor Telepon',
|
||||
'Jurusan',
|
||||
'Angkatan',
|
||||
'Status',
|
||||
'Jenis Kelamin',
|
||||
'Tempat Lahir',
|
||||
'Tanggal Lahir',
|
||||
'Alamat',
|
||||
];
|
||||
}
|
||||
|
||||
public function map($user): array
|
||||
{
|
||||
/** @var User $user */
|
||||
return [
|
||||
$user->student?->student_number ?? '-',
|
||||
$user->profile?->full_name ?? '-',
|
||||
$user->username,
|
||||
$user->email,
|
||||
$user->profile?->phone_number ?? '-',
|
||||
$user->student?->department?->name ?? '-',
|
||||
$user->student?->enrollment_year ?? '-',
|
||||
$user->student?->status?->label() ?? '-',
|
||||
$user->profile?->gender?->label() ?? '-',
|
||||
$user->profile?->birth_place ?? '-',
|
||||
$user->profile?->birth_date?->format('d M Y') ?? '-',
|
||||
$user->profile?->address ?? '-',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\AcademicClasses\Assignment;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\AcademicClasses\Assignment\AssignmentRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Assignment;
|
||||
use App\Services\Admin\AcademicClasses\Assignment\AssignmentService;
|
||||
use App\Services\Admin\Manage\CourseClass\CourseClassService;
|
||||
use App\Services\Admin\Master\AcademicTermService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class AssignmentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AssignmentService $service,
|
||||
private readonly CourseClassService $courseClassService,
|
||||
private readonly AcademicTermService $academicTermService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
$academicTermSelected = $request->has('academic_term_id');
|
||||
$academicTermId = $academicTermSelected
|
||||
? ((int) $request->validated('academic_term_id') ?: null)
|
||||
: $this->academicTermService->getActive()?->id;
|
||||
|
||||
return Inertia::render('admin/academic-classes/assignments/index', [
|
||||
'assignments' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
$request->user(),
|
||||
...$request->validatedWithDefaults(),
|
||||
courseClassId: $request->validated('course_class_id'),
|
||||
academicTermId: $academicTermId,
|
||||
)),
|
||||
'courseClasses' => $this->courseClassService->getAllForSelect($request->user()),
|
||||
'filterCourseClasses' => Inertia::always($this->courseClassService->getAllForSelectByAcademicTerm($request->user(), $academicTermId)),
|
||||
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
||||
'filters' => Inertia::always([
|
||||
'course_class_id' => $request->validated('course_class_id'),
|
||||
'academic_term_id' => $academicTermSelected
|
||||
? (string) ($academicTermId ?? 0)
|
||||
: ($academicTermId ? (string) $academicTermId : null),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(AssignmentRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated(), $request->file('attachment'));
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tugas berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.academic-classes.assignments.index');
|
||||
}
|
||||
|
||||
public function update(AssignmentRequest $request, Assignment $assignment): RedirectResponse
|
||||
{
|
||||
$this->service->update($assignment, $request->validated(), $request->file('attachment'));
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tugas berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.academic-classes.assignments.index');
|
||||
}
|
||||
|
||||
public function destroy(Assignment $assignment): RedirectResponse
|
||||
{
|
||||
abort_unless(request()->user()->canManageAssignment($assignment), 403);
|
||||
|
||||
$this->service->delete($assignment);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Tugas berhasil dihapus.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\AcademicClasses\Assignment;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\AcademicClasses\Assignment\SubmitAssignmentRequest;
|
||||
use App\Http\Requests\Admin\AcademicClasses\Assignment\GradeSubmissionRequest;
|
||||
use App\Models\Assignment;
|
||||
use App\Models\Submission;
|
||||
use App\Services\Admin\AcademicClasses\Assignment\SubmissionService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class SubmissionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SubmissionService $service,
|
||||
) {}
|
||||
|
||||
public function index(Assignment $assignment): Response
|
||||
{
|
||||
abort_unless(request()->user()->canManageAssignment($assignment), 403);
|
||||
|
||||
$assignment->load(['courseClass.course', 'courseClass.lecturer.user.profile', 'courseClass.academicTerm']);
|
||||
|
||||
return Inertia::render('admin/academic-classes/assignments/submissions', [
|
||||
'assignment' => $assignment,
|
||||
'submissions' => $this->service->forAssignment($assignment),
|
||||
]);
|
||||
}
|
||||
|
||||
public function grade(GradeSubmissionRequest $request, Assignment $assignment, Submission $submission): RedirectResponse
|
||||
{
|
||||
$this->service->grade($submission, $request->validated('score'));
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Nilai berhasil disimpan.'])->back();
|
||||
}
|
||||
|
||||
public function submit(SubmitAssignmentRequest $request, Assignment $assignment): RedirectResponse
|
||||
{
|
||||
$this->service->submitForStudent($assignment, $request->user()->student, $request->validated(), $request->file('file'));
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tugas berhasil dikumpulkan.']);
|
||||
|
||||
return to_route('admin.academic-classes.assignments.index');
|
||||
}
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\AcademicClasses;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\AcademicClasses\AttendanceStoreRequest;
|
||||
use App\Models\CourseClass;
|
||||
use App\Services\Admin\AcademicClasses\AttendanceService;
|
||||
use App\Services\Admin\Master\AcademicTermService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class AttendanceController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AttendanceService $service,
|
||||
private readonly AcademicTermService $academicTermService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$academicTermId = $request->has('academic_term_id')
|
||||
? $request->integer('academic_term_id')
|
||||
: $this->academicTermService->getActive()?->id;
|
||||
|
||||
return Inertia::render('admin/academic-classes/attendances/index', [
|
||||
'courseClasses' => $this->service->courseClassesForUser($request->user(), $academicTermId),
|
||||
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
||||
'filters' => [
|
||||
'academic_term_id' => $academicTermId ? (string) $academicTermId : null,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(CourseClass $courseClass): Response
|
||||
{
|
||||
$this->abortUnlessLecturerOwnsClass($courseClass);
|
||||
|
||||
$courseClass->load(['course', 'academicTerm']);
|
||||
|
||||
return Inertia::render('admin/academic-classes/attendances/course-class', [
|
||||
'courseClass' => $courseClass,
|
||||
'sessions' => $this->service->sessionsForClass($courseClass),
|
||||
'nextMeetingNumber' => $this->service->nextMeetingNumber($courseClass),
|
||||
]);
|
||||
}
|
||||
|
||||
public function session(CourseClass $courseClass, int $meetingNumber): Response
|
||||
{
|
||||
$this->abortUnlessLecturerOwnsClass($courseClass);
|
||||
|
||||
$courseClass->load('course');
|
||||
$data = $this->service->session($courseClass, $meetingNumber);
|
||||
|
||||
return Inertia::render('admin/academic-classes/attendances/session', [
|
||||
'courseClass' => $courseClass,
|
||||
'meetingNumber' => $meetingNumber,
|
||||
'roster' => $data['roster'],
|
||||
'date' => $data['date'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(AttendanceStoreRequest $request, CourseClass $courseClass, int $meetingNumber): RedirectResponse
|
||||
{
|
||||
$this->service->saveSession($courseClass->id, $meetingNumber, $request->validated('date'), $request->validated('records'));
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kehadiran berhasil disimpan.']);
|
||||
|
||||
return to_route('admin.academic-classes.attendances.session', [$courseClass->id, $meetingNumber]);
|
||||
}
|
||||
|
||||
public function destroy(CourseClass $courseClass, int $meetingNumber): RedirectResponse
|
||||
{
|
||||
$this->abortUnlessLecturerOwnsClass($courseClass);
|
||||
|
||||
$this->service->deleteSession($courseClass->id, $meetingNumber);
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Sesi kehadiran berhasil dihapus.']);
|
||||
|
||||
return to_route('admin.academic-classes.attendances.show', [$courseClass->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A dosen may only take or manage attendance for classes they lecture.
|
||||
*/
|
||||
private function abortUnlessLecturerOwnsClass(CourseClass $courseClass): void
|
||||
{
|
||||
$user = request()->user();
|
||||
|
||||
abort_if(
|
||||
$user->hasRole('dosen') && $courseClass->lecturer_id !== $user->lecturer?->id,
|
||||
403,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\AcademicClasses;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\AcademicClasses\MaterialRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Material;
|
||||
use App\Services\Admin\AcademicClasses\MaterialService;
|
||||
use App\Services\Admin\Manage\CourseClass\CourseClassService;
|
||||
use App\Services\Admin\Master\AcademicTermService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class MaterialController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MaterialService $service,
|
||||
private readonly CourseClassService $courseClassService,
|
||||
private readonly AcademicTermService $academicTermService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
$academicTermSelected = $request->has('academic_term_id');
|
||||
$academicTermId = $academicTermSelected
|
||||
? ((int) $request->validated('academic_term_id') ?: null)
|
||||
: $this->academicTermService->getActive()?->id;
|
||||
|
||||
return Inertia::render('admin/academic-classes/materials/index', [
|
||||
'materials' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
$request->user(),
|
||||
...$request->validatedWithDefaults(),
|
||||
courseClassId: $request->validated('course_class_id'),
|
||||
academicTermId: $academicTermId,
|
||||
)),
|
||||
'courseClasses' => $this->courseClassService->getAllForSelect($request->user()),
|
||||
'filterCourseClasses' => Inertia::always($this->courseClassService->getAllForSelectByAcademicTerm($request->user(), $academicTermId)),
|
||||
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
||||
'filters' => Inertia::always([
|
||||
'course_class_id' => $request->validated('course_class_id'),
|
||||
'academic_term_id' => $academicTermSelected
|
||||
? (string) ($academicTermId ?? 0)
|
||||
: ($academicTermId ? (string) $academicTermId : null),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(MaterialRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated(), $request->file('file'));
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Materi berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.academic-classes.materials.index');
|
||||
}
|
||||
|
||||
public function update(MaterialRequest $request, Material $material): RedirectResponse
|
||||
{
|
||||
$this->service->update($material, $request->validated(), $request->file('file'));
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Materi berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.academic-classes.materials.index');
|
||||
}
|
||||
|
||||
public function destroy(Material $material): RedirectResponse
|
||||
{
|
||||
abort_unless(request()->user()->canManageMaterial($material), 403);
|
||||
|
||||
$this->service->delete($material);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Materi berhasil dihapus.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\AcademicClasses;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\AcademicClasses\ScheduleRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Schedule;
|
||||
use App\Services\Admin\AcademicClasses\ScheduleService;
|
||||
use App\Services\Admin\Master\AcademicTermService;
|
||||
use App\Services\Admin\Master\CourseService;
|
||||
use App\Services\Admin\Master\DepartmentService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ScheduleController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ScheduleService $service,
|
||||
private readonly AcademicTermService $academicTermService,
|
||||
private readonly DepartmentService $departmentService,
|
||||
private readonly CourseService $courseService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
$isPersonalView = $user->hasRole(UserRole::Mahasiswa->value) || $user->hasRole(UserRole::Dosen->value);
|
||||
|
||||
$academicTermId = $request->has('academic_term_id')
|
||||
? $request->validated('academic_term_id')
|
||||
: $this->academicTermService->getActive()?->id;
|
||||
|
||||
return Inertia::render('admin/academic-classes/schedules/index', [
|
||||
'schedules' => $this->service->all(
|
||||
$user,
|
||||
academicTermId: $academicTermId,
|
||||
departmentId: $request->validated('department_id'),
|
||||
semesterNumber: $request->validated('semester_number'),
|
||||
),
|
||||
'courseClasses' => $this->service->courseClassOptions(),
|
||||
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
||||
'departments' => $this->departmentService->getAllForSelect(),
|
||||
'semesterNumbers' => $this->courseService->getSemesterNumbers(),
|
||||
'isPersonalView' => $isPersonalView,
|
||||
'filters' => [
|
||||
'academic_term_id' => $academicTermId ? (string) $academicTermId : null,
|
||||
'department_id' => $request->validated('department_id'),
|
||||
'semester_number' => $request->validated('semester_number'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(ScheduleRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Jadwal berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.academic-classes.schedules.index');
|
||||
}
|
||||
|
||||
public function update(ScheduleRequest $request, Schedule $schedule): RedirectResponse
|
||||
{
|
||||
$this->service->update($schedule, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Jadwal berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.academic-classes.schedules.index');
|
||||
}
|
||||
|
||||
public function destroy(Schedule $schedule): RedirectResponse
|
||||
{
|
||||
$this->service->delete($schedule);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Jadwal berhasil dihapus.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Developer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Services\Admin\Developer\LogViewerService;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class LogController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LogViewerService $service,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
$files = $this->service->availableFiles();
|
||||
$file = $request->validated('file') ?: $files->first()['name'] ?? null;
|
||||
|
||||
return Inertia::render('admin/developer/logs/index', [
|
||||
'entries' => $file
|
||||
? Inertia::scroll(fn () => $this->service->paginated(
|
||||
$file,
|
||||
...$request->validatedWithDefaults(),
|
||||
level: $request->validated('level'),
|
||||
))
|
||||
: null,
|
||||
'files' => $files,
|
||||
'selectedFile' => $file,
|
||||
'filters' => $request->only(['file', 'level']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function download(PaginatedRequest $request): BinaryFileResponse
|
||||
{
|
||||
$file = $request->validated('file');
|
||||
|
||||
abort_unless($file, 404);
|
||||
|
||||
$files = $this->service->availableFiles();
|
||||
|
||||
abort_unless($files->pluck('name')->contains($file), 404);
|
||||
|
||||
return response()->download(storage_path('logs/'.$file));
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Developer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Developer\UpdateRolePermissionsRequest;
|
||||
use App\Services\Admin\Developer\RolePermissionService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class RolePermissionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RolePermissionService $service,
|
||||
) {}
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
return Inertia::render('admin/developer/roles/index', [
|
||||
'roles' => $this->service->roles(),
|
||||
'permissionGroups' => $this->service->groupedPermissions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateRolePermissionsRequest $request, Role $role): RedirectResponse
|
||||
{
|
||||
$this->service->updatePermissions($role, $request->validated('permissions'));
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Permission role berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.developer.roles.index');
|
||||
}
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\FeedbackStatus;
|
||||
use App\Enums\FeedbackType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Feedback\FeedbackRequest;
|
||||
use App\Http\Requests\Admin\Feedback\ReplyFeedbackRequest;
|
||||
use App\Http\Requests\Admin\Feedback\UpdateFeedbackStatusRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Feedback;
|
||||
use App\Services\Admin\FeedbackService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class FeedbackController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FeedbackService $service,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/feedback/index', [
|
||||
'feedbacks' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
$request->user(),
|
||||
...$request->validatedWithDefaults(),
|
||||
type: $request->validated('type'),
|
||||
status: $request->validated('status'),
|
||||
)),
|
||||
'types' => FeedbackType::options(),
|
||||
'statuses' => FeedbackStatus::options(),
|
||||
'filters' => $request->only(['type', 'status']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/feedback/create', [
|
||||
'types' => FeedbackType::options(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(FeedbackRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->user(), $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Masukan berhasil dikirim.']);
|
||||
|
||||
return to_route('admin.feedback.index');
|
||||
}
|
||||
|
||||
public function edit(Request $request, Feedback $feedback): Response
|
||||
{
|
||||
abort_unless($feedback->user_id === $request->user()->id, 403);
|
||||
abort_unless($feedback->status === FeedbackStatus::Submitted, 403);
|
||||
|
||||
return Inertia::render('admin/feedback/edit', [
|
||||
'feedback' => $feedback,
|
||||
'types' => FeedbackType::options(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(FeedbackRequest $request, Feedback $feedback): RedirectResponse
|
||||
{
|
||||
$this->service->update($feedback, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Masukan berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.feedback.index');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Feedback $feedback): RedirectResponse
|
||||
{
|
||||
abort_unless($feedback->user_id === $request->user()->id, 403);
|
||||
abort_unless($feedback->status === FeedbackStatus::Submitted, 403);
|
||||
|
||||
$this->service->delete($feedback);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Masukan berhasil dihapus.'])->back();
|
||||
}
|
||||
|
||||
public function updateStatus(UpdateFeedbackStatusRequest $request, Feedback $feedback): RedirectResponse
|
||||
{
|
||||
$this->service->updateStatus($feedback, $request->validated('status'));
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status masukan berhasil diperbarui.'])->back();
|
||||
}
|
||||
|
||||
public function reply(ReplyFeedbackRequest $request, Feedback $feedback): RedirectResponse
|
||||
{
|
||||
$this->service->reply($feedback, $request->validated('reply'));
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Balasan berhasil dikirim.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Finances;
|
||||
|
||||
use App\Enums\StudentStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Finances\TuitionInvoiceRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\TuitionInvoice;
|
||||
use App\Services\Admin\Finances\TuitionInvoiceService;
|
||||
use App\Services\Admin\Master\AcademicTermService;
|
||||
use App\Services\Admin\Users\StudentService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class TuitionInvoiceController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TuitionInvoiceService $service,
|
||||
private readonly StudentService $studentService,
|
||||
private readonly AcademicTermService $academicTermService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
$students = $this->studentService->getAllForSelect(status: StudentStatus::Active->value);
|
||||
|
||||
$invoicedTermIdsByStudent = $this->service->getInvoicedTermIdsByStudent($students->pluck('id')->all());
|
||||
|
||||
$students->each(function ($student) use ($invoicedTermIdsByStudent) {
|
||||
$student->invoiced_term_ids = $invoicedTermIdsByStudent->get($student->id, []);
|
||||
});
|
||||
|
||||
$academicTerms = $this->academicTermService->getAllForSelect();
|
||||
|
||||
$requestedTermId = $request->validated('academic_term_id');
|
||||
$summaryTermId = $requestedTermId ?? $this->academicTermService->getActive()?->id;
|
||||
|
||||
return Inertia::render('admin/finances/tuition-invoices/index', [
|
||||
'invoices' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
...$request->validatedWithDefaults(),
|
||||
academicTermId: $requestedTermId,
|
||||
status: $request->validated('status'),
|
||||
paymentMethod: $request->validated('payment_method'),
|
||||
)),
|
||||
'summary' => $this->service->summary(
|
||||
search: $request->validated('search') ?? '',
|
||||
academicTermId: $summaryTermId,
|
||||
),
|
||||
'summaryAcademicTerm' => $academicTerms->firstWhere('id', $summaryTermId),
|
||||
'students' => $students,
|
||||
'academicTerms' => $academicTerms,
|
||||
'filters' => $request->only(['academic_term_id', 'status', 'payment_method']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(TuitionInvoiceRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->createMany($request->validated('student_ids'), $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tagihan berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.finances.tuition-invoices.index');
|
||||
}
|
||||
|
||||
public function update(TuitionInvoiceRequest $request, TuitionInvoice $tuitionInvoice): RedirectResponse
|
||||
{
|
||||
$this->service->update($tuitionInvoice, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tagihan berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.finances.tuition-invoices.index');
|
||||
}
|
||||
|
||||
public function destroy(TuitionInvoice $tuitionInvoice): RedirectResponse
|
||||
{
|
||||
$this->service->delete($tuitionInvoice);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Tagihan berhasil dihapus.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Finances;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Finances\TuitionPaymentRequest;
|
||||
use App\Models\TuitionInvoice;
|
||||
use App\Models\TuitionPayment;
|
||||
use App\Services\Admin\Finances\TuitionPaymentService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class TuitionPaymentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TuitionPaymentService $service,
|
||||
) {}
|
||||
|
||||
public function index(TuitionInvoice $tuitionInvoice): Response
|
||||
{
|
||||
$tuitionInvoice->load(['student.user.profile', 'student.department', 'academicTerm']);
|
||||
|
||||
return Inertia::render('admin/finances/tuition-invoices/payments', [
|
||||
'invoice' => $tuitionInvoice,
|
||||
'payments' => $this->service->forInvoice($tuitionInvoice),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(TuitionPaymentRequest $request, TuitionInvoice $tuitionInvoice): RedirectResponse
|
||||
{
|
||||
$this->service->create($tuitionInvoice, $request->validated(), $request->file('proof'));
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Pembayaran berhasil ditambahkan.'])->back();
|
||||
}
|
||||
|
||||
public function update(TuitionPaymentRequest $request, TuitionInvoice $tuitionInvoice, TuitionPayment $payment): RedirectResponse
|
||||
{
|
||||
$this->service->update($payment, $request->validated(), $request->file('proof'));
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Pembayaran berhasil diperbarui.'])->back();
|
||||
}
|
||||
|
||||
public function destroy(TuitionInvoice $tuitionInvoice, TuitionPayment $payment): RedirectResponse
|
||||
{
|
||||
$this->service->delete($payment);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Pembayaran berhasil dihapus.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Manage\AnnouncementRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Announcement;
|
||||
use App\Services\Admin\Manage\AnnouncementService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class AnnouncementController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AnnouncementService $service,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/manage/announcements/index', [
|
||||
'announcements' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
$request->user(),
|
||||
...$request->validatedWithDefaults(),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(AnnouncementRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengumuman berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.manage.announcements.index');
|
||||
}
|
||||
|
||||
public function update(AnnouncementRequest $request, Announcement $announcement): RedirectResponse
|
||||
{
|
||||
$this->service->update($announcement, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengumuman berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.manage.announcements.index');
|
||||
}
|
||||
|
||||
public function destroy(Announcement $announcement): RedirectResponse
|
||||
{
|
||||
$this->service->delete($announcement);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengumuman berhasil dihapus.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage\CourseClass;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ClassEnrollment;
|
||||
use App\Models\CourseClass;
|
||||
use App\Services\Admin\Manage\CourseClass\ClassEnrollmentService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ClassEnrollmentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ClassEnrollmentService $service,
|
||||
) {}
|
||||
|
||||
public function index(CourseClass $courseClass): Response
|
||||
{
|
||||
$courseClass->load(['course', 'lecturer.user.profile', 'academicTerm']);
|
||||
|
||||
return Inertia::render('admin/manage/course-classes/enrollments', [
|
||||
'courseClass' => $courseClass,
|
||||
'enrollments' => $this->service->forClass($courseClass),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(CourseClass $courseClass, ClassEnrollment $enrollment): RedirectResponse
|
||||
{
|
||||
$this->service->unenroll($enrollment);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Mahasiswa berhasil dikeluarkan dari kelas.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage\CourseClass;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Manage\CourseClass\CourseClassRequest;
|
||||
use App\Http\Requests\Admin\Manage\CourseClass\DuplicateCourseClassesRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\CourseClass;
|
||||
use App\Services\Admin\Manage\CourseClass\CourseClassService;
|
||||
use App\Services\Admin\Master\AcademicTermService;
|
||||
use App\Services\Admin\Master\CourseService;
|
||||
use App\Services\Admin\Users\LecturerService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class CourseClassController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CourseClassService $service,
|
||||
private readonly CourseService $courseService,
|
||||
private readonly LecturerService $lecturerService,
|
||||
private readonly AcademicTermService $academicTermService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/manage/course-classes/index', [
|
||||
'courseClasses' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
...$request->validatedWithDefaults(),
|
||||
academicTermId: $request->validated('academic_term_id'),
|
||||
method: $request->validated('method'),
|
||||
)),
|
||||
'courses' => $this->courseService->getAllForSelect(),
|
||||
'lecturers' => $this->lecturerService->getAllForSelect(),
|
||||
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
||||
'courseClassAssignments' => $this->service->getAllForSelect($request->user()),
|
||||
'filters' => $request->only(['academic_term_id', 'method']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(CourseClassRequest $request): RedirectResponse
|
||||
{
|
||||
$created = $this->service->createMany($request->validated());
|
||||
|
||||
Inertia::flash('toast', [
|
||||
'type' => 'success',
|
||||
'message' => $created->count() > 1
|
||||
? "{$created->count()} kelas mata kuliah berhasil ditambahkan."
|
||||
: 'Kelas mata kuliah berhasil ditambahkan.',
|
||||
]);
|
||||
|
||||
return to_route('admin.manage.course-classes.index');
|
||||
}
|
||||
|
||||
public function update(CourseClassRequest $request, CourseClass $courseClass): RedirectResponse
|
||||
{
|
||||
$this->service->update($courseClass, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kelas mata kuliah berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.manage.course-classes.index');
|
||||
}
|
||||
|
||||
public function destroy(CourseClass $courseClass): RedirectResponse
|
||||
{
|
||||
$this->service->delete($courseClass);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kelas mata kuliah berhasil dihapus.'])->back();
|
||||
}
|
||||
|
||||
public function duplicate(DuplicateCourseClassesRequest $request): RedirectResponse
|
||||
{
|
||||
$result = $this->service->duplicateFromTerm(
|
||||
$request->validated('source_academic_term_id'),
|
||||
$request->validated('target_academic_term_id'),
|
||||
);
|
||||
|
||||
$message = $result['created'] > 0
|
||||
? "{$result['created']} kelas mata kuliah berhasil diduplikasi."
|
||||
: 'Tidak ada kelas mata kuliah baru yang diduplikasi.';
|
||||
|
||||
if ($result['skipped'] > 0) {
|
||||
$message .= " {$result['skipped']} dilewati karena sudah ada di periode tujuan.";
|
||||
}
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => $message]);
|
||||
|
||||
return to_route('admin.manage.course-classes.index');
|
||||
}
|
||||
}
|
||||
@ -1,181 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Manage\CourseRegistration\RejectCourseRegistrationRequest;
|
||||
use App\Http\Requests\Admin\Manage\CourseRegistration\SignAsAdvisorCourseRegistrationRequest;
|
||||
use App\Http\Requests\Admin\Manage\CourseRegistration\SignAsDepartmentLeaderCourseRegistrationRequest;
|
||||
use App\Http\Requests\Admin\Manage\CourseRegistration\UpdateCourseRegistrationRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Course;
|
||||
use App\Models\CourseRegistrationSubmission;
|
||||
use App\Models\Student;
|
||||
use App\Services\Admin\Manage\CourseRegistrationService;
|
||||
use App\Services\Admin\Master\AcademicTermService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class CourseRegistrationController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CourseRegistrationService $service,
|
||||
private readonly AcademicTermService $academicTermService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response|RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($user->hasRole(UserRole::Mahasiswa->value)) {
|
||||
return to_route('admin.manage.course-registrations.show', $user->student->id);
|
||||
}
|
||||
|
||||
return Inertia::render('admin/manage/course-registrations/index', [
|
||||
'departments' => $this->service->departmentSummary(),
|
||||
'registrations' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
$user,
|
||||
...$request->validatedWithDefaults(),
|
||||
departmentId: $request->validated('department_id'),
|
||||
)),
|
||||
'filters' => $request->only(['department_id']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Student $student): Response
|
||||
{
|
||||
$user = request()->user();
|
||||
$isStudent = $user->hasRole(UserRole::Mahasiswa->value);
|
||||
|
||||
abort_if($isStudent && $user->student->id !== $student->id, 403);
|
||||
abort_if(! $isStudent && ! $user->canAccessCourseRegistrationOf($student), 403);
|
||||
|
||||
$backHref = $isStudent
|
||||
? null
|
||||
: route('admin.manage.course-registrations.index');
|
||||
|
||||
$canReview = ! $isStudent
|
||||
&& $user->can('approve-course-registrations')
|
||||
&& $user->canReviewCourseRegistrationOf($student);
|
||||
|
||||
$canSignAsDepartmentLeader = $user->isDepartmentLeaderOf($student);
|
||||
$canSignAsAdvisor = $user->isAdvisorOf($student);
|
||||
|
||||
$activeTerm = $this->academicTermService->getActive();
|
||||
$allCourseClasses = $activeTerm
|
||||
? $this->service->availableCourseClasses($student->department_id, $activeTerm->id)
|
||||
: collect();
|
||||
|
||||
$departmentCourses = $this->service->departmentCourses($student->department_id);
|
||||
|
||||
$semesters = $departmentCourses
|
||||
->map(fn (Course $course) => $course->semester_number)
|
||||
->unique()
|
||||
->sort()
|
||||
->values();
|
||||
|
||||
$submissions = $activeTerm
|
||||
? $semesters->mapWithKeys(fn (?int $semesterNumber) => [
|
||||
CourseRegistrationSubmission::semesterKey($semesterNumber) => $this->service->buildSubmissionPayload(
|
||||
$student,
|
||||
$activeTerm,
|
||||
$semesterNumber,
|
||||
$departmentCourses,
|
||||
$allCourseClasses,
|
||||
),
|
||||
])
|
||||
: collect();
|
||||
|
||||
return Inertia::render('admin/manage/course-registrations/show', [
|
||||
'submissions' => $submissions,
|
||||
'semesters' => $semesters->values(),
|
||||
'openSemesters' => $activeTerm?->open_semesters ?? [],
|
||||
'studentCurrentSemester' => $student->current_semester,
|
||||
'hasActiveTerm' => $activeTerm !== null,
|
||||
'backHref' => $backHref,
|
||||
'canReview' => $canReview,
|
||||
'canSignAsDepartmentLeader' => $canSignAsDepartmentLeader,
|
||||
'canSignAsAdvisor' => $canSignAsAdvisor,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Student $student, string $semester, UpdateCourseRegistrationRequest $request): RedirectResponse
|
||||
{
|
||||
$semesterNumber = CourseRegistrationSubmission::parseSemesterKey($semester);
|
||||
|
||||
$activeTerm = $this->academicTermService->getActive();
|
||||
|
||||
abort_if($activeTerm === null, 422);
|
||||
|
||||
$selectableIds = $this->service->selectableCourseClassIds(
|
||||
$student->department_id,
|
||||
$activeTerm->id,
|
||||
$semesterNumber,
|
||||
$activeTerm->open_semesters ?? [],
|
||||
$student->current_semester,
|
||||
);
|
||||
|
||||
$selectedIds = array_values(array_intersect(
|
||||
$request->validated('course_class_ids') ?? [],
|
||||
$selectableIds,
|
||||
));
|
||||
|
||||
$submission = $this->service->findOrCreateForStudent($student, $activeTerm, $semesterNumber);
|
||||
|
||||
$this->service->saveRegistrations(
|
||||
$submission,
|
||||
$selectedIds,
|
||||
$activeTerm->id,
|
||||
$student->id,
|
||||
$request->file('signature'),
|
||||
);
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'KRS berhasil disimpan.']);
|
||||
|
||||
return to_route('admin.manage.course-registrations.show', $student->id);
|
||||
}
|
||||
|
||||
public function signAsDepartmentLeader(Student $student, string $semester, SignAsDepartmentLeaderCourseRegistrationRequest $request): RedirectResponse
|
||||
{
|
||||
$submission = $this->service->findOrCreateForSemester($student, $semester);
|
||||
|
||||
$this->service->signAsDepartmentLeader($submission, $request->file('signature'));
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tanda tangan berhasil disimpan.']);
|
||||
|
||||
return to_route('admin.manage.course-registrations.show', $student->id);
|
||||
}
|
||||
|
||||
public function signAsAdvisor(Student $student, string $semester, SignAsAdvisorCourseRegistrationRequest $request): RedirectResponse
|
||||
{
|
||||
$submission = $this->service->findOrCreateForSemester($student, $semester);
|
||||
|
||||
$this->service->signAsAdvisor($submission, $request->file('signature'));
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tanda tangan berhasil disimpan.']);
|
||||
|
||||
return to_route('admin.manage.course-registrations.show', $student->id);
|
||||
}
|
||||
|
||||
public function approve(CourseRegistrationSubmission $submission): RedirectResponse
|
||||
{
|
||||
abort_unless(
|
||||
request()->user()->can('approve-course-registrations')
|
||||
&& request()->user()->canReviewCourseRegistrationOf($submission->student),
|
||||
403
|
||||
);
|
||||
|
||||
$this->service->approve($submission);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'KRS berhasil disetujui.'])->back();
|
||||
}
|
||||
|
||||
public function reject(RejectCourseRegistrationRequest $request, CourseRegistrationSubmission $submission): RedirectResponse
|
||||
{
|
||||
$this->service->reject($submission, $request->validated('reason'));
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'KRS berhasil ditolak.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Master;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Master\AcademicTermRequest;
|
||||
use App\Http\Requests\Admin\UpdateStatusRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\AcademicTerm;
|
||||
use App\Services\Admin\Master\AcademicTermService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class AcademicTermController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AcademicTermService $service,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/master/academic-terms/index', [
|
||||
'academicTerms' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
...$request->validatedWithDefaults(),
|
||||
semester: $request->validated('semester'),
|
||||
isActive: $request->filled('is_active') ? $request->boolean('is_active') : null,
|
||||
)),
|
||||
'filters' => $request->only(['semester', 'is_active']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(AcademicTermRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Periode akademik berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.master.academic-terms.index');
|
||||
}
|
||||
|
||||
public function update(AcademicTermRequest $request, AcademicTerm $academicTerm): RedirectResponse
|
||||
{
|
||||
$this->service->update($academicTerm, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Periode akademik berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.master.academic-terms.index');
|
||||
}
|
||||
|
||||
public function destroy(AcademicTerm $academicTerm): RedirectResponse
|
||||
{
|
||||
$this->service->delete($academicTerm);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Periode akademik berhasil dihapus.'])->back();
|
||||
}
|
||||
|
||||
public function updateStatus(UpdateStatusRequest $request, AcademicTerm $academicTerm): RedirectResponse
|
||||
{
|
||||
$this->service->updateActiveStatus($academicTerm, $request->boolean('is_active'));
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status periode akademik berhasil diperbarui.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Master;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Master\CourseRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Course;
|
||||
use App\Services\Admin\Master\CourseService;
|
||||
use App\Services\Admin\Master\DepartmentService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class CourseController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CourseService $service,
|
||||
private readonly DepartmentService $departmentService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/master/courses/index', [
|
||||
'courses' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
$request->user(),
|
||||
...$request->validatedWithDefaults(),
|
||||
departmentId: $request->validated('department_id'),
|
||||
semesterNumber: $request->validated('semester_number'),
|
||||
)),
|
||||
'departments' => $this->departmentService->getAllForSelect(),
|
||||
'semesterNumbers' => $this->service->getSemesterNumbers(),
|
||||
'filters' => $request->only(['department_id', 'semester_number']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(CourseRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Mata kuliah berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.master.courses.index');
|
||||
}
|
||||
|
||||
public function update(CourseRequest $request, Course $course): RedirectResponse
|
||||
{
|
||||
$this->service->update($course, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Mata kuliah berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.master.courses.index');
|
||||
}
|
||||
|
||||
public function destroy(Course $course): RedirectResponse
|
||||
{
|
||||
$this->service->delete($course);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Mata kuliah berhasil dihapus.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Master;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Master\DepartmentRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Department;
|
||||
use App\Services\Admin\Master\DepartmentService;
|
||||
use App\Services\Admin\Users\LecturerService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class DepartmentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DepartmentService $service,
|
||||
private readonly LecturerService $lecturerService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/master/departments/index', [
|
||||
'departments' => Inertia::scroll(fn () => $this->service->paginated(...$request->validatedWithDefaults())),
|
||||
'lecturers' => $this->lecturerService->getAllForSelect(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(DepartmentRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Jurusan berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.master.departments.index');
|
||||
}
|
||||
|
||||
public function update(DepartmentRequest $request, Department $department): RedirectResponse
|
||||
{
|
||||
$this->service->update($department, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Jurusan berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.master.departments.index');
|
||||
}
|
||||
|
||||
public function destroy(Department $department): RedirectResponse
|
||||
{
|
||||
$this->service->delete($department);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Jurusan berhasil dihapus.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Services;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Services\AcademicAdvisingLogRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\AcademicAdvisingLog;
|
||||
use App\Services\Admin\Services\AcademicAdvisingLogService;
|
||||
use App\Services\Admin\Users\LecturerService;
|
||||
use App\Services\Admin\Users\StudentService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class AcademicAdvisingLogController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AcademicAdvisingLogService $service,
|
||||
private readonly StudentService $studentService,
|
||||
private readonly LecturerService $lecturerService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/services/academic-advising-logs/index', [
|
||||
'logs' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
$request->user(),
|
||||
...$request->validatedWithDefaults(),
|
||||
lecturerId: $request->validated('lecturer_id'),
|
||||
)),
|
||||
'students' => $this->studentService->getAllForSelect(),
|
||||
'lecturers' => $this->lecturerService->getAllForSelect(),
|
||||
'filters' => $request->only(['lecturer_id']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(AcademicAdvisingLogRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.services.academic-advising-logs.index');
|
||||
}
|
||||
|
||||
public function update(AcademicAdvisingLogRequest $request, AcademicAdvisingLog $academicAdvisingLog): RedirectResponse
|
||||
{
|
||||
$this->service->update($academicAdvisingLog, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.services.academic-advising-logs.index');
|
||||
}
|
||||
|
||||
public function destroy(AcademicAdvisingLog $academicAdvisingLog): RedirectResponse
|
||||
{
|
||||
$this->service->delete($academicAdvisingLog);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil dihapus.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Services;
|
||||
|
||||
use App\Enums\LetterStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Services\LetterRequest\LetterRequestRequest;
|
||||
use App\Http\Requests\Admin\Services\LetterRequest\LetterRequestStatusRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\LetterRequest;
|
||||
use App\Services\Admin\Services\LetterRequestService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class LetterRequestController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LetterRequestService $service,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/services/letter-requests/index', [
|
||||
'letterRequests' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
$request->user(),
|
||||
...$request->validatedWithDefaults(),
|
||||
status: $request->validated('status'),
|
||||
)),
|
||||
'filters' => $request->only(['status']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(LetterRequestRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create([
|
||||
'user_id' => auth()->id(),
|
||||
...$request->validated(),
|
||||
]);
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil diajukan.']);
|
||||
|
||||
return to_route('admin.services.letter-requests.index');
|
||||
}
|
||||
|
||||
public function update(LetterRequestRequest $request, LetterRequest $letterRequest): RedirectResponse
|
||||
{
|
||||
$this->service->update($letterRequest, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.services.letter-requests.index');
|
||||
}
|
||||
|
||||
public function destroy(LetterRequest $letterRequest): RedirectResponse
|
||||
{
|
||||
abort_if($letterRequest->status !== LetterStatus::Submitted, 403);
|
||||
|
||||
$this->service->delete($letterRequest);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil dihapus.'])->back();
|
||||
}
|
||||
|
||||
public function updateStatus(LetterRequestStatusRequest $request, LetterRequest $letterRequest): RedirectResponse
|
||||
{
|
||||
$this->service->updateStatus($letterRequest, $request->validated('status'), $request->file('result'));
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status surat permohonan berhasil diperbarui.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Settings\ProfileDeleteRequest;
|
||||
use App\Http\Requests\Admin\Settings\ProfileUpdateRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
[$role, $roleData] = match (true) {
|
||||
$user->hasRole('mahasiswa') => $this->studentRoleData($user),
|
||||
$user->hasRole('dosen') => $this->lecturerRoleData($user),
|
||||
default => $this->administratorRoleData($user),
|
||||
};
|
||||
|
||||
return Inertia::render('admin/settings/profile', [
|
||||
'mustVerifyEmail' => $user instanceof MustVerifyEmail,
|
||||
'status' => $request->session()->get('status'),
|
||||
'profile' => $user->profile,
|
||||
'role' => $role,
|
||||
'roleData' => $roleData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$user->fill($request->safe()->only(['username', 'email']));
|
||||
|
||||
if ($user->isDirty('email')) {
|
||||
$user->email_verified_at = null;
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
$user->profile()->updateOrCreate([], $request->safe()->only([
|
||||
'full_name', 'phone_number', 'address', 'gender', 'birth_date', 'birth_place',
|
||||
]));
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Profil berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.settings.profile.edit');
|
||||
}
|
||||
|
||||
public function destroy(ProfileDeleteRequest $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
private function studentRoleData(User $user): array
|
||||
{
|
||||
$user->load(['profile', 'student.department', 'student.academicAdvisor.user.profile']);
|
||||
|
||||
return ['mahasiswa', $user->student ? [
|
||||
'student_number' => $user->student->student_number,
|
||||
'department' => $user->student->department?->name,
|
||||
'enrollment_year' => $user->student->enrollment_year,
|
||||
'academic_advisor' => $user->student->academicAdvisor?->user?->profile?->full_name,
|
||||
] : null];
|
||||
}
|
||||
|
||||
private function lecturerRoleData(User $user): array
|
||||
{
|
||||
$user->load(['profile', 'lecturer.departments']);
|
||||
|
||||
return ['dosen', $user->lecturer ? [
|
||||
'lecturer_number' => $user->lecturer->lecturer_number,
|
||||
'department' => $user->lecturer->departments->pluck('name')->join(', ') ?: null,
|
||||
] : null];
|
||||
}
|
||||
|
||||
private function administratorRoleData(User $user): array
|
||||
{
|
||||
$user->load(['profile', 'roles']);
|
||||
|
||||
return ['admin', [
|
||||
'roles' => $user->roles->pluck('name')->all(),
|
||||
]];
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Settings\PasswordUpdateRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class SecurityController extends Controller
|
||||
{
|
||||
public function edit(): Response
|
||||
{
|
||||
return Inertia::render('admin/settings/security', [
|
||||
'passwordRules' => Password::defaults()->toPasswordRulesString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(PasswordUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$request->user()->update([
|
||||
'password' => $request->password,
|
||||
]);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil diperbarui.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Users;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\UpdateStatusRequest;
|
||||
use App\Http\Requests\Admin\Users\AdministratorRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\Admin\Users\AdministratorService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class AdministratorController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdministratorService $service,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/users/administrators/index', [
|
||||
'administrators' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
...$request->validatedWithDefaults(),
|
||||
gender: $request->validated('gender'),
|
||||
)),
|
||||
'filters' => $request->only(['gender']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/users/administrators/create', [
|
||||
'roles' => $this->service->availableRoles(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(AdministratorRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Administrator berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.users.administrators.index');
|
||||
}
|
||||
|
||||
public function edit(User $user): Response
|
||||
{
|
||||
$user->load(['profile', 'roles']);
|
||||
|
||||
return Inertia::render('admin/users/administrators/edit', [
|
||||
'user' => $user,
|
||||
'roles' => $this->service->availableRoles(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(AdministratorRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->service->update($user, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Administrator berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.users.administrators.index');
|
||||
}
|
||||
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->delete($user);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Administrator berhasil dihapus.'])->back();
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->resetPassword($user);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil direset.'])->back();
|
||||
}
|
||||
|
||||
public function updateUserStatus(UpdateStatusRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->service->updateUserStatus($user, $request->boolean('is_active'));
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status akun berhasil diperbarui.'])->back();
|
||||
}
|
||||
}
|
||||
@ -1,103 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Users;
|
||||
|
||||
use App\Exports\LecturersExport;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\UpdateStatusRequest;
|
||||
use App\Http\Requests\Admin\Users\LecturerRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\Admin\Master\DepartmentService;
|
||||
use App\Services\Admin\Users\LecturerService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class LecturerController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LecturerService $service,
|
||||
private readonly DepartmentService $departmentService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/users/lecturers/index', [
|
||||
'lecturers' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
...$request->validatedWithDefaults(),
|
||||
gender: $request->validated('gender'),
|
||||
departmentId: $request->validated('department_id'),
|
||||
)),
|
||||
'departments' => $this->departmentService->getAllForSelect(),
|
||||
'filters' => $request->only(['gender', 'department_id']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/users/lecturers/create', [
|
||||
'departments' => $this->departmentService->getAllForSelect(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(LecturerRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Dosen berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.users.lecturers.index');
|
||||
}
|
||||
|
||||
public function edit(User $user): Response
|
||||
{
|
||||
$user->load(['profile', 'lecturer.departments']);
|
||||
|
||||
return Inertia::render('admin/users/lecturers/edit', [
|
||||
'user' => $user,
|
||||
'departments' => $this->departmentService->getAllForSelect(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(LecturerRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->service->update($user, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Dosen berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.users.lecturers.index');
|
||||
}
|
||||
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->delete($user);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Dosen berhasil dihapus.'])->back();
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->resetPassword($user);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil direset.'])->back();
|
||||
}
|
||||
|
||||
public function updateUserStatus(UpdateStatusRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->service->updateUserStatus($user, $request->boolean('is_active'));
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status akun berhasil diperbarui.'])->back();
|
||||
}
|
||||
|
||||
public function export(PaginatedRequest $request): BinaryFileResponse
|
||||
{
|
||||
return Excel::download(new LecturersExport(
|
||||
search: $request->validated('search') ?? '',
|
||||
gender: $request->validated('gender'),
|
||||
departmentId: $request->validated('department_id'),
|
||||
), 'dosen.xlsx');
|
||||
}
|
||||
}
|
||||
@ -1,124 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Users;
|
||||
|
||||
use App\Enums\StudentStatus;
|
||||
use App\Exports\StudentsExport;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\UpdateStatusRequest;
|
||||
use App\Http\Requests\Admin\Users\Student\StudentRequest;
|
||||
use App\Http\Requests\Admin\Users\Student\StudentStatusRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\Admin\Master\DepartmentService;
|
||||
use App\Services\Admin\Users\LecturerService;
|
||||
use App\Services\Admin\Users\StudentService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class StudentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StudentService $service,
|
||||
private readonly DepartmentService $departmentService,
|
||||
private readonly LecturerService $lecturerService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/users/students/index', [
|
||||
'students' => Inertia::scroll(fn () => $this->service->paginated(
|
||||
$request->user(),
|
||||
...$request->validatedWithDefaults(),
|
||||
gender: $request->validated('gender'),
|
||||
departmentId: $request->validated('department_id'),
|
||||
status: $request->validated('status'),
|
||||
enrollmentYear: $request->validated('enrollment_year'),
|
||||
)),
|
||||
'departments' => $this->departmentService->getAllForSelect(),
|
||||
'statuses' => StudentStatus::options(),
|
||||
'enrollmentYears' => $this->service->getEnrollmentYears(),
|
||||
'filters' => $request->only(['gender', 'department_id', 'status', 'enrollment_year']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/users/students/create', [
|
||||
'departments' => $this->departmentService->getAllForSelect(),
|
||||
'lecturers' => $this->lecturerService->getAllForSelect(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StudentRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Mahasiswa berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.users.students.index');
|
||||
}
|
||||
|
||||
public function edit(User $user): Response
|
||||
{
|
||||
$user->load(['profile', 'student.department', 'student.academicAdvisor.user.profile']);
|
||||
|
||||
return Inertia::render('admin/users/students/edit', [
|
||||
'user' => $user,
|
||||
'departments' => $this->departmentService->getAllForSelect(),
|
||||
'lecturers' => $this->lecturerService->getAllForSelect(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(StudentRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->service->update($user, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Mahasiswa berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.users.students.index');
|
||||
}
|
||||
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->delete($user);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Mahasiswa berhasil dihapus.'])->back();
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): RedirectResponse
|
||||
{
|
||||
$this->service->resetPassword($user);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil direset.'])->back();
|
||||
}
|
||||
|
||||
public function updateStatus(StudentStatusRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->service->updateStatus($user, $request->validated('status'));
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status mahasiswa berhasil diperbarui.'])->back();
|
||||
}
|
||||
|
||||
public function updateUserStatus(UpdateStatusRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->service->updateUserStatus($user, $request->boolean('is_active'));
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status akun berhasil diperbarui.'])->back();
|
||||
}
|
||||
|
||||
public function export(PaginatedRequest $request): BinaryFileResponse
|
||||
{
|
||||
return Excel::download(new StudentsExport(
|
||||
user: $request->user(),
|
||||
search: $request->validated('search') ?? '',
|
||||
gender: $request->validated('gender'),
|
||||
departmentId: $request->validated('department_id'),
|
||||
status: $request->validated('status'),
|
||||
enrollmentYear: $request->validated('enrollment_year'),
|
||||
), 'mahasiswa.xlsx');
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use Inertia\Inertia;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
|
||||
|
||||
class GoogleAuthController extends Controller
|
||||
{
|
||||
public function redirect(): SymfonyRedirectResponse
|
||||
{
|
||||
return Socialite::driver('google')->redirect();
|
||||
}
|
||||
|
||||
public function callback(Request $request): RedirectResponse
|
||||
{
|
||||
$googleUser = Socialite::driver('google')->user();
|
||||
|
||||
$user = User::where('email', $googleUser->getEmail())->first();
|
||||
|
||||
if (! $user) {
|
||||
Inertia::flash('toast', [
|
||||
'type' => 'error',
|
||||
'message' => 'Akun dengan email Google tersebut tidak ditemukan. Silakan hubungi administrator.',
|
||||
]);
|
||||
|
||||
return Redirect::route('login');
|
||||
}
|
||||
|
||||
if (! $user->is_active) {
|
||||
Inertia::flash('toast', [
|
||||
'type' => 'error',
|
||||
'message' => 'Akun Anda tidak aktif. Silakan hubungi administrator.',
|
||||
]);
|
||||
|
||||
return Redirect::route('login');
|
||||
}
|
||||
|
||||
if ($user->google_id !== $googleUser->getId()) {
|
||||
$user->update(['google_id' => $googleUser->getId()]);
|
||||
}
|
||||
|
||||
Auth::login($user, remember: true);
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return Redirect::intended(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\MediaUploadRequest;
|
||||
use App\Services\MediaUploadService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class MediaUploadController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MediaUploadService $service,
|
||||
) {}
|
||||
|
||||
public function store(MediaUploadRequest $request): JsonResponse
|
||||
{
|
||||
$media = $this->service->store(
|
||||
$request->user(),
|
||||
$request->file('file'),
|
||||
$request->validated('collection') ?? 'content',
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'id' => $media->id,
|
||||
'name' => $media->name,
|
||||
'file_name' => $media->file_name,
|
||||
'mime_type' => $media->mime_type,
|
||||
'size' => $media->size,
|
||||
'url' => $media->getUrl(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Notification;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NotificationService $service,
|
||||
) {}
|
||||
|
||||
public function markAllRead(Request $request): RedirectResponse
|
||||
{
|
||||
$this->service->markAllAsRead($request->user());
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function destroyAll(Request $request): RedirectResponse
|
||||
{
|
||||
$this->service->deleteAll($request->user());
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function markRead(Request $request, Notification $notification): RedirectResponse
|
||||
{
|
||||
abort_unless($notification->user_id === $request->user()->id, 403);
|
||||
|
||||
$this->service->markAsRead($notification);
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Notification $notification): RedirectResponse
|
||||
{
|
||||
abort_unless($notification->user_id === $request->user()->id, 403);
|
||||
|
||||
$this->service->delete($notification);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
62
app/Http/Controllers/Settings/ProfileController.php
Normal file
62
app/Http/Controllers/Settings/ProfileController.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Settings\ProfileDeleteRequest;
|
||||
use App\Http\Requests\Settings\ProfileUpdateRequest;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user's profile settings page.
|
||||
*/
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
return Inertia::render('settings/profile', [
|
||||
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
|
||||
'status' => $request->session()->get('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*/
|
||||
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$request->user()->fill($request->validated());
|
||||
|
||||
if ($request->user()->isDirty('email')) {
|
||||
$request->user()->email_verified_at = null;
|
||||
}
|
||||
|
||||
$request->user()->save();
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => __('Profile updated.')]);
|
||||
|
||||
return to_route('profile.edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's profile.
|
||||
*/
|
||||
public function destroy(ProfileDeleteRequest $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
66
app/Http/Controllers/Settings/SecurityController.php
Normal file
66
app/Http/Controllers/Settings/SecurityController.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Settings\PasswordUpdateRequest;
|
||||
use App\Http\Requests\Settings\TwoFactorAuthenticationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
class SecurityController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user's security settings page.
|
||||
*/
|
||||
public function edit(TwoFactorAuthenticationRequest $request): Response
|
||||
{
|
||||
$props = [
|
||||
'canManageTwoFactor' => Features::canManageTwoFactorAuthentication(),
|
||||
'canManagePasskeys' => Features::canManagePasskeys(),
|
||||
'passkeys' => Features::canManagePasskeys()
|
||||
? $request->user()
|
||||
->passkeys()
|
||||
->select(['id', 'name', 'credential', 'created_at', 'last_used_at'])
|
||||
->latest()
|
||||
->get()
|
||||
->map(fn ($passkey) => [
|
||||
'id' => $passkey->id,
|
||||
'name' => $passkey->name,
|
||||
'authenticator' => $passkey->authenticator,
|
||||
'created_at_diff' => $passkey->created_at->diffForHumans(),
|
||||
'last_used_at_diff' => $passkey->last_used_at?->diffForHumans(),
|
||||
])
|
||||
->values()
|
||||
->all()
|
||||
: [],
|
||||
'passwordRules' => Password::defaults()->toPasswordRulesString(),
|
||||
];
|
||||
|
||||
if (Features::canManageTwoFactorAuthentication()) {
|
||||
$request->ensureStateIsValid();
|
||||
|
||||
$props['twoFactorEnabled'] = $request->user()->hasEnabledTwoFactorAuthentication();
|
||||
$props['requiresConfirmation'] = Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm');
|
||||
}
|
||||
|
||||
return Inertia::render('settings/security', $props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function update(PasswordUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$request->user()->update([
|
||||
'password' => $request->password,
|
||||
]);
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => __('Password updated.')]);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,7 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\Admin\FeedbackService;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
@ -42,19 +39,9 @@ public function share(Request $request): array
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'auth' => [
|
||||
'user' => $request->user()?->load('roles:id,name'),
|
||||
'permissions' => $request->user()?->getAllPermissions()->pluck('name')->values() ?? [],
|
||||
'user' => $request->user(),
|
||||
],
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||
'unreadNotificationsCount' => fn () => $request->user()
|
||||
? app(NotificationService::class)->unreadCount($request->user())
|
||||
: 0,
|
||||
'pendingFeedbackCount' => fn () => $request->user()
|
||||
? app(FeedbackService::class)->pendingCount($request->user())
|
||||
: 0,
|
||||
'notifications' => Inertia::merge(fn () => $request->user()
|
||||
? app(NotificationService::class)->paginated($request->user())
|
||||
: null)->append('data', 'id'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\AcademicClasses\Assignment;
|
||||
|
||||
use App\Enums\AssignmentStatus;
|
||||
use App\Enums\UserRole;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AssignmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
if ($this->isMethod('post')) {
|
||||
return $this->user()->can('create-assignments');
|
||||
}
|
||||
|
||||
return $this->user()->can('update-assignments')
|
||||
&& $this->user()->canManageAssignment($this->route('assignment'));
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$user = $this->user();
|
||||
|
||||
$courseClassRule = Rule::exists('course_classes', 'id');
|
||||
|
||||
if ($user->hasRole(UserRole::Dosen->value)) {
|
||||
$courseClassRule->where('lecturer_id', $user->lecturer?->id);
|
||||
}
|
||||
|
||||
return [
|
||||
'course_class_id' => ['required', 'integer', $courseClassRule],
|
||||
'title' => ['required', 'string', 'max:150'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'deadline' => ['required', 'date'],
|
||||
'status' => ['nullable', Rule::enum(AssignmentStatus::class)],
|
||||
'attachment' => ['nullable', 'file', 'max:10240', 'mimes:pdf,doc,docx,ppt,pptx,xls,xlsx,jpg,jpeg,png,mp4,zip'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\AcademicClasses\Assignment;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class GradeSubmissionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('update-assignment-submissions')
|
||||
&& $this->user()->canManageAssignment($this->route('assignment'));
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'score' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\AcademicClasses\Assignment;
|
||||
|
||||
use App\Enums\AssignmentStatus;
|
||||
use App\Enums\RegistrationStatus;
|
||||
use App\Models\Submission;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SubmitAssignmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
if (! $this->user()->can('submit-assignments')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$student = $this->user()->student;
|
||||
$assignment = $this->route('assignment');
|
||||
|
||||
if (! $student || ! $assignment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether the assignment still accepts submissions is governed by its
|
||||
// status, not the deadline — a lecturer closes it explicitly.
|
||||
if ($assignment->status !== AssignmentStatus::Open) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $assignment->courseClass()
|
||||
->whereHas('registrations', function ($q) use ($student) {
|
||||
$q->where('student_id', $student->id)
|
||||
->whereHas('submission', fn ($q) => $q->where('status', RegistrationStatus::Approved));
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$assignment = $this->route('assignment');
|
||||
$student = $this->user()->student;
|
||||
|
||||
$hasExistingFile = Submission::query()
|
||||
->where('assignment_id', $assignment->id)
|
||||
->where('student_id', $student?->id)
|
||||
->whereHas('media')
|
||||
->exists();
|
||||
|
||||
return [
|
||||
'notes' => ['nullable', 'string'],
|
||||
'file' => [
|
||||
$hasExistingFile ? 'nullable' : 'required',
|
||||
'file',
|
||||
'max:10240',
|
||||
'mimes:pdf,doc,docx,ppt,pptx,xls,xlsx,jpg,jpeg,png,mp4,zip',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\AcademicClasses;
|
||||
|
||||
use App\Enums\AttendanceStatus;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AttendanceStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
if (! $this->user()->can('create-attendances')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = $this->user();
|
||||
$courseClass = $this->route('course_class');
|
||||
|
||||
if ($user->hasRole('dosen') && $courseClass?->lecturer_id !== $user->lecturer?->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'date' => ['required', 'date'],
|
||||
'records' => ['required', 'array', 'min:1'],
|
||||
'records.*.student_id' => ['required', 'integer', Rule::exists('students', 'id')],
|
||||
'records.*.status' => ['required', Rule::enum(AttendanceStatus::class)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\AcademicClasses;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class MaterialRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
if ($this->isMethod('post')) {
|
||||
return $this->user()->can('create-materials');
|
||||
}
|
||||
|
||||
return $this->user()->can('update-materials')
|
||||
&& $this->user()->canManageMaterial($this->route('material'));
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$user = $this->user();
|
||||
|
||||
$courseClassRule = Rule::exists('course_classes', 'id');
|
||||
|
||||
if ($user->hasRole(UserRole::Dosen->value)) {
|
||||
$courseClassRule->where('lecturer_id', $user->lecturer?->id);
|
||||
}
|
||||
|
||||
return [
|
||||
'course_class_id' => ['required', 'integer', $courseClassRule],
|
||||
'title' => ['required', 'string', 'max:150'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'meeting_number' => ['nullable', 'integer', 'min:1'],
|
||||
'file' => [
|
||||
'nullable',
|
||||
'file',
|
||||
'max:10240',
|
||||
'mimes:pdf,doc,docx,ppt,pptx,xls,xlsx,jpg,jpeg,png,mp4,zip',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\AcademicClasses;
|
||||
|
||||
use App\Enums\DayOfWeek;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ScheduleRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-schedules' : 'update-schedules');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'course_class_id' => ['required', 'integer', Rule::exists('course_classes', 'id')],
|
||||
'day_of_week' => ['required', 'string', Rule::enum(DayOfWeek::class)],
|
||||
'start_time' => ['required', 'date_format:H:i'],
|
||||
'end_time' => ['required', 'date_format:H:i', 'after:start_time'],
|
||||
'room' => ['nullable', 'string', 'max:20'],
|
||||
'online_link' => ['nullable', 'string', 'max:255', 'url'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Developer;
|
||||
|
||||
use App\Support\PermissionCatalog;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateRolePermissionsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('update-roles');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'permissions' => ['present', 'array'],
|
||||
'permissions.*' => ['string', Rule::in(PermissionCatalog::all())],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Feedback;
|
||||
|
||||
use App\Enums\FeedbackStatus;
|
||||
use App\Enums\FeedbackType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class FeedbackRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
if ($this->isMethod('post')) {
|
||||
return $this->user()->can('create-feedback');
|
||||
}
|
||||
|
||||
$feedback = $this->route('feedback');
|
||||
|
||||
return $this->user()->can('update-feedback')
|
||||
&& $feedback->user_id === $this->user()->id
|
||||
&& $feedback->status === FeedbackStatus::Submitted;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => ['required', 'string', Rule::enum(FeedbackType::class)],
|
||||
'subject' => ['required', 'string', 'max:150'],
|
||||
'message' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Feedback;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ReplyFeedbackRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('reply-feedback');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'reply' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Feedback;
|
||||
|
||||
use App\Enums\FeedbackStatus;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateFeedbackStatusRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('update-feedback-status');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => ['required', Rule::enum(FeedbackStatus::class)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Finances;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class TuitionInvoiceRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-tuition-invoices' : 'update-tuition-invoices');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')],
|
||||
'amount_due' => ['required', 'numeric', 'min:0'],
|
||||
'due_date' => ['required', 'date'],
|
||||
];
|
||||
|
||||
if ($this->isMethod('post')) {
|
||||
$rules['student_ids'] = ['required', 'array', 'min:1'];
|
||||
$rules['student_ids.*'] = [
|
||||
'integer',
|
||||
Rule::exists('students', 'id'),
|
||||
Rule::unique('tuition_invoices', 'student_id')
|
||||
->where('academic_term_id', $this->input('academic_term_id')),
|
||||
];
|
||||
} else {
|
||||
$invoice = $this->route('tuition_invoice');
|
||||
|
||||
$rules['student_id'] = [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('students', 'id'),
|
||||
Rule::unique('tuition_invoices', 'student_id')
|
||||
->where('academic_term_id', $this->input('academic_term_id'))
|
||||
->ignore($invoice?->id),
|
||||
];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'student_ids.*.unique' => 'Salah satu mahasiswa yang dipilih sudah memiliki tagihan untuk periode akademik tersebut.',
|
||||
'student_id.unique' => 'Mahasiswa ini sudah memiliki tagihan untuk periode akademik tersebut.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Finances;
|
||||
|
||||
use App\Enums\PaymentMethod;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class TuitionPaymentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-tuition-payments' : 'update-tuition-payments');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$invoice = $this->route('tuition_invoice');
|
||||
$payment = $this->route('payment');
|
||||
|
||||
$otherPaidTotal = $invoice->payments()
|
||||
->when($payment, fn ($query) => $query->where('id', '!=', $payment->id))
|
||||
->sum('amount_paid');
|
||||
|
||||
$remaining = max(0, round($invoice->amount_due - $otherPaidTotal, 2));
|
||||
|
||||
return [
|
||||
'amount_paid' => ['required', 'numeric', 'min:0.01', "max:{$remaining}"],
|
||||
'paid_at' => ['required', 'date', 'before_or_equal:now'],
|
||||
'payment_method' => ['required', Rule::enum(PaymentMethod::class)],
|
||||
'notes' => ['nullable', 'string'],
|
||||
'proof' => ['nullable', 'file', 'max:10240', 'mimes:pdf,jpg,jpeg,png'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'amount_paid.max' => 'Jumlah dibayar tidak boleh melebihi sisa tagihan.',
|
||||
'paid_at.before_or_equal' => 'Tanggal & waktu bayar tidak boleh melebihi waktu saat ini.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Manage;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AnnouncementRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-announcements' : 'update-announcements');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'max:200'],
|
||||
'content' => ['required', 'string'],
|
||||
'target_roles' => ['required', 'array', 'min:1'],
|
||||
'target_roles.*' => [
|
||||
Rule::enum(UserRole::class)->only([UserRole::Mahasiswa, UserRole::Dosen]),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Manage\CourseClass;
|
||||
|
||||
use App\Enums\ClassMethod;
|
||||
use App\Enums\Semester;
|
||||
use App\Models\AcademicTerm;
|
||||
use App\Models\Course;
|
||||
use App\Models\Lecturer;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class CourseClassRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-course-classes' : 'update-course-classes');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$academicTermId = $this->input('academic_term_id');
|
||||
$academicTerm = $academicTermId ? AcademicTerm::find($academicTermId) : null;
|
||||
|
||||
$semesterParityRule = function ($attribute, $value, $fail) use ($academicTerm) {
|
||||
if (! $academicTerm) {
|
||||
return;
|
||||
}
|
||||
|
||||
$semesterNumber = Course::find($value)?->semester_number;
|
||||
|
||||
if ($semesterNumber === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isOddSemester = $semesterNumber % 2 === 1;
|
||||
$matches = $academicTerm->semester === Semester::odd ? $isOddSemester : ! $isOddSemester;
|
||||
|
||||
if (! $matches) {
|
||||
$fail('Mata kuliah semester '.$semesterNumber.' tidak sesuai dengan periode '.$academicTerm->semester->label().'.');
|
||||
}
|
||||
};
|
||||
|
||||
if ($this->isMethod('post')) {
|
||||
return [
|
||||
'course_ids' => ['required', 'array', 'min:1'],
|
||||
'course_ids.*' => [
|
||||
'integer',
|
||||
Rule::exists('courses', 'id'),
|
||||
Rule::unique('course_classes', 'course_id')
|
||||
->where('academic_term_id', $academicTermId)
|
||||
->whereNull('deleted_at'),
|
||||
$semesterParityRule,
|
||||
],
|
||||
'lecturer_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
function ($attribute, $value, $fail) {
|
||||
$courseIds = (array) $this->input('course_ids', []);
|
||||
$departmentIds = Course::whereIn('id', $courseIds)->pluck('department_id')->unique();
|
||||
$lecturer = Lecturer::find($value);
|
||||
$coveredDepartmentIds = $lecturer
|
||||
? $lecturer->departments()->pluck('departments.id')
|
||||
: collect();
|
||||
|
||||
if ($departmentIds->diff($coveredDepartmentIds)->isNotEmpty()) {
|
||||
$fail('Dosen tidak terdaftar di jurusan seluruh mata kuliah yang dipilih.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')],
|
||||
'method' => ['nullable', 'string', Rule::enum(ClassMethod::class)],
|
||||
];
|
||||
}
|
||||
|
||||
$departmentId = Course::find($this->input('course_id'))?->department_id;
|
||||
|
||||
return [
|
||||
'course_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('courses', 'id'),
|
||||
Rule::unique('course_classes', 'course_id')
|
||||
->where('academic_term_id', $academicTermId)
|
||||
->whereNull('deleted_at')
|
||||
->ignore($this->route('course_class')?->id),
|
||||
$semesterParityRule,
|
||||
],
|
||||
'lecturer_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('lecturer_department', 'lecturer_id')->where('department_id', $departmentId),
|
||||
],
|
||||
'academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')],
|
||||
'method' => ['nullable', 'string', Rule::enum(ClassMethod::class)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Manage\CourseClass;
|
||||
|
||||
use App\Models\AcademicTerm;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class DuplicateCourseClassesRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('create-course-classes');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'source_academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')],
|
||||
'target_academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$validator->after(function (Validator $validator) {
|
||||
if ($this->input('source_academic_term_id') === $this->input('target_academic_term_id')) {
|
||||
$validator->errors()->add('target_academic_term_id', 'Periode tujuan harus berbeda dari periode sumber.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sourceTerm = AcademicTerm::find($this->input('source_academic_term_id'));
|
||||
$targetTerm = AcademicTerm::find($this->input('target_academic_term_id'));
|
||||
|
||||
if ($sourceTerm && $targetTerm && $sourceTerm->semester !== $targetTerm->semester) {
|
||||
$validator->errors()->add('target_academic_term_id', 'Periode tujuan harus semester yang sama dengan periode sumber (Ganjil ke Ganjil, Genap ke Genap).');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Manage\CourseRegistration;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RejectCourseRegistrationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('reject-course-registrations')
|
||||
&& $this->user()->canReviewCourseRegistrationOf($this->route('submission')->student);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'reason' => ['required', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Manage\CourseRegistration;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SignAsAdvisorCourseRegistrationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->isAdvisorOf($this->route('student'));
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'signature' => ['required', 'file', 'image', 'max:2048'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Manage\CourseRegistration;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SignAsDepartmentLeaderCourseRegistrationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->isDepartmentLeaderOf($this->route('student'));
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'signature' => ['required', 'file', 'image', 'max:2048'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Manage\CourseRegistration;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateCourseRegistrationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
$student = $this->route('student');
|
||||
|
||||
return $this->user()->hasRole(UserRole::Mahasiswa->value)
|
||||
&& $this->user()->student?->id === $student?->id;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'course_class_ids' => ['array'],
|
||||
'course_class_ids.*' => ['integer'],
|
||||
'signature' => ['nullable', 'file', 'image', 'max:2048'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Master;
|
||||
|
||||
use App\Enums\Semester;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AcademicTermRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-academic-terms' : 'update-academic-terms');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'academic_year' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (! preg_match('/^(\d{4})\/(\d{4})$/', $value, $matches)) {
|
||||
$fail('Format tahun ajaran harus YYYY/YYYY, contoh: 2025/2026.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$startYear = (int) $matches[1];
|
||||
$endYear = (int) $matches[2];
|
||||
|
||||
if ($endYear !== $startYear + 1) {
|
||||
$fail('Tahun kedua harus selisih 1 tahun dari tahun pertama. Contoh: 2025/2026, 2026/2027.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'semester' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::enum(Semester::class),
|
||||
Rule::unique('academic_terms')->where(function ($query) {
|
||||
return $query->where('academic_year', $this->input('academic_year'));
|
||||
})->ignore($this->route('academic_term')),
|
||||
],
|
||||
'start_date' => [
|
||||
'required',
|
||||
'date',
|
||||
],
|
||||
'end_date' => [
|
||||
'required',
|
||||
'date',
|
||||
'after_or_equal:start_date',
|
||||
],
|
||||
'is_active' => ['boolean'],
|
||||
'open_semesters' => ['nullable', 'array'],
|
||||
'open_semesters.*' => ['integer', 'between:1,8'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Master;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class CourseRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-courses' : 'update-courses');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'code' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
Rule::unique('courses')->ignore($this->route('course')),
|
||||
],
|
||||
'name' => ['required', 'string', 'max:150'],
|
||||
'credits' => ['required', 'integer', 'min:1', 'max:10'],
|
||||
'department_id' => ['required', 'integer', Rule::exists('departments', 'id')],
|
||||
'semester_number' => ['nullable', 'integer', 'min:1', 'max:14'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Master;
|
||||
|
||||
use App\Enums\DegreeLevel;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class DepartmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-departments' : 'update-departments');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'code' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:10',
|
||||
Rule::unique('departments')->ignore($this->route('department')),
|
||||
],
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'degree_level' => ['nullable', 'string', Rule::enum(DegreeLevel::class)],
|
||||
'lecturer_id' => [
|
||||
'nullable',
|
||||
Rule::requiredIf($this->route('department') !== null),
|
||||
'integer',
|
||||
Rule::exists('lecturer_department', 'lecturer_id')
|
||||
->where('department_id', $this->route('department')?->id ?? 0),
|
||||
],
|
||||
'leadership_started_at' => ['nullable', 'date', 'required_with:lecturer_id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Services;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AcademicAdvisingLogRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-academic-advising-logs' : 'update-academic-advising-logs');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'student_ids' => ['required', 'array', 'min:1'],
|
||||
'student_ids.*' => ['integer', Rule::exists('students', 'id')],
|
||||
'lecturer_id' => ['required', 'integer', Rule::exists('lecturers', 'id')],
|
||||
'topic' => ['required', 'string', 'max:150'],
|
||||
'notes' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Services\LetterRequest;
|
||||
|
||||
use App\Enums\LetterStatus;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LetterRequestRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
if ($this->isMethod('post')) {
|
||||
return $this->user()->can('create-letter-requests');
|
||||
}
|
||||
|
||||
$letterRequest = $this->route('letter_request');
|
||||
|
||||
return $this->user()->can('update-letter-requests')
|
||||
&& $letterRequest->user_id === $this->user()->id
|
||||
&& $letterRequest->status === LetterStatus::Submitted;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'letter_type' => ['required', 'string', 'max:100'],
|
||||
'purpose' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Services\LetterRequest;
|
||||
|
||||
use App\Enums\LetterStatus;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class LetterRequestStatusRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('update-letter-requests-status');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => ['nullable', 'required_without:result', Rule::enum(LetterStatus::class)],
|
||||
'result' => ['nullable', 'file', 'max:10240', 'mimes:pdf,jpg,jpeg,png,doc,docx'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateStatusRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
$permission = match ($this->route()?->getName()) {
|
||||
'admin.master.academic-terms.update_status' => 'update-academic-terms-status',
|
||||
'admin.users.lecturers.update_user_status' => 'update-lecturers-status',
|
||||
'admin.users.students.update_user_status' => 'update-students-account-status',
|
||||
'admin.users.administrators.update_user_status' => 'update-administrators-status',
|
||||
default => null,
|
||||
};
|
||||
|
||||
return $permission !== null && $this->user()->can($permission);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => ['required', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Users;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Enums\UserRole;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AdministratorRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-administrators' : 'update-administrators');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$userId = $this->route('user')?->id;
|
||||
|
||||
return [
|
||||
'username' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
'min:5',
|
||||
'alpha_dash',
|
||||
$userId === null
|
||||
? Rule::unique('users')
|
||||
: Rule::unique('users')->ignore($userId),
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
$userId === null
|
||||
? Rule::unique('users')
|
||||
: Rule::unique('users')->ignore($userId),
|
||||
],
|
||||
'role' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::enum(UserRole::class)->only([UserRole::StaffAdmin, UserRole::StaffKeuangan]),
|
||||
],
|
||||
|
||||
// Profile
|
||||
'full_name' => ['required', 'string', 'max:150'],
|
||||
'phone_number' => ['required', 'string', 'max:20'],
|
||||
'address' => ['required', 'string'],
|
||||
'gender' => ['required', 'string', Rule::enum(Gender::class)],
|
||||
'birth_date' => ['required', 'date'],
|
||||
'birth_place' => ['required', 'string', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Users;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class LecturerRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-lecturers' : 'update-lecturers');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$userId = $this->route('user')?->id;
|
||||
|
||||
return [
|
||||
'username' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
'min:5',
|
||||
'alpha_dash',
|
||||
$userId === null
|
||||
? Rule::unique('users')
|
||||
: Rule::unique('users')->ignore($userId),
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
$userId === null
|
||||
? Rule::unique('users')
|
||||
: Rule::unique('users')->ignore($userId),
|
||||
],
|
||||
|
||||
// Profile
|
||||
'full_name' => ['required', 'string', 'max:150'],
|
||||
'phone_number' => ['required', 'string', 'max:20'],
|
||||
'address' => ['required', 'string'],
|
||||
'gender' => ['required', 'string', Rule::enum(Gender::class)],
|
||||
'birth_date' => ['required', 'date'],
|
||||
'birth_place' => ['required', 'string', 'max:100'],
|
||||
|
||||
// Lecturer fields
|
||||
'lecturer_number' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
Rule::unique('lecturers')->ignore($userId, 'user_id'),
|
||||
],
|
||||
'department_ids' => ['required', 'array', 'min:1'],
|
||||
'department_ids.*' => ['integer', Rule::exists('departments', 'id')],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,86 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Users\Student;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Enums\StudentStatus;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StudentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-students' : 'update-students');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$userId = $this->route('user')?->id;
|
||||
|
||||
return [
|
||||
'username' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
'min:5',
|
||||
'alpha_dash',
|
||||
$userId === null
|
||||
? Rule::unique('users')
|
||||
: Rule::unique('users')->ignore($userId),
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
$userId === null
|
||||
? Rule::unique('users')
|
||||
: Rule::unique('users')->ignore($userId),
|
||||
],
|
||||
|
||||
// Profile
|
||||
'full_name' => ['required', 'string', 'max:150'],
|
||||
'phone_number' => ['required', 'string', 'max:20'],
|
||||
'address' => ['required', 'string'],
|
||||
'gender' => ['required', 'string', Rule::enum(Gender::class)],
|
||||
'birth_date' => ['required', 'date'],
|
||||
'birth_place' => ['required', 'string', 'max:100'],
|
||||
|
||||
// Student fields
|
||||
'student_number' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:10',
|
||||
Rule::unique('students')->ignore($userId, 'user_id'),
|
||||
],
|
||||
'department_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('departments', 'id'),
|
||||
],
|
||||
'enrollment_year' => [
|
||||
'required',
|
||||
'integer',
|
||||
'digits:4',
|
||||
],
|
||||
'current_semester' => [
|
||||
'required',
|
||||
'integer',
|
||||
'min:1',
|
||||
'max:14',
|
||||
],
|
||||
'academic_advisor_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('lecturer_department', 'lecturer_id')
|
||||
->where('department_id', $this->input('department_id')),
|
||||
],
|
||||
'status' => [
|
||||
'nullable',
|
||||
'string',
|
||||
Rule::enum(StudentStatus::class),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Users\Student;
|
||||
|
||||
use App\Enums\StudentStatus;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StudentStatusRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('update-students-academic-status');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => ['required', 'string', Rule::enum(StudentStatus::class)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class MediaUploadRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'file' => ['required', 'image', 'max:10240', 'mimes:jpg,jpeg,png,gif,webp'],
|
||||
'collection' => ['nullable', 'string', 'max:50'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\ClassMethod;
|
||||
use App\Enums\Gender;
|
||||
use App\Enums\PaymentMethod;
|
||||
use App\Enums\Semester;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PaginatedRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'per_page' => ['nullable', 'integer', 'in:25,50,100,999999'],
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
'highlight' => ['nullable', 'integer'],
|
||||
'gender' => ['nullable', 'string', Rule::enum(Gender::class)],
|
||||
'department_id' => ['nullable', 'integer'],
|
||||
'status' => ['nullable', 'string'],
|
||||
'enrollment_year' => ['nullable', 'integer'],
|
||||
'semester_number' => ['nullable', 'integer'],
|
||||
'semester' => ['nullable', 'string', Rule::enum(Semester::class)],
|
||||
'is_active' => ['nullable', Rule::in(['true', 'false'])],
|
||||
'academic_term_id' => ['nullable', 'integer'],
|
||||
'method' => ['nullable', 'string', Rule::enum(ClassMethod::class)],
|
||||
'course_class_id' => ['nullable', 'integer'],
|
||||
'lecturer_id' => ['nullable', 'integer'],
|
||||
'type' => ['nullable', 'string'],
|
||||
'payment_method' => ['nullable', 'string', Rule::enum(PaymentMethod::class)],
|
||||
'file' => ['nullable', 'string'],
|
||||
'level' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
public function validatedWithDefaults(): array
|
||||
{
|
||||
$validated = $this->validated();
|
||||
|
||||
return [
|
||||
'perPage' => $validated['per_page'] ?? 25,
|
||||
'search' => $validated['search'] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Settings;
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use App\Concerns\PasswordValidationRules;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Settings;
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use App\Concerns\PasswordValidationRules;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Settings;
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use App\Concerns\ProfileValidationRules;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Settings;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Laravel\Fortify\InteractsWithTwoFactorState;
|
||||
|
||||
class TwoFactorAuthenticationRequest extends FormRequest
|
||||
{
|
||||
use InteractsWithTwoFactorState;
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
class AcademicAdvisingLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public function lecturer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Lecturer::class);
|
||||
}
|
||||
|
||||
public function students(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Student::class);
|
||||
}
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\Semester;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
#[Appends(['formatted_start_date', 'formatted_end_date'])]
|
||||
class AcademicTerm extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public function courseClasses(): HasMany
|
||||
{
|
||||
return $this->hasMany(CourseClass::class);
|
||||
}
|
||||
|
||||
public function courseRegistrations(): HasMany
|
||||
{
|
||||
return $this->hasMany(CourseRegistration::class);
|
||||
}
|
||||
|
||||
public function courseRegistrationSubmissions(): HasMany
|
||||
{
|
||||
return $this->hasMany(CourseRegistrationSubmission::class);
|
||||
}
|
||||
|
||||
public function tuitionInvoices(): HasMany
|
||||
{
|
||||
return $this->hasMany(TuitionInvoice::class);
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'semester' => Semester::class,
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'is_active' => 'boolean',
|
||||
'open_semesters' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
protected function formattedStartDate(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->start_date->format('d M Y'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedEndDate(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->end_date->format('d M Y'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
class Announcement extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'target_roles' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\AssignmentStatus;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
#[Appends(['attachment_url', 'attachment_name'])]
|
||||
class Assignment extends Model implements HasMedia
|
||||
{
|
||||
use HasFactory, InteractsWithMedia, SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'deadline' => 'datetime',
|
||||
'status' => AssignmentStatus::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function registerMediaCollections(): void
|
||||
{
|
||||
$this->addMediaCollection('assignment_attachment')->singleFile();
|
||||
}
|
||||
|
||||
public function courseClass(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CourseClass::class);
|
||||
}
|
||||
|
||||
public function submissions(): HasMany
|
||||
{
|
||||
return $this->hasMany(Submission::class);
|
||||
}
|
||||
|
||||
protected function attachmentUrl(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->getFirstMediaUrl('assignment_attachment') ?: null,
|
||||
);
|
||||
}
|
||||
|
||||
protected function attachmentName(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->getFirstMedia('assignment_attachment')?->file_name,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user