Compare commits
5 Commits
c1ba03d314
...
93c5514c22
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93c5514c22 | ||
|
|
7353b46975 | ||
|
|
56c41db373 | ||
|
|
4350f7d9ea | ||
|
|
6834d47e2b |
@ -2,15 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Services;
|
namespace App\Http\Controllers\Admin\Services;
|
||||||
|
|
||||||
|
use App\Enums\LetterStatus;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Services\LetterRequestRequest;
|
use App\Http\Requests\Admin\Services\LetterRequest\LetterRequestRequest;
|
||||||
|
use App\Http\Requests\Admin\Services\LetterRequest\LetterRequestStatusRequest;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\LetterRequest;
|
use App\Models\LetterRequest;
|
||||||
use App\Models\Student;
|
|
||||||
use App\Services\Admin\Services\LetterRequestService;
|
use App\Services\Admin\Services\LetterRequestService;
|
||||||
use App\Services\Admin\Users\StudentService;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
|
|
||||||
@ -18,7 +17,6 @@ class LetterRequestController extends Controller
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly LetterRequestService $service,
|
private readonly LetterRequestService $service,
|
||||||
private readonly StudentService $studentService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
@ -28,60 +26,25 @@ public function index(PaginatedRequest $request): Response
|
|||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
status: $request->validated('status'),
|
status: $request->validated('status'),
|
||||||
),
|
),
|
||||||
'students' => $this->studentService->getAllForSelect(),
|
|
||||||
'filters' => $request->only(['status']),
|
'filters' => $request->only(['status']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mine(Request $request): Response
|
|
||||||
{
|
|
||||||
return Inertia::render('student/letter-requests/index', [
|
|
||||||
'letterRequests' => $this->service->mine($this->currentStudent($request)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(): Response
|
|
||||||
{
|
|
||||||
return Inertia::render('student/letter-requests/create');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function show(Request $request, LetterRequest $letterRequest): Response
|
|
||||||
{
|
|
||||||
$student = $this->currentStudent($request);
|
|
||||||
|
|
||||||
abort_if($letterRequest->student_id !== $student->id, 403);
|
|
||||||
|
|
||||||
return Inertia::render('student/letter-requests/show', [
|
|
||||||
'letterRequest' => $this->service->withDetails($letterRequest),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(LetterRequestRequest $request): RedirectResponse
|
public function store(LetterRequestRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
if ($request->routeIs('student.*')) {
|
$this->service->create([
|
||||||
$student = $this->currentStudent($request);
|
'user_id' => auth()->id(),
|
||||||
|
...$request->validated(),
|
||||||
$letterRequest = $this->service->create([
|
]);
|
||||||
'student_id' => $student->id,
|
|
||||||
'letter_type' => $request->validated('letter_type'),
|
|
||||||
'purpose' => $request->validated('purpose'),
|
|
||||||
], null);
|
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil diajukan.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil diajukan.']);
|
||||||
|
|
||||||
return to_route('student.letter-requests.show', $letterRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->service->create($request->validated(), $request->file('result'));
|
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil ditambahkan.']);
|
|
||||||
|
|
||||||
return to_route('admin.services.letter-requests.index');
|
return to_route('admin.services.letter-requests.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(LetterRequestRequest $request, LetterRequest $letterRequest): RedirectResponse
|
public function update(LetterRequestRequest $request, LetterRequest $letterRequest): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->service->update($letterRequest, $request->validated(), $request->file('result'));
|
$this->service->update($letterRequest, $request->validated());
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil diperbarui.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil diperbarui.']);
|
||||||
|
|
||||||
@ -90,17 +53,17 @@ public function update(LetterRequestRequest $request, LetterRequest $letterReque
|
|||||||
|
|
||||||
public function destroy(LetterRequest $letterRequest): RedirectResponse
|
public function destroy(LetterRequest $letterRequest): RedirectResponse
|
||||||
{
|
{
|
||||||
|
abort_if($letterRequest->status !== LetterStatus::Submitted->value, 403);
|
||||||
|
|
||||||
$this->service->delete($letterRequest);
|
$this->service->delete($letterRequest);
|
||||||
|
|
||||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil dihapus.'])->back();
|
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil dihapus.'])->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function currentStudent(Request $request): Student
|
public function updateStatus(LetterRequestStatusRequest $request, LetterRequest $letterRequest): RedirectResponse
|
||||||
{
|
{
|
||||||
$student = $request->user()->student;
|
$this->service->updateStatus($letterRequest, $request->validated('status'), $request->file('result'));
|
||||||
|
|
||||||
abort_if(! $student, 403);
|
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status surat permohonan berhasil diperbarui.'])->back();
|
||||||
|
|
||||||
return $student;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
<?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->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'letter_type' => ['required', 'string', 'max:100'],
|
||||||
|
'purpose' => ['nullable', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
<?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,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests\Admin\Services;
|
|
||||||
|
|
||||||
use App\Enums\LetterStatus;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class LetterRequestRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
if ($this->routeIs('student.*')) {
|
|
||||||
return (bool) $this->user()->student;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->user()->can($this->isMethod('post') ? 'create-letter-requests' : 'update-letter-requests');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
if ($this->routeIs('student.*')) {
|
|
||||||
return [
|
|
||||||
'letter_type' => ['required', 'string', 'max:100'],
|
|
||||||
'purpose' => ['nullable', 'string'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'student_id' => ['required', 'integer', Rule::exists('students', 'id')],
|
|
||||||
'letter_type' => ['required', 'string', 'max:100'],
|
|
||||||
'purpose' => ['nullable', 'string'],
|
|
||||||
'status' => ['required', Rule::enum(LetterStatus::class)],
|
|
||||||
'result' => ['nullable', 'file', 'max:10240', 'mimes:pdf,jpg,jpeg,png,doc,docx'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -32,9 +32,9 @@ public function registerMediaCollections(): void
|
|||||||
$this->addMediaCollection('letter_result')->singleFile();
|
$this->addMediaCollection('letter_result')->singleFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function student(): BelongsTo
|
public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Student::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function processor(): BelongsTo
|
public function processor(): BelongsTo
|
||||||
|
|||||||
@ -26,12 +26,16 @@ public function getSemesterNumbers(): array
|
|||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', ?int $departmentId = null, ?int $semesterNumber = null): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', ?int $departmentId = null, ?int $semesterNumber = null): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
return Course::query()
|
return Course::query()
|
||||||
->select(['id', 'code', 'name', 'credits', 'department_id', 'semester_number'])
|
->select(['id', 'code', 'name', 'credits', 'department_id', 'semester_number'])
|
||||||
->with('department:id,name')
|
->with('department:id,name')
|
||||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('code', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('code', 'like', "%{$search}%"))
|
||||||
->when($departmentId, fn ($q) => $q->where('department_id', $departmentId))
|
->when($departmentId, fn ($q) => $q->where('department_id', $departmentId))
|
||||||
->when($semesterNumber, fn ($q) => $q->where('semester_number', $semesterNumber))
|
->when($semesterNumber, fn ($q) => $q->where('semester_number', $semesterNumber))
|
||||||
|
->when($user->hasRole('mahasiswa'), fn ($q) => $q->where('department_id', $user->student?->department_id))
|
||||||
|
->when($user->hasRole('dosen'), fn ($q) => $q->whereIn('department_id', $user->lecturer?->departments()->pluck('departments.id') ?? []))
|
||||||
->latest()
|
->latest()
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,71 +4,56 @@
|
|||||||
|
|
||||||
use App\Enums\LetterStatus;
|
use App\Enums\LetterStatus;
|
||||||
use App\Models\LetterRequest;
|
use App\Models\LetterRequest;
|
||||||
use App\Models\Student;
|
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
|
|
||||||
class LetterRequestService
|
class LetterRequestService
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @return Collection<int, LetterRequest>
|
|
||||||
*/
|
|
||||||
public function mine(Student $student): Collection
|
|
||||||
{
|
|
||||||
return LetterRequest::query()
|
|
||||||
->where('student_id', $student->id)
|
|
||||||
->with('processor.profile')
|
|
||||||
->latest()
|
|
||||||
->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function withDetails(LetterRequest $letterRequest): LetterRequest
|
|
||||||
{
|
|
||||||
return $letterRequest->load('processor.profile');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', ?string $status = null): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', ?string $status = null): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
return LetterRequest::query()
|
return LetterRequest::query()
|
||||||
->select(['id', 'student_id', 'letter_type', 'purpose', 'status', 'processed_by', 'submitted_at', 'completed_at'])
|
->select(['id', 'user_id', 'letter_type', 'purpose', 'status', 'processed_by', 'submitted_at', 'completed_at'])
|
||||||
->with(['student.user.profile', 'student.department', 'processor.profile'])
|
->with(['user.profile', 'user.student.department', 'processor.profile'])
|
||||||
->when($search, fn ($q) => $q->where('letter_type', 'like', "%{$search}%")
|
->when($search, fn ($q) => $q->where('letter_type', 'like', "%{$search}%")
|
||||||
->orWhereHas('student', fn ($q) => $q->where('student_number', 'like', "%{$search}%")
|
->orWhereHas('user', fn ($q) => $q->whereHas('profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%"))
|
||||||
->orWhereHas('user.profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%"))))
|
->orWhereHas('student', fn ($q) => $q->where('student_number', 'like', "%{$search}%"))))
|
||||||
->when($status, fn ($q) => $q->where('status', $status))
|
->when($status, fn ($q) => $q->where('status', $status))
|
||||||
|
->when($user->hasRole('mahasiswa'), fn ($q) => $q->where('user_id', $user->id))
|
||||||
->latest()
|
->latest()
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data, ?UploadedFile $result): LetterRequest
|
public function create(array $data): LetterRequest
|
||||||
{
|
{
|
||||||
$status = $data['status'] ?? LetterStatus::Submitted->value;
|
return LetterRequest::create([
|
||||||
|
'user_id' => $data['user_id'],
|
||||||
$letterRequest = LetterRequest::create([
|
|
||||||
'student_id' => $data['student_id'],
|
|
||||||
'letter_type' => $data['letter_type'],
|
'letter_type' => $data['letter_type'],
|
||||||
'purpose' => $data['purpose'] ?? null,
|
'purpose' => $data['purpose'] ?? null,
|
||||||
'status' => $status,
|
'status' => LetterStatus::Submitted->value,
|
||||||
'processed_by' => $status !== LetterStatus::Submitted->value ? auth()->id() : null,
|
|
||||||
'submitted_at' => now(),
|
'submitted_at' => now(),
|
||||||
'completed_at' => $status === LetterStatus::Completed->value ? now() : null,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($result) {
|
|
||||||
$letterRequest->addMedia($result)->toMediaCollection('letter_result');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function update(LetterRequest $letterRequest, array $data): LetterRequest
|
||||||
|
{
|
||||||
|
$letterRequest->letter_type = $data['letter_type'];
|
||||||
|
$letterRequest->purpose = $data['purpose'] ?? null;
|
||||||
|
$letterRequest->update();
|
||||||
|
|
||||||
return $letterRequest;
|
return $letterRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(LetterRequest $letterRequest, array $data, ?UploadedFile $result): LetterRequest
|
public function delete(LetterRequest $letterRequest): bool
|
||||||
{
|
{
|
||||||
$status = $data['status'] ?? LetterStatus::Submitted->value;
|
return $letterRequest->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateStatus(LetterRequest $letterRequest, ?string $status, ?UploadedFile $result): LetterRequest
|
||||||
|
{
|
||||||
|
$status = $result ? LetterStatus::Completed->value : $status;
|
||||||
|
|
||||||
$letterRequest->student_id = $data['student_id'];
|
|
||||||
$letterRequest->letter_type = $data['letter_type'];
|
|
||||||
$letterRequest->purpose = $data['purpose'] ?? null;
|
|
||||||
$letterRequest->status = $status;
|
$letterRequest->status = $status;
|
||||||
$letterRequest->processed_by = $status !== LetterStatus::Submitted->value ? auth()->id() : null;
|
$letterRequest->processed_by = $status !== LetterStatus::Submitted->value ? auth()->id() : null;
|
||||||
$letterRequest->completed_at = $status === LetterStatus::Completed->value
|
$letterRequest->completed_at = $status === LetterStatus::Completed->value
|
||||||
@ -82,9 +67,4 @@ public function update(LetterRequest $letterRequest, array $data, ?UploadedFile
|
|||||||
|
|
||||||
return $letterRequest;
|
return $letterRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(LetterRequest $letterRequest): bool
|
|
||||||
{
|
|
||||||
return $letterRequest->delete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ class PermissionCatalog
|
|||||||
];
|
];
|
||||||
|
|
||||||
public const SERVICES = [
|
public const SERVICES = [
|
||||||
'view-letter-requests', 'create-letter-requests', 'update-letter-requests', 'delete-letter-requests',
|
'view-letter-requests', 'create-letter-requests', 'update-letter-requests', 'update-letter-requests-status', 'delete-letter-requests',
|
||||||
'view-academic-advising-logs', 'create-academic-advising-logs', 'update-academic-advising-logs', 'delete-academic-advising-logs',
|
'view-academic-advising-logs', 'create-academic-advising-logs', 'update-academic-advising-logs', 'delete-academic-advising-logs',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ public function up(): void
|
|||||||
{
|
{
|
||||||
Schema::create('letter_requests', function (Blueprint $table) {
|
Schema::create('letter_requests', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('student_id')->constrained()->cascadeOnDelete();
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||||
$table->string('letter_type', 100);
|
$table->string('letter_type', 100);
|
||||||
$table->text('purpose')->nullable();
|
$table->text('purpose')->nullable();
|
||||||
$table->enum('status', LetterStatus::values())->nullable()->default(LetterStatus::Submitted->value);
|
$table->enum('status', LetterStatus::values())->nullable()->default(LetterStatus::Submitted->value);
|
||||||
|
|||||||
@ -11,7 +11,7 @@ public function run(): void
|
|||||||
{
|
{
|
||||||
LetterRequest::insert([
|
LetterRequest::insert([
|
||||||
[
|
[
|
||||||
'student_id' => 1,
|
'user_id' => 1,
|
||||||
'letter_type' => 'Surat Aktif Kuliah',
|
'letter_type' => 'Surat Aktif Kuliah',
|
||||||
'purpose' => 'Persyaratan pengajuan beasiswa',
|
'purpose' => 'Persyaratan pengajuan beasiswa',
|
||||||
'status' => 'completed',
|
'status' => 'completed',
|
||||||
@ -22,7 +22,7 @@ public function run(): void
|
|||||||
'updated_at' => '2026-07-02 14:00:00',
|
'updated_at' => '2026-07-02 14:00:00',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'student_id' => 2,
|
'user_id' => 2,
|
||||||
'letter_type' => 'Surat Keterangan Lulus Sementara',
|
'letter_type' => 'Surat Keterangan Lulus Sementara',
|
||||||
'purpose' => 'Persyaratan melamar magang',
|
'purpose' => 'Persyaratan melamar magang',
|
||||||
'status' => 'in_process',
|
'status' => 'in_process',
|
||||||
|
|||||||
@ -22,7 +22,10 @@ public function run(): void
|
|||||||
$users = PermissionCatalog::USERS;
|
$users = PermissionCatalog::USERS;
|
||||||
|
|
||||||
$feedbackSelfService = [
|
$feedbackSelfService = [
|
||||||
'view-feedback', 'create-feedback', 'update-feedback', 'delete-feedback',
|
'view-feedback',
|
||||||
|
'create-feedback',
|
||||||
|
'update-feedback',
|
||||||
|
'delete-feedback',
|
||||||
];
|
];
|
||||||
|
|
||||||
$permissionNames = PermissionCatalog::all();
|
$permissionNames = PermissionCatalog::all();
|
||||||
@ -34,14 +37,27 @@ public function run(): void
|
|||||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||||
|
|
||||||
$roles = [
|
$roles = [
|
||||||
'mahasiswa' => ['view-dashboard', ...$feedbackSelfService],
|
'mahasiswa' => [
|
||||||
'dosen' => ['view-dashboard', ...$feedbackSelfService],
|
'view-dashboard',
|
||||||
|
'view-academic-terms',
|
||||||
|
'view-courses',
|
||||||
|
'view-letter-requests',
|
||||||
|
'create-letter-requests',
|
||||||
|
'update-letter-requests',
|
||||||
|
'delete-letter-requests',
|
||||||
|
...$feedbackSelfService,
|
||||||
|
],
|
||||||
|
'dosen' => [
|
||||||
|
'view-dashboard',
|
||||||
|
'view-courses',
|
||||||
|
...$feedbackSelfService,
|
||||||
|
],
|
||||||
'staff-admin' => [
|
'staff-admin' => [
|
||||||
'view-dashboard',
|
'view-dashboard',
|
||||||
...$master,
|
...$master,
|
||||||
...$academicClasses,
|
...$academicClasses,
|
||||||
...$manage,
|
...$manage,
|
||||||
...$services,
|
...array_diff($services, ['create-letter-requests', 'update-letter-requests']),
|
||||||
...$users,
|
...$users,
|
||||||
...$feedbackSelfService,
|
...$feedbackSelfService,
|
||||||
'update-feedback-status',
|
'update-feedback-status',
|
||||||
|
|||||||
@ -56,7 +56,6 @@ import { index as lecturersRoute } from '@/routes/admin/users/lecturers';
|
|||||||
import { index as studentsRoute } from '@/routes/admin/users/students';
|
import { index as studentsRoute } from '@/routes/admin/users/students';
|
||||||
import { index as lecturerAcademicAdvisingLogsRoute } from '@/routes/lecturer/academic-advising-logs';
|
import { index as lecturerAcademicAdvisingLogsRoute } from '@/routes/lecturer/academic-advising-logs';
|
||||||
import { index as studentCourseRegistrationsRoute } from '@/routes/student/course-registrations';
|
import { index as studentCourseRegistrationsRoute } from '@/routes/student/course-registrations';
|
||||||
import { index as studentLetterRequestsRoute } from '@/routes/student/letter-requests';
|
|
||||||
import type { Auth } from '@/types/auth';
|
import type { Auth } from '@/types/auth';
|
||||||
|
|
||||||
const STAFF_ROLES = ['developer', 'staff-admin', 'staff-keuangan', 'kaprodi'];
|
const STAFF_ROLES = ['developer', 'staff-admin', 'staff-keuangan', 'kaprodi'];
|
||||||
@ -64,63 +63,65 @@ const STAFF_ROLES = ['developer', 'staff-admin', 'staff-keuangan', 'kaprodi'];
|
|||||||
function buildNavMain({
|
function buildNavMain({
|
||||||
isMahasiswa,
|
isMahasiswa,
|
||||||
isDosen,
|
isDosen,
|
||||||
canViewLogs,
|
permissions,
|
||||||
canViewRoles,
|
|
||||||
}: {
|
}: {
|
||||||
isMahasiswa: boolean;
|
isMahasiswa: boolean;
|
||||||
isDosen: boolean;
|
isDosen: boolean;
|
||||||
canViewLogs: boolean;
|
permissions: string[];
|
||||||
canViewRoles: boolean;
|
|
||||||
}): (NavGroup | NavItem)[] {
|
}): (NavGroup | NavItem)[] {
|
||||||
return [
|
const can = (permission: string) => permissions.includes(permission);
|
||||||
{
|
|
||||||
name: 'Dasbor',
|
const penggunaItems: NavItem[] = [
|
||||||
url: '#',
|
...(can('view-administrators')
|
||||||
icon: IconGridDots,
|
? [
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Pengguna',
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
name: 'Administrator',
|
name: 'Administrator',
|
||||||
url: administratorsRoute.url(),
|
url: administratorsRoute.url(),
|
||||||
icon: User,
|
icon: User,
|
||||||
},
|
},
|
||||||
{
|
]
|
||||||
name: 'Dosen',
|
: []),
|
||||||
url: lecturersRoute.url(),
|
...(can('view-lecturers')
|
||||||
icon: Users,
|
? [{ name: 'Dosen', url: lecturersRoute.url(), icon: Users }]
|
||||||
},
|
: []),
|
||||||
|
...(can('view-students')
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'Mahasiswa',
|
name: 'Mahasiswa',
|
||||||
url: studentsRoute.url(),
|
url: studentsRoute.url(),
|
||||||
icon: GraduationCap,
|
icon: GraduationCap,
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
},
|
: []),
|
||||||
{
|
];
|
||||||
label: 'Master',
|
|
||||||
items: [
|
const masterItems: NavItem[] = [
|
||||||
|
...(can('view-academic-terms')
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'Periode Akademik',
|
name: 'Periode Akademik',
|
||||||
url: academicTerm.url(),
|
url: academicTerm.url(),
|
||||||
icon: Calendar,
|
icon: Calendar,
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(can('view-departments')
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'Jurusan',
|
name: 'Jurusan',
|
||||||
url: departmentsRoute.url(),
|
url: departmentsRoute.url(),
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
},
|
},
|
||||||
{
|
]
|
||||||
name: 'Mata Kuliah',
|
: []),
|
||||||
url: coursesRoute.url(),
|
...(can('view-courses')
|
||||||
icon: BookOpen,
|
? [{ name: 'Mata Kuliah', url: coursesRoute.url(), icon: BookOpen }]
|
||||||
},
|
: []),
|
||||||
],
|
];
|
||||||
},
|
|
||||||
{
|
const kelolaItems: NavItem[] = [
|
||||||
label: 'Kelola',
|
...(isMahasiswa || can('view-course-registrations')
|
||||||
items: [
|
? [
|
||||||
{
|
{
|
||||||
name: 'Registrasi KRS',
|
name: 'Registrasi KRS',
|
||||||
url: isMahasiswa
|
url: isMahasiswa
|
||||||
@ -128,63 +129,85 @@ function buildNavMain({
|
|||||||
: courseRegistrationsRoute.url(),
|
: courseRegistrationsRoute.url(),
|
||||||
icon: FileCheck2,
|
icon: FileCheck2,
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(can('view-course-classes')
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'Kelas Mata Kuliah',
|
name: 'Kelas Mata Kuliah',
|
||||||
url: courseClassesRoute.url(),
|
url: courseClassesRoute.url(),
|
||||||
icon: School,
|
icon: School,
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(can('view-announcements')
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'Pengumuman',
|
name: 'Pengumuman',
|
||||||
url: announcementsRoute.url(),
|
url: announcementsRoute.url(),
|
||||||
icon: Megaphone,
|
icon: Megaphone,
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
},
|
: []),
|
||||||
{
|
];
|
||||||
label: 'Kelas',
|
|
||||||
items: [
|
const kelasItems: NavItem[] = [
|
||||||
{
|
...(can('view-materials')
|
||||||
name: 'Materi',
|
? [{ name: 'Materi', url: materialsRoute.url(), icon: FileText }]
|
||||||
url: materialsRoute.url(),
|
: []),
|
||||||
icon: FileText,
|
...(can('view-assignments')
|
||||||
},
|
? [
|
||||||
{
|
{
|
||||||
name: 'Tugas',
|
name: 'Tugas',
|
||||||
url: assignmentsRoute.url(),
|
url: assignmentsRoute.url(),
|
||||||
icon: ClipboardList,
|
icon: ClipboardList,
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(can('view-schedules')
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'Jadwal',
|
name: 'Jadwal',
|
||||||
url: schedulesRoute.url(),
|
url: schedulesRoute.url(),
|
||||||
icon: CalendarClock,
|
icon: CalendarClock,
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(can('view-attendances')
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'Kehadiran',
|
name: 'Kehadiran',
|
||||||
url: attendancesRoute.url(),
|
url: attendancesRoute.url(),
|
||||||
icon: ClipboardCheck,
|
icon: ClipboardCheck,
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
},
|
: []),
|
||||||
{
|
];
|
||||||
label: 'Keuangan',
|
|
||||||
items: [
|
const keuanganItems: NavItem[] = [
|
||||||
|
...(can('view-tuition-invoices')
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'Tagihan',
|
name: 'Tagihan',
|
||||||
url: tuitionInvoicesRoute.url(),
|
url: tuitionInvoicesRoute.url(),
|
||||||
icon: Receipt,
|
icon: Receipt,
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
},
|
: []),
|
||||||
{
|
];
|
||||||
label: 'Layanan',
|
|
||||||
items: [
|
const layananItems: NavItem[] = [
|
||||||
|
...(can('view-letter-requests')
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'Surat Permohonan',
|
name: 'Surat Permohonan',
|
||||||
url: isMahasiswa
|
url: letterRequestsRoute.url(),
|
||||||
? studentLetterRequestsRoute.url()
|
|
||||||
: letterRequestsRoute.url(),
|
|
||||||
icon: Mail,
|
icon: Mail,
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(isDosen || can('view-academic-advising-logs')
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'Bimbingan Akademik',
|
name: 'Bimbingan Akademik',
|
||||||
url: isDosen
|
url: isDosen
|
||||||
@ -192,23 +215,15 @@ function buildNavMain({
|
|||||||
: academicAdvisingLogsRoute.url(),
|
: academicAdvisingLogsRoute.url(),
|
||||||
icon: MessageCircle,
|
icon: MessageCircle,
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
...(canViewLogs || canViewRoles
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: 'Pengembang',
|
|
||||||
items: [
|
|
||||||
...(canViewLogs
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
name: 'Logs',
|
|
||||||
url: logsRoute.url(),
|
|
||||||
icon: ScrollText,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(canViewRoles
|
];
|
||||||
|
|
||||||
|
const pengembangItems: NavItem[] = [
|
||||||
|
...(can('view-logs')
|
||||||
|
? [{ name: 'Logs', url: logsRoute.url(), icon: ScrollText }]
|
||||||
|
: []),
|
||||||
|
...(can('view-roles')
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: 'Role & Permission',
|
name: 'Role & Permission',
|
||||||
@ -217,11 +232,45 @@ function buildNavMain({
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const groups: (NavGroup | NavItem)[] = [
|
||||||
|
{
|
||||||
|
name: 'Dasbor',
|
||||||
|
url: '#',
|
||||||
|
icon: IconGridDots,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (penggunaItems.length > 0) {
|
||||||
|
groups.push({ label: 'Pengguna', items: penggunaItems });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (masterItems.length > 0) {
|
||||||
|
groups.push({ label: 'Master', items: masterItems });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kelolaItems.length > 0) {
|
||||||
|
groups.push({ label: 'Kelola', items: kelolaItems });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kelasItems.length > 0) {
|
||||||
|
groups.push({ label: 'Kelas', items: kelasItems });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keuanganItems.length > 0) {
|
||||||
|
groups.push({ label: 'Keuangan', items: keuanganItems });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layananItems.length > 0) {
|
||||||
|
groups.push({ label: 'Layanan', items: layananItems });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pengembangItems.length > 0) {
|
||||||
|
groups.push({ label: 'Pengembang', items: pengembangItems });
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildNavSecondary(
|
function buildNavSecondary(
|
||||||
@ -249,13 +298,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
const isStaff = roleNames.some((role) => STAFF_ROLES.includes(role));
|
const isStaff = roleNames.some((role) => STAFF_ROLES.includes(role));
|
||||||
const isMahasiswa = !isStaff && roleNames.includes('mahasiswa');
|
const isMahasiswa = !isStaff && roleNames.includes('mahasiswa');
|
||||||
const isDosen = !isStaff && roleNames.includes('dosen');
|
const isDosen = !isStaff && roleNames.includes('dosen');
|
||||||
const canViewLogs = (auth?.permissions ?? []).includes('view-logs');
|
const permissions = auth?.permissions ?? [];
|
||||||
const canViewRoles = (auth?.permissions ?? []).includes('view-roles');
|
|
||||||
const navMain = buildNavMain({
|
const navMain = buildNavMain({
|
||||||
isMahasiswa,
|
isMahasiswa,
|
||||||
isDosen,
|
isDosen,
|
||||||
canViewLogs,
|
permissions,
|
||||||
canViewRoles,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
import { SidebarTrigger } from "@/components/ui/sidebar"
|
|
||||||
|
|
||||||
export function SiteHeader() {
|
|
||||||
return (
|
|
||||||
<header className="sticky top-0 z-50 flex h-(--header-height) shrink-0 items-center gap-2 border-b bg-white transition-[width,height] ease-linear dark:bg-[#0a0a0a] group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
|
||||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
|
||||||
<SidebarTrigger className="-ml-1" />
|
|
||||||
<Separator
|
|
||||||
orientation="vertical"
|
|
||||||
className="mx-2 data-[orientation=vertical]:h-4"
|
|
||||||
/>
|
|
||||||
<h1 className="text-base font-medium">Documents</h1>
|
|
||||||
<div className="ml-auto flex items-center gap-2">
|
|
||||||
<Button variant="ghost" asChild size="sm" className="hidden sm:flex">
|
|
||||||
<a
|
|
||||||
href="https://github.com/shadcn-ui/ui/tree/main/apps/v4/app/(examples)/dashboard"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
className="dark:text-foreground"
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -26,7 +26,7 @@ export function createAssignmentColumns(
|
|||||||
canViewSubmissions,
|
canViewSubmissions,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<Assignment>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'title',
|
accessorKey: 'title',
|
||||||
header: () => <span>Judul</span>,
|
header: () => <span>Judul</span>,
|
||||||
@ -96,7 +96,10 @@ export function createAssignmentColumns(
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canViewSubmissions || canUpdate || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -133,6 +136,8 @@ export function createAssignmentColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -129,7 +129,10 @@ export default function SubmissionIndex({
|
|||||||
<div className="text-center">{row.original.score ?? '-'}</div>
|
<div className="text-center">{row.original.score ?? '-'}</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -156,8 +159,8 @@ export default function SubmissionIndex({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export function createMaterialColumns(
|
|||||||
): ColumnDef<Material>[] {
|
): ColumnDef<Material>[] {
|
||||||
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<Material>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'meeting_number',
|
accessorKey: 'meeting_number',
|
||||||
header: () => <span className="block text-center">Pertemuan</span>,
|
header: () => <span className="block text-center">Pertemuan</span>,
|
||||||
@ -86,7 +86,10 @@ export function createMaterialColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -117,6 +120,8 @@ export function createMaterialColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export function createTuitionInvoiceColumns(
|
|||||||
canViewPayments,
|
canViewPayments,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<TuitionInvoice>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'student.student_number',
|
accessorKey: 'student.student_number',
|
||||||
header: () => <span>Mahasiswa</span>,
|
header: () => <span>Mahasiswa</span>,
|
||||||
@ -126,7 +126,10 @@ export function createTuitionInvoiceColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canViewPayments || canUpdate || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -163,6 +166,8 @@ export function createTuitionInvoiceColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -148,7 +148,10 @@ export default function TuitionPaymentIndex({ invoice, payments }: Props) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -175,8 +178,8 @@ export default function TuitionPaymentIndex({ invoice, payments }: Props) {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export function createAnnouncementColumns(
|
|||||||
): ColumnDef<Announcement>[] {
|
): ColumnDef<Announcement>[] {
|
||||||
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<Announcement>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'title',
|
accessorKey: 'title',
|
||||||
header: () => <span>Judul</span>,
|
header: () => <span>Judul</span>,
|
||||||
@ -71,7 +71,10 @@ export function createAnnouncementColumns(
|
|||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
format(new Date(row.original.created_at), 'd MMM yyyy, HH:mm'),
|
format(new Date(row.original.created_at), 'd MMM yyyy, HH:mm'),
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -98,6 +101,8 @@ export function createAnnouncementColumns(
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export function createCourseClassColumns(
|
|||||||
canViewEnrollments,
|
canViewEnrollments,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<CourseClass>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'course.name',
|
accessorKey: 'course.name',
|
||||||
header: () => <span>Mata Kuliah</span>,
|
header: () => <span>Mata Kuliah</span>,
|
||||||
@ -90,7 +90,10 @@ export function createCourseClassColumns(
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canViewEnrollments || canUpdate || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -127,6 +130,8 @@ export function createCourseClassColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,7 +82,10 @@ export default function ClassEnrollmentIndex({
|
|||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -103,8 +106,8 @@ export default function ClassEnrollmentIndex({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export function createAcademicTermColumns(
|
|||||||
canUpdateStatus,
|
canUpdateStatus,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<AcademicTerm>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: () => <span>Nama</span>,
|
header: () => <span>Nama</span>,
|
||||||
@ -89,7 +89,10 @@ export function createAcademicTermColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -120,6 +123,8 @@ export function createAcademicTermColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export function createCourseColumns(
|
|||||||
): ColumnDef<Course>[] {
|
): ColumnDef<Course>[] {
|
||||||
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<Course>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'code',
|
accessorKey: 'code',
|
||||||
header: () => <span>Kode</span>,
|
header: () => <span>Kode</span>,
|
||||||
@ -73,7 +73,10 @@ export function createCourseColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -104,6 +107,8 @@ export function createCourseColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export function createDepartmentColumns(
|
|||||||
): ColumnDef<Department>[] {
|
): ColumnDef<Department>[] {
|
||||||
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<Department>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'code',
|
accessorKey: 'code',
|
||||||
header: () => <span>Kode</span>,
|
header: () => <span>Kode</span>,
|
||||||
@ -55,7 +55,10 @@ export function createDepartmentColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -86,6 +89,8 @@ export function createDepartmentColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export function createAcademicAdvisingLogColumns(
|
|||||||
): ColumnDef<AcademicAdvisingLog>[] {
|
): ColumnDef<AcademicAdvisingLog>[] {
|
||||||
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<AcademicAdvisingLog>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'student.student_number',
|
accessorKey: 'student.student_number',
|
||||||
header: () => <span>Mahasiswa</span>,
|
header: () => <span>Mahasiswa</span>,
|
||||||
@ -72,7 +72,10 @@ export function createAcademicAdvisingLogColumns(
|
|||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -99,6 +102,8 @@ export function createAcademicAdvisingLogColumns(
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,39 +2,63 @@ import type { ColumnDef } from '@tanstack/react-table';
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { Paperclip, Pencil, Trash2 } from 'lucide-react';
|
import { Paperclip, Pencil, Trash2 } from 'lucide-react';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { StatusBadge } from '@/components/status-badge';
|
||||||
import type { LetterRequest, LetterStatus } from '@/types/letter-request';
|
import type { LetterRequest } from '@/types/letter-request';
|
||||||
import { LetterStatusLabels } from '@/types/letter-request';
|
import { LetterStatuses, LetterStatusLabels } from '@/types/letter-request';
|
||||||
|
|
||||||
export type { LetterRequest } from '@/types/letter-request';
|
export type { LetterRequest } from '@/types/letter-request';
|
||||||
|
|
||||||
|
const LetterStatusVariants: Record<
|
||||||
|
string,
|
||||||
|
'default' | 'secondary' | 'destructive' | 'outline'
|
||||||
|
> = {
|
||||||
|
submitted: 'outline',
|
||||||
|
in_process: 'secondary',
|
||||||
|
completed: 'default',
|
||||||
|
rejected: 'destructive',
|
||||||
|
};
|
||||||
|
|
||||||
|
const letterStatusOptions = LetterStatuses.map((status) => ({
|
||||||
|
value: status,
|
||||||
|
label: LetterStatusLabels[status],
|
||||||
|
}));
|
||||||
|
|
||||||
type CreateColumnsParams = {
|
type CreateColumnsParams = {
|
||||||
handleEdit: (letterRequest: LetterRequest) => void;
|
handleEdit: (letterRequest: LetterRequest) => void;
|
||||||
handleDeleteClick: (letterRequest: LetterRequest) => void;
|
handleDeleteClick: (letterRequest: LetterRequest) => void;
|
||||||
|
handleStatusChange: (letterRequest: LetterRequest, status: string) => void;
|
||||||
canUpdate: boolean;
|
canUpdate: boolean;
|
||||||
canDelete: boolean;
|
canDelete: boolean;
|
||||||
|
canProcess: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createLetterRequestColumns(
|
export function createLetterRequestColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<LetterRequest>[] {
|
): ColumnDef<LetterRequest>[] {
|
||||||
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
const {
|
||||||
|
handleEdit,
|
||||||
|
handleDeleteClick,
|
||||||
|
handleStatusChange,
|
||||||
|
canUpdate,
|
||||||
|
canDelete,
|
||||||
|
canProcess,
|
||||||
|
} = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<LetterRequest>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'student.student_number',
|
accessorKey: 'user.student.student_number',
|
||||||
header: () => <span>Mahasiswa</span>,
|
header: () => <span>Mahasiswa</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const student = row.original.student;
|
const user = row.original.user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{student?.user?.profile?.full_name ?? 'N/A'}
|
{user?.profile?.full_name ?? 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{student?.student_number} ·{' '}
|
{user?.student?.student_number} ·{' '}
|
||||||
{student?.department?.name ?? '-'}
|
{user?.student?.department?.name ?? '-'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -67,30 +91,19 @@ export function createLetterRequestColumns(
|
|||||||
className: 'w-[120px] text-center',
|
className: 'w-[120px] text-center',
|
||||||
headerClassName: 'w-[120px] text-center',
|
headerClassName: 'w-[120px] text-center',
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => (
|
||||||
const status = row.getValue('status') as LetterStatus | null;
|
|
||||||
|
|
||||||
const variant =
|
|
||||||
status === 'completed'
|
|
||||||
? 'default'
|
|
||||||
: status === 'rejected'
|
|
||||||
? 'destructive'
|
|
||||||
: status === 'in_process'
|
|
||||||
? 'secondary'
|
|
||||||
: 'outline';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
{status ? (
|
<StatusBadge
|
||||||
<Badge variant={variant}>
|
status={row.original.status}
|
||||||
{LetterStatusLabels[status]}
|
statuses={letterStatusOptions}
|
||||||
</Badge>
|
variants={LetterStatusVariants}
|
||||||
) : (
|
onChange={(status) =>
|
||||||
<span className="text-muted-foreground">-</span>
|
handleStatusChange(row.original, status)
|
||||||
)}
|
}
|
||||||
|
disabled={!canProcess}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'processor.profile.full_name',
|
accessorKey: 'processor.profile.full_name',
|
||||||
@ -132,20 +145,26 @@ export function createLetterRequestColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
className: 'w-[100px] text-center',
|
className: 'w-[100px] text-center',
|
||||||
headerClassName: 'w-[100px] text-center',
|
headerClassName: 'w-[100px] text-center',
|
||||||
},
|
},
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
|
const isLocked = row.original.status !== 'submitted';
|
||||||
|
|
||||||
|
return (
|
||||||
<RowActions
|
<RowActions
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: canUpdate,
|
show: canUpdate && !isLocked,
|
||||||
onClick: () => handleEdit(row.original),
|
onClick: () => handleEdit(row.original),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -153,12 +172,15 @@ export function createLetterRequestColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: canDelete,
|
show: canDelete && !isLocked,
|
||||||
onClick: () => handleDeleteClick(row.original),
|
onClick: () => handleDeleteClick(row.original),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
},
|
},
|
||||||
];
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
import { Head, router } from '@inertiajs/react';
|
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import type { PaginationState } from '@/components/data-table';
|
import type { PaginationState } from '@/components/data-table';
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||||
@ -10,38 +7,25 @@ import { FilterDialog } from '@/components/filter-dialog';
|
|||||||
import { FormDialog } from '@/components/form-dialog';
|
import { FormDialog } from '@/components/form-dialog';
|
||||||
import InputError from '@/components/input-error';
|
import InputError from '@/components/input-error';
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
|
||||||
Combobox,
|
|
||||||
ComboboxContent,
|
|
||||||
ComboboxEmpty,
|
|
||||||
ComboboxInput,
|
|
||||||
ComboboxItem,
|
|
||||||
ComboboxList,
|
|
||||||
} from '@/components/ui/combobox';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { usePermissions } from '@/hooks/use-permissions';
|
import { usePermissions } from '@/hooks/use-permissions';
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
index as letterRequestIndex,
|
|
||||||
destroy,
|
destroy,
|
||||||
|
index as letterRequestIndex,
|
||||||
store,
|
store,
|
||||||
update,
|
update,
|
||||||
|
update_status,
|
||||||
} from '@/routes/admin/services/letter-requests';
|
} from '@/routes/admin/services/letter-requests';
|
||||||
import type {
|
import type { LetterRequest } from '@/types/letter-request';
|
||||||
LetterRequest,
|
|
||||||
LetterRequestStudent,
|
|
||||||
} from '@/types/letter-request';
|
|
||||||
import { LetterStatuses, LetterStatusLabels } from '@/types/letter-request';
|
import { LetterStatuses, LetterStatusLabels } from '@/types/letter-request';
|
||||||
|
import { Head, router } from '@inertiajs/react';
|
||||||
|
import { Info, Plus } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { createLetterRequestColumns } from './columns';
|
import { createLetterRequestColumns } from './columns';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -52,30 +36,26 @@ type Props = {
|
|||||||
per_page: number;
|
per_page: number;
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
students: LetterRequestStudent[];
|
|
||||||
highlight?: number;
|
highlight?: number;
|
||||||
filters: {
|
filters: {
|
||||||
status?: string;
|
status?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function studentLabel(student: LetterRequestStudent): string {
|
|
||||||
return `${student.user?.profile?.full_name ?? 'N/A'} - ${student.student_number}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LetterRequestIndex({
|
export default function LetterRequestIndex({
|
||||||
letterRequests,
|
letterRequests,
|
||||||
students,
|
|
||||||
highlight,
|
highlight,
|
||||||
filters,
|
filters,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<LetterRequest | null>(null);
|
const [editing, setEditing] = useState<LetterRequest | null>(null);
|
||||||
const [deleting, setDeleting] = useState<LetterRequest | null>(null);
|
const [deleting, setDeleting] = useState<LetterRequest | null>(null);
|
||||||
|
const [processing, setProcessing] = useState<LetterRequest | null>(null);
|
||||||
const { hasPermission } = usePermissions();
|
const { hasPermission } = usePermissions();
|
||||||
const canCreate = hasPermission('create-letter-requests');
|
const canCreate = hasPermission('create-letter-requests');
|
||||||
const canUpdate = hasPermission('update-letter-requests');
|
const canUpdate = hasPermission('update-letter-requests');
|
||||||
const canDelete = hasPermission('delete-letter-requests');
|
const canDelete = hasPermission('delete-letter-requests');
|
||||||
|
const canProcess = hasPermission('update-letter-requests-status');
|
||||||
|
|
||||||
const filterFields: FilterField[] = [
|
const filterFields: FilterField[] = [
|
||||||
{
|
{
|
||||||
@ -117,11 +97,27 @@ export default function LetterRequestIndex({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleStatusChange(letterRequest: LetterRequest, status: string) {
|
||||||
|
if (status === 'completed') {
|
||||||
|
setProcessing(letterRequest);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
update_status.url(letterRequest.id),
|
||||||
|
{ status },
|
||||||
|
{ preserveScroll: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const columns = createLetterRequestColumns({
|
const columns = createLetterRequestColumns({
|
||||||
handleEdit: (letterRequest) => setEditing(letterRequest),
|
handleEdit: (letterRequest) => setEditing(letterRequest),
|
||||||
handleDeleteClick: (letterRequest) => setDeleting(letterRequest),
|
handleDeleteClick: (letterRequest) => setDeleting(letterRequest),
|
||||||
|
handleStatusChange,
|
||||||
canUpdate,
|
canUpdate,
|
||||||
canDelete,
|
canDelete,
|
||||||
|
canProcess,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -146,18 +142,14 @@ export default function LetterRequestIndex({
|
|||||||
onClick={() => setCreateOpen(true)}
|
onClick={() => setCreateOpen(true)}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Tambah
|
Ajukan
|
||||||
</button>
|
</button>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CreateForm
|
<CreateForm open={createOpen} onOpenChange={setCreateOpen} />
|
||||||
open={createOpen}
|
|
||||||
onOpenChange={setCreateOpen}
|
|
||||||
students={students}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditForm
|
<EditForm
|
||||||
key={editing?.id}
|
key={editing?.id}
|
||||||
@ -168,9 +160,30 @@ export default function LetterRequestIndex({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
editing={editing}
|
editing={editing}
|
||||||
students={students}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ProcessForm
|
||||||
|
key={processing?.id}
|
||||||
|
open={processing !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setProcessing(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
processing={processing}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{canProcess && (
|
||||||
|
<Alert>
|
||||||
|
<Info />
|
||||||
|
<AlertTitle>Ubah status permohonan</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Klik badge Status pada tabel untuk mengubah status
|
||||||
|
permohonan secara langsung.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={letterRequests.data}
|
data={letterRequests.data}
|
||||||
@ -198,7 +211,7 @@ export default function LetterRequestIndex({
|
|||||||
}}
|
}}
|
||||||
title="Hapus Surat Permohonan"
|
title="Hapus Surat Permohonan"
|
||||||
description={(letterRequest) =>
|
description={(letterRequest) =>
|
||||||
`Apakah Anda yakin ingin menghapus permohonan "${letterRequest.letter_type}" milik "${letterRequest.student?.user?.profile?.full_name ?? 'mahasiswa ini'}"? Tindakan ini tidak dapat dibatalkan.`
|
`Apakah Anda yakin ingin menghapus permohonan "${letterRequest.letter_type}" milik "${letterRequest.user?.profile?.full_name ?? 'mahasiswa ini'}"? Tindakan ini tidak dapat dibatalkan.`
|
||||||
}
|
}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
/>
|
/>
|
||||||
@ -210,55 +223,12 @@ export default function LetterRequestIndex({
|
|||||||
function LetterRequestFields({
|
function LetterRequestFields({
|
||||||
errors,
|
errors,
|
||||||
editing,
|
editing,
|
||||||
students,
|
|
||||||
resetKey,
|
|
||||||
}: {
|
}: {
|
||||||
errors: Record<string, string>;
|
errors: Record<string, string>;
|
||||||
editing?: LetterRequest;
|
editing?: LetterRequest;
|
||||||
students: LetterRequestStudent[];
|
|
||||||
resetKey?: string;
|
|
||||||
}) {
|
}) {
|
||||||
const [student, setStudent] = useState<LetterRequestStudent | null>(
|
|
||||||
editing?.student ?? null,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>
|
|
||||||
Mahasiswa <span className="text-destructive">*</span>
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="student_id"
|
|
||||||
value={student?.id ?? ''}
|
|
||||||
/>
|
|
||||||
<Combobox
|
|
||||||
items={students}
|
|
||||||
value={student}
|
|
||||||
onValueChange={setStudent}
|
|
||||||
itemToStringLabel={(s) => studentLabel(s)}
|
|
||||||
isItemEqualToValue={(a, b) => a.id === b.id}
|
|
||||||
>
|
|
||||||
<ComboboxInput
|
|
||||||
placeholder="Pilih mahasiswa"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<ComboboxContent>
|
|
||||||
<ComboboxEmpty>
|
|
||||||
Mahasiswa tidak ditemukan.
|
|
||||||
</ComboboxEmpty>
|
|
||||||
<ComboboxList>
|
|
||||||
{(s: LetterRequestStudent) => (
|
|
||||||
<ComboboxItem key={s.id} value={s}>
|
|
||||||
{studentLabel(s)}
|
|
||||||
</ComboboxItem>
|
|
||||||
)}
|
|
||||||
</ComboboxList>
|
|
||||||
</ComboboxContent>
|
|
||||||
</Combobox>
|
|
||||||
<InputError message={errors.student_id} />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="letter_type">
|
<Label htmlFor="letter_type">
|
||||||
Jenis Surat <span className="text-destructive">*</span>
|
Jenis Surat <span className="text-destructive">*</span>
|
||||||
@ -283,36 +253,6 @@ function LetterRequestFields({
|
|||||||
/>
|
/>
|
||||||
<InputError message={errors.purpose} />
|
<InputError message={errors.purpose} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>
|
|
||||||
Status <span className="text-destructive">*</span>
|
|
||||||
</Label>
|
|
||||||
<input type="hidden" name="status" />
|
|
||||||
<Select
|
|
||||||
name="status"
|
|
||||||
defaultValue={editing?.status ?? 'submitted'}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Pilih status" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{LetterStatuses.map((status) => (
|
|
||||||
<SelectItem key={status} value={status}>
|
|
||||||
{LetterStatusLabels[status]}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<InputError message={errors.status} />
|
|
||||||
</div>
|
|
||||||
<FileUploadField
|
|
||||||
key={resetKey}
|
|
||||||
name="result"
|
|
||||||
label="Dokumen Hasil"
|
|
||||||
existingFileName={editing?.result_name}
|
|
||||||
existingFileUrl={editing?.result_url}
|
|
||||||
error={errors.result}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -320,45 +260,75 @@ function LetterRequestFields({
|
|||||||
function CreateForm({
|
function CreateForm({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
students,
|
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
students: LetterRequestStudent[];
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<FormDialog
|
<FormDialog
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
title="Tambah Surat Permohonan"
|
title="Ajukan Surat Permohonan"
|
||||||
action={store()}
|
action={store()}
|
||||||
resetOnSuccess
|
resetOnSuccess
|
||||||
onSuccess={() => onOpenChange(false)}
|
onSuccess={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
{({ errors }) => (
|
{({ errors }) => (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<LetterRequestFields
|
<LetterRequestFields errors={errors} />
|
||||||
key={open ? 'open' : 'closed'}
|
|
||||||
errors={errors}
|
|
||||||
students={students}
|
|
||||||
resetKey={open ? 'open' : 'closed'}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormDialog>
|
</FormDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ProcessForm({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
processing,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
processing: LetterRequest | null;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FormDialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
title="Selesaikan Surat Permohonan"
|
||||||
|
action={processing ? update_status(processing.id) : ''}
|
||||||
|
resetOnSuccess
|
||||||
|
onSuccess={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{({ errors }) =>
|
||||||
|
processing && (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Unggah dokumen hasil untuk menandai surat ini
|
||||||
|
sebagai selesai.
|
||||||
|
</p>
|
||||||
|
<FileUploadField
|
||||||
|
name="result"
|
||||||
|
label="Dokumen Hasil"
|
||||||
|
existingFileName={processing.result_name}
|
||||||
|
existingFileUrl={processing.result_url}
|
||||||
|
error={errors.result}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</FormDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function EditForm({
|
function EditForm({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
editing,
|
editing,
|
||||||
students,
|
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
editing: LetterRequest | null;
|
editing: LetterRequest | null;
|
||||||
students: LetterRequestStudent[];
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<FormDialog
|
<FormDialog
|
||||||
@ -372,11 +342,7 @@ function EditForm({
|
|||||||
{({ errors }) =>
|
{({ errors }) =>
|
||||||
editing && (
|
editing && (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<LetterRequestFields
|
<LetterRequestFields errors={errors} editing={editing} />
|
||||||
errors={errors}
|
|
||||||
editing={editing}
|
|
||||||
students={students}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export function createAdministratorColumns(
|
|||||||
canUpdateStatus,
|
canUpdateStatus,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<Administrator>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'profile.full_name',
|
accessorKey: 'profile.full_name',
|
||||||
header: () => <span>Nama Lengkap</span>,
|
header: () => <span>Nama Lengkap</span>,
|
||||||
@ -86,7 +86,10 @@ export function createAdministratorColumns(
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canResetPassword || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -123,6 +126,8 @@ export function createAdministratorColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export function createLecturerColumns(
|
|||||||
canUpdateStatus,
|
canUpdateStatus,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<Lecturer>[] = [
|
||||||
{
|
{
|
||||||
id: 'identity',
|
id: 'identity',
|
||||||
header: () => <span>NIDN</span>,
|
header: () => <span>NIDN</span>,
|
||||||
@ -104,7 +104,10 @@ export function createLecturerColumns(
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canResetPassword || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -141,6 +144,8 @@ export function createLecturerColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export function createStudentColumns(
|
|||||||
canUpdateAccountStatus,
|
canUpdateAccountStatus,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
return [
|
const columns: ColumnDef<Student>[] = [
|
||||||
{
|
{
|
||||||
id: 'identity',
|
id: 'identity',
|
||||||
header: () => <span>NIM</span>,
|
header: () => <span>NIM</span>,
|
||||||
@ -143,7 +143,10 @@ export function createStudentColumns(
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canResetPassword || canDelete) {
|
||||||
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -180,6 +183,8 @@ export function createStudentColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { ChartAreaInteractive } from "@/components/chart-area-interactive";
|
import { ChartAreaInteractive } from "@/components/chart-area-interactive";
|
||||||
import { SectionCards } from "@/components/section-cards";
|
import { SectionCards } from "@/components/section-cards";
|
||||||
import { SiteHeader } from "@/components/site-header";
|
|
||||||
import {
|
import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
@ -17,7 +16,6 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<SiteHeader />
|
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col">
|
||||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
import type { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { Eye } from 'lucide-react';
|
|
||||||
import { RowActions } from '@/components/row-actions';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { show } from '@/routes/student/letter-requests';
|
|
||||||
import type { LetterRequest, LetterStatus } from '@/types/letter-request';
|
|
||||||
import { LetterStatusLabels } from '@/types/letter-request';
|
|
||||||
|
|
||||||
export const letterRequestColumns: ColumnDef<LetterRequest>[] = [
|
|
||||||
{
|
|
||||||
accessorKey: 'letter_type',
|
|
||||||
header: () => <span>Jenis Surat</span>,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const letterRequest = row.original;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">{letterRequest.letter_type}</p>
|
|
||||||
{letterRequest.purpose && (
|
|
||||||
<p className="line-clamp-1 max-w-xs text-xs text-muted-foreground">
|
|
||||||
{letterRequest.purpose}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'status',
|
|
||||||
header: () => <span className="block text-center">Status</span>,
|
|
||||||
meta: {
|
|
||||||
className: 'w-[130px] text-center',
|
|
||||||
headerClassName: 'w-[130px] text-center',
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status as LetterStatus | null;
|
|
||||||
|
|
||||||
const variant =
|
|
||||||
status === 'completed'
|
|
||||||
? 'default'
|
|
||||||
: status === 'rejected'
|
|
||||||
? 'destructive'
|
|
||||||
: status === 'in_process'
|
|
||||||
? 'secondary'
|
|
||||||
: 'outline';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
{status ? (
|
|
||||||
<Badge variant={variant}>
|
|
||||||
{LetterStatusLabels[status]}
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">-</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'submitted_at',
|
|
||||||
header: () => <span>Diajukan</span>,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const submittedAt = row.original.submitted_at;
|
|
||||||
|
|
||||||
return submittedAt
|
|
||||||
? format(new Date(submittedAt), 'd MMM yyyy, HH:mm')
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
|
||||||
meta: {
|
|
||||||
className: 'w-[80px] text-center',
|
|
||||||
headerClassName: 'w-[80px] text-center',
|
|
||||||
},
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<RowActions
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
label: 'Lihat Detail',
|
|
||||||
icon: <Eye className="h-4 w-4" />,
|
|
||||||
href: show.url(row.original.id),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
import { Form, Head, Link } from '@inertiajs/react';
|
|
||||||
import { ArrowLeft } from 'lucide-react';
|
|
||||||
import InputError from '@/components/input-error';
|
|
||||||
import { PageHeader } from '@/components/page-header';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
|
||||||
import { index, store } from '@/routes/student/letter-requests';
|
|
||||||
|
|
||||||
export default function LetterRequestCreate() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head title="Ajukan Surat Permohonan" />
|
|
||||||
|
|
||||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
|
||||||
<PageHeader
|
|
||||||
title="Ajukan Surat Permohonan"
|
|
||||||
actions={
|
|
||||||
<Button variant="outline" asChild>
|
|
||||||
<Link href={index.url()}>
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
Kembali
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form action={store()}>
|
|
||||||
{({ errors, processing }) => (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Detail Permohonan</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="grid gap-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="letter_type">
|
|
||||||
Jenis Surat{' '}
|
|
||||||
<span className="text-destructive">
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="letter_type"
|
|
||||||
name="letter_type"
|
|
||||||
placeholder="Contoh: Surat Aktif Kuliah"
|
|
||||||
aria-invalid={!!errors.letter_type}
|
|
||||||
/>
|
|
||||||
<InputError message={errors.letter_type} />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="purpose">Keperluan</Label>
|
|
||||||
<Textarea
|
|
||||||
id="purpose"
|
|
||||||
name="purpose"
|
|
||||||
placeholder="Jelaskan keperluan surat ini"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.purpose} />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="justify-end">
|
|
||||||
<Button type="submit" disabled={processing}>
|
|
||||||
Ajukan
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import { Head, Link } from '@inertiajs/react';
|
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
import { DataTable } from '@/components/data-table';
|
|
||||||
import { PageHeader } from '@/components/page-header';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { create } from '@/routes/student/letter-requests';
|
|
||||||
import type { LetterRequest } from '@/types/letter-request';
|
|
||||||
import { letterRequestColumns } from './columns';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
letterRequests: LetterRequest[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LetterRequestIndex({ letterRequests }: Props) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head title="Surat Permohonan" />
|
|
||||||
|
|
||||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
|
||||||
<PageHeader
|
|
||||||
title="Surat Permohonan"
|
|
||||||
actions={
|
|
||||||
<Button asChild>
|
|
||||||
<Link href={create.url()}>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
Ajukan Surat
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
columns={letterRequestColumns}
|
|
||||||
data={letterRequests}
|
|
||||||
emptyText="Anda belum pernah mengajukan surat permohonan."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
import { Head, Link } from '@inertiajs/react';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { ArrowLeft, Paperclip } from 'lucide-react';
|
|
||||||
import { PageHeader } from '@/components/page-header';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { index } from '@/routes/student/letter-requests';
|
|
||||||
import type { LetterRequest, LetterStatus } from '@/types/letter-request';
|
|
||||||
import { LetterStatusLabels } from '@/types/letter-request';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
letterRequest: LetterRequest;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LetterRequestShow({ letterRequest }: Props) {
|
|
||||||
const status = letterRequest.status as LetterStatus | null;
|
|
||||||
|
|
||||||
const variant =
|
|
||||||
status === 'completed'
|
|
||||||
? 'default'
|
|
||||||
: status === 'rejected'
|
|
||||||
? 'destructive'
|
|
||||||
: status === 'in_process'
|
|
||||||
? 'secondary'
|
|
||||||
: 'outline';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head title="Detail Surat Permohonan" />
|
|
||||||
|
|
||||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
|
||||||
<PageHeader
|
|
||||||
title="Detail Surat Permohonan"
|
|
||||||
actions={
|
|
||||||
<Button variant="outline" asChild>
|
|
||||||
<Link href={index.url()}>
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
Kembali
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-start justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<CardTitle>{letterRequest.letter_type}</CardTitle>
|
|
||||||
{letterRequest.submitted_at && (
|
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
|
||||||
Diajukan pada{' '}
|
|
||||||
{format(
|
|
||||||
new Date(letterRequest.submitted_at),
|
|
||||||
'd MMM yyyy, HH:mm',
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{status && (
|
|
||||||
<Badge variant={variant}>
|
|
||||||
{LetterStatusLabels[status]}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="grid gap-4 border-t pt-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-muted-foreground">
|
|
||||||
Keperluan
|
|
||||||
</p>
|
|
||||||
<p className="mt-1">
|
|
||||||
{letterRequest.purpose || '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{letterRequest.processor && (
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-muted-foreground">
|
|
||||||
Diproses Oleh
|
|
||||||
</p>
|
|
||||||
<p className="mt-1">
|
|
||||||
{letterRequest.processor.profile
|
|
||||||
?.full_name ?? '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{letterRequest.completed_at && (
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-muted-foreground">
|
|
||||||
Selesai Pada
|
|
||||||
</p>
|
|
||||||
<p className="mt-1">
|
|
||||||
{format(
|
|
||||||
new Date(letterRequest.completed_at),
|
|
||||||
'd MMM yyyy, HH:mm',
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-muted-foreground">
|
|
||||||
Dokumen
|
|
||||||
</p>
|
|
||||||
{letterRequest.result_url ? (
|
|
||||||
<a
|
|
||||||
href={letterRequest.result_url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="mt-1 inline-flex items-center gap-1 text-primary underline underline-offset-4 hover:text-primary/80"
|
|
||||||
>
|
|
||||||
<Paperclip className="h-3.5 w-3.5" />
|
|
||||||
{letterRequest.result_name}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<p className="mt-1 text-muted-foreground">
|
|
||||||
Belum tersedia.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -14,11 +14,13 @@ export const LetterStatusLabels: Record<LetterStatus, string> = {
|
|||||||
rejected: 'Ditolak',
|
rejected: 'Ditolak',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LetterRequestStudent = {
|
export type LetterRequestUser = {
|
||||||
id: number;
|
id: number;
|
||||||
|
profile: { full_name: string } | null;
|
||||||
|
student: {
|
||||||
student_number: string;
|
student_number: string;
|
||||||
department: { id: number; name: string } | null;
|
department: { id: number; name: string } | null;
|
||||||
user: { profile: { full_name: string } | null } | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LetterRequestProcessor = {
|
export type LetterRequestProcessor = {
|
||||||
@ -28,8 +30,8 @@ export type LetterRequestProcessor = {
|
|||||||
|
|
||||||
export type LetterRequest = {
|
export type LetterRequest = {
|
||||||
id: number;
|
id: number;
|
||||||
student_id: number;
|
user_id: number;
|
||||||
student: LetterRequestStudent | null;
|
user: LetterRequestUser | null;
|
||||||
letter_type: string;
|
letter_type: string;
|
||||||
purpose: string | null;
|
purpose: string | null;
|
||||||
status: LetterStatus | null;
|
status: LetterStatus | null;
|
||||||
|
|||||||
@ -137,13 +137,6 @@
|
|||||||
Route::post('/', [CourseRegistrationController::class, 'store'])->name('store');
|
Route::post('/', [CourseRegistrationController::class, 'store'])->name('store');
|
||||||
Route::get('{submission}', [CourseRegistrationController::class, 'show'])->name('show');
|
Route::get('{submission}', [CourseRegistrationController::class, 'show'])->name('show');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::prefix('letter-requests')->name('student.letter-requests.')->group(function () {
|
|
||||||
Route::get('/', [LetterRequestController::class, 'mine'])->name('index');
|
|
||||||
Route::get('create', [LetterRequestController::class, 'create'])->name('create');
|
|
||||||
Route::post('/', [LetterRequestController::class, 'store'])->name('store');
|
|
||||||
Route::get('{letter_request}', [LetterRequestController::class, 'show'])->name('show');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware('role:dosen')->prefix('academic-advising-logs')->name('lecturer.academic-advising-logs.')->group(function () {
|
Route::middleware('role:dosen')->prefix('academic-advising-logs')->name('lecturer.academic-advising-logs.')->group(function () {
|
||||||
@ -174,6 +167,7 @@
|
|||||||
->middlewareFor(['store'], 'permission:create-letter-requests')
|
->middlewareFor(['store'], 'permission:create-letter-requests')
|
||||||
->middlewareFor(['update'], 'permission:update-letter-requests')
|
->middlewareFor(['update'], 'permission:update-letter-requests')
|
||||||
->middlewareFor(['destroy'], 'permission:delete-letter-requests');
|
->middlewareFor(['destroy'], 'permission:delete-letter-requests');
|
||||||
|
Route::patch('letter-requests/{letter_request}/status', [LetterRequestController::class, 'updateStatus'])->name('letter-requests.update_status')->middleware('permission:update-letter-requests-status');
|
||||||
|
|
||||||
Route::resource('academic-advising-logs', AcademicAdvisingLogController::class)
|
Route::resource('academic-advising-logs', AcademicAdvisingLogController::class)
|
||||||
->except(['create', 'edit', 'show'])
|
->except(['create', 'edit', 'show'])
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user