Compare commits
No commits in common. "e9acf29c20bf44555870af3b6edc8c1776763e45" and "5cc33cce2c1e5ae065e88c537829084f06872f62" have entirely different histories.
e9acf29c20
...
5cc33cce2c
@ -6,12 +6,10 @@
|
|||||||
use App\Http\Requests\Admin\Services\AcademicAdvisingLogRequest;
|
use App\Http\Requests\Admin\Services\AcademicAdvisingLogRequest;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\AcademicAdvisingLog;
|
use App\Models\AcademicAdvisingLog;
|
||||||
use App\Models\Lecturer;
|
|
||||||
use App\Services\Admin\Services\AcademicAdvisingLogService;
|
use App\Services\Admin\Services\AcademicAdvisingLogService;
|
||||||
use App\Services\Admin\Users\LecturerService;
|
use App\Services\Admin\Users\LecturerService;
|
||||||
use App\Services\Admin\Users\StudentService;
|
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;
|
||||||
|
|
||||||
@ -36,36 +34,8 @@ public function index(PaginatedRequest $request): Response
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mine(Request $request): Response
|
|
||||||
{
|
|
||||||
$lecturer = $this->currentLecturer($request);
|
|
||||||
|
|
||||||
return Inertia::render('lecturer/academic-advising-logs/index', [
|
|
||||||
'logs' => $this->service->forLecturer($lecturer),
|
|
||||||
'advisees' => $lecturer->advisees()
|
|
||||||
->with('user.profile')
|
|
||||||
->get(['id', 'user_id', 'student_number']),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(AcademicAdvisingLogRequest $request): RedirectResponse
|
public function store(AcademicAdvisingLogRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
if ($request->routeIs('lecturer.*')) {
|
|
||||||
$lecturer = $this->currentLecturer($request);
|
|
||||||
|
|
||||||
$this->service->create([
|
|
||||||
'student_id' => $request->validated('student_id'),
|
|
||||||
'lecturer_id' => $lecturer->id,
|
|
||||||
'topic' => $request->validated('topic'),
|
|
||||||
'notes' => $request->validated('notes'),
|
|
||||||
'session_date' => $request->validated('session_date'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil dicatat.']);
|
|
||||||
|
|
||||||
return to_route('lecturer.academic-advising-logs.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->service->create($request->validated());
|
$this->service->create($request->validated());
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil ditambahkan.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil ditambahkan.']);
|
||||||
@ -88,13 +58,4 @@ public function destroy(AcademicAdvisingLog $academicAdvisingLog): RedirectRespo
|
|||||||
|
|
||||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil dihapus.'])->back();
|
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Bimbingan akademik berhasil dihapus.'])->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function currentLecturer(Request $request): Lecturer
|
|
||||||
{
|
|
||||||
$lecturer = $request->user()->lecturer;
|
|
||||||
|
|
||||||
abort_if(! $lecturer, 403);
|
|
||||||
|
|
||||||
return $lecturer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,11 +6,9 @@
|
|||||||
use App\Http\Requests\Admin\Services\LetterRequestRequest;
|
use App\Http\Requests\Admin\Services\LetterRequestRequest;
|
||||||
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 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;
|
||||||
|
|
||||||
@ -33,45 +31,8 @@ public function index(PaginatedRequest $request): Response
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
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.*')) {
|
|
||||||
$student = $this->currentStudent($request);
|
|
||||||
|
|
||||||
$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.']);
|
|
||||||
|
|
||||||
return to_route('student.letter-requests.show', $letterRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->service->create($request->validated(), $request->file('result'));
|
$this->service->create($request->validated(), $request->file('result'));
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil ditambahkan.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Surat permohonan berhasil ditambahkan.']);
|
||||||
@ -94,13 +55,4 @@ public function destroy(LetterRequest $letterRequest): RedirectResponse
|
|||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
$student = $request->user()->student;
|
|
||||||
|
|
||||||
abort_if(! $student, 403);
|
|
||||||
|
|
||||||
return $student;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,27 +9,11 @@ class AcademicAdvisingLogRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
if ($this->routeIs('lecturer.*')) {
|
|
||||||
return (bool) $this->user()->lecturer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
if ($this->routeIs('lecturer.*')) {
|
|
||||||
$lecturer = $this->user()->lecturer;
|
|
||||||
$adviseeIds = $lecturer ? $lecturer->advisees()->pluck('id') : collect();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'student_id' => ['required', 'integer', Rule::in($adviseeIds)],
|
|
||||||
'topic' => ['nullable', 'string', 'max:150'],
|
|
||||||
'notes' => ['nullable', 'string'],
|
|
||||||
'session_date' => ['nullable', 'date'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'student_id' => ['required', 'integer', Rule::exists('students', 'id')],
|
'student_id' => ['required', 'integer', Rule::exists('students', 'id')],
|
||||||
'lecturer_id' => ['required', 'integer', Rule::exists('lecturers', 'id')],
|
'lecturer_id' => ['required', 'integer', Rule::exists('lecturers', 'id')],
|
||||||
@ -38,11 +22,4 @@ public function rules(): array
|
|||||||
'session_date' => ['nullable', 'date'],
|
'session_date' => ['nullable', 'date'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function messages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'student_id.in' => 'Mahasiswa yang dipilih bukan mahasiswa bimbingan Anda.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,22 +10,11 @@ class LetterRequestRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
if ($this->routeIs('student.*')) {
|
|
||||||
return (bool) $this->user()->student;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
if ($this->routeIs('student.*')) {
|
|
||||||
return [
|
|
||||||
'letter_type' => ['required', 'string', 'max:100'],
|
|
||||||
'purpose' => ['nullable', 'string'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'student_id' => ['required', 'integer', Rule::exists('students', 'id')],
|
'student_id' => ['required', 'integer', Rule::exists('students', 'id')],
|
||||||
'letter_type' => ['required', 'string', 'max:100'],
|
'letter_type' => ['required', 'string', 'max:100'],
|
||||||
|
|||||||
@ -23,9 +23,4 @@ public function user(): BelongsTo
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function creator(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'created_by');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,16 +3,10 @@
|
|||||||
namespace App\Services\Admin\Manage;
|
namespace App\Services\Admin\Manage;
|
||||||
|
|
||||||
use App\Models\Announcement;
|
use App\Models\Announcement;
|
||||||
use App\Models\Student;
|
|
||||||
use App\Services\NotificationService;
|
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
class AnnouncementService
|
class AnnouncementService
|
||||||
{
|
{
|
||||||
public function __construct(
|
|
||||||
private readonly NotificationService $notificationService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', ?int $departmentId = null): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', ?int $departmentId = null): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return Announcement::query()
|
return Announcement::query()
|
||||||
@ -26,27 +20,13 @@ public function paginated(int $perPage = 25, string $search = '', ?int $departme
|
|||||||
|
|
||||||
public function create(array $data): Announcement
|
public function create(array $data): Announcement
|
||||||
{
|
{
|
||||||
$announcement = Announcement::create([
|
return Announcement::create([
|
||||||
'title' => $data['title'],
|
'title' => $data['title'],
|
||||||
'content' => $data['content'],
|
'content' => $data['content'],
|
||||||
'department_id' => $data['department_id'] ?? null,
|
'department_id' => $data['department_id'] ?? null,
|
||||||
'enrollment_year' => $data['enrollment_year'] ?? null,
|
'enrollment_year' => $data['enrollment_year'] ?? null,
|
||||||
'created_by' => auth()->id(),
|
'created_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$recipientUserIds = Student::query()
|
|
||||||
->when($announcement->department_id, fn ($q, $departmentId) => $q->where('department_id', $departmentId))
|
|
||||||
->when($announcement->enrollment_year, fn ($q, $year) => $q->where('enrollment_year', $year))
|
|
||||||
->pluck('user_id');
|
|
||||||
|
|
||||||
$this->notificationService->sendToUsers(
|
|
||||||
$recipientUserIds,
|
|
||||||
$announcement->title,
|
|
||||||
$announcement->content,
|
|
||||||
$announcement->created_by,
|
|
||||||
);
|
|
||||||
|
|
||||||
return $announcement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Announcement $announcement, array $data): Announcement
|
public function update(Announcement $announcement, array $data): Announcement
|
||||||
|
|||||||
@ -3,24 +3,10 @@
|
|||||||
namespace App\Services\Admin\Services;
|
namespace App\Services\Admin\Services;
|
||||||
|
|
||||||
use App\Models\AcademicAdvisingLog;
|
use App\Models\AcademicAdvisingLog;
|
||||||
use App\Models\Lecturer;
|
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
|
|
||||||
class AcademicAdvisingLogService
|
class AcademicAdvisingLogService
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @return Collection<int, AcademicAdvisingLog>
|
|
||||||
*/
|
|
||||||
public function forLecturer(Lecturer $lecturer): Collection
|
|
||||||
{
|
|
||||||
return AcademicAdvisingLog::query()
|
|
||||||
->where('lecturer_id', $lecturer->id)
|
|
||||||
->with('student.user.profile')
|
|
||||||
->latest()
|
|
||||||
->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', ?int $lecturerId = null): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', ?int $lecturerId = null): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return AcademicAdvisingLog::query()
|
return AcademicAdvisingLog::query()
|
||||||
|
|||||||
@ -4,30 +4,11 @@
|
|||||||
|
|
||||||
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
|
||||||
{
|
{
|
||||||
return LetterRequest::query()
|
return LetterRequest::query()
|
||||||
|
|||||||
@ -5,40 +5,13 @@
|
|||||||
use App\Models\Notification;
|
use App\Models\Notification;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
class NotificationService
|
class NotificationService
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @param Collection<int, int>|array<int, int> $userIds
|
|
||||||
*/
|
|
||||||
public function sendToUsers(Collection|array $userIds, string $title, ?string $content = null, ?int $createdBy = null): void
|
|
||||||
{
|
|
||||||
$userIds = collect($userIds)->filter()->unique()->values();
|
|
||||||
|
|
||||||
if ($userIds->isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$createdBy ??= auth()->id();
|
|
||||||
$now = now();
|
|
||||||
|
|
||||||
Notification::insert($userIds->map(fn (int $userId) => [
|
|
||||||
'user_id' => $userId,
|
|
||||||
'created_by' => $createdBy,
|
|
||||||
'title' => $title,
|
|
||||||
'content' => $content,
|
|
||||||
'is_read' => false,
|
|
||||||
'created_at' => $now,
|
|
||||||
'updated_at' => $now,
|
|
||||||
])->all());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function paginated(User $user, int $perPage = 15): LengthAwarePaginator
|
public function paginated(User $user, int $perPage = 15): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return Notification::query()
|
return Notification::query()
|
||||||
->where('user_id', $user->id)
|
->where('user_id', $user->id)
|
||||||
->with('creator.profile')
|
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
|
|||||||
@ -11,7 +11,6 @@ public function up(): void
|
|||||||
Schema::create('notifications', function (Blueprint $table) {
|
Schema::create('notifications', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
|
||||||
$table->string('title', 150)->nullable();
|
$table->string('title', 150)->nullable();
|
||||||
$table->text('content')->nullable();
|
$table->text('content')->nullable();
|
||||||
$table->boolean('is_read')->nullable()->default(false);
|
$table->boolean('is_read')->nullable()->default(false);
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('notifications', function (Blueprint $table) {
|
|
||||||
$table->foreignId('created_by')->nullable()->after('user_id')->constrained('users')->nullOnDelete();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('notifications', function (Blueprint $table) {
|
|
||||||
$table->dropConstrainedForeignId('created_by');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -50,21 +50,12 @@ import { index as letterRequestsRoute } from '@/routes/admin/services/letter-req
|
|||||||
import { index as administratorsRoute } from '@/routes/admin/users/administrators';
|
import { index as administratorsRoute } from '@/routes/admin/users/administrators';
|
||||||
import { index as lecturersRoute } from '@/routes/admin/users/lecturers';
|
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 studentCourseRegistrationsRoute } from '@/routes/student/course-registrations';
|
|
||||||
import { index as studentLetterRequestsRoute } from '@/routes/student/letter-requests';
|
|
||||||
import type { Auth } from '@/types/auth';
|
|
||||||
|
|
||||||
const STAFF_ROLES = ['developer', 'staff-admin', 'staff-keuangan', 'kaprodi'];
|
const data: {
|
||||||
|
navMain: (NavGroup | NavItem)[];
|
||||||
function buildNavMain({
|
navSecondary: { title: string; url: string; icon: Icon }[];
|
||||||
isMahasiswa,
|
} = {
|
||||||
isDosen,
|
navMain: [
|
||||||
}: {
|
|
||||||
isMahasiswa: boolean;
|
|
||||||
isDosen: boolean;
|
|
||||||
}): (NavGroup | NavItem)[] {
|
|
||||||
return [
|
|
||||||
{
|
{
|
||||||
name: 'Dasbor',
|
name: 'Dasbor',
|
||||||
url: '#',
|
url: '#',
|
||||||
@ -115,9 +106,7 @@ function buildNavMain({
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Registrasi KRS',
|
name: 'Registrasi KRS',
|
||||||
url: isMahasiswa
|
url: courseRegistrationsRoute.url(),
|
||||||
? studentCourseRegistrationsRoute.url()
|
|
||||||
: courseRegistrationsRoute.url(),
|
|
||||||
icon: FileCheck2,
|
icon: FileCheck2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -172,24 +161,18 @@ function buildNavMain({
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'Surat Permohonan',
|
name: 'Surat Permohonan',
|
||||||
url: isMahasiswa
|
url: letterRequestsRoute.url(),
|
||||||
? studentLetterRequestsRoute.url()
|
|
||||||
: letterRequestsRoute.url(),
|
|
||||||
icon: Mail,
|
icon: Mail,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Bimbingan Akademik',
|
name: 'Bimbingan Akademik',
|
||||||
url: isDosen
|
url: academicAdvisingLogsRoute.url(),
|
||||||
? lecturerAcademicAdvisingLogsRoute.url()
|
|
||||||
: academicAdvisingLogsRoute.url(),
|
|
||||||
icon: MessageCircle,
|
icon: MessageCircle,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
}
|
navSecondary: [
|
||||||
|
|
||||||
const navSecondary: { title: string; url: string; icon: Icon }[] = [
|
|
||||||
{
|
{
|
||||||
title: 'Kritik dan Saran',
|
title: 'Kritik dan Saran',
|
||||||
url: feedbackRoute.url(),
|
url: feedbackRoute.url(),
|
||||||
@ -200,15 +183,11 @@ const navSecondary: { title: string; url: string; icon: Icon }[] = [
|
|||||||
url: '#',
|
url: '#',
|
||||||
icon: IconHelp,
|
icon: IconHelp,
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
const { name, auth } = usePage<{ auth: Auth }>().props;
|
const { name } = usePage().props;
|
||||||
const roleNames = auth?.user?.roles?.map((role) => role.name) ?? [];
|
|
||||||
const isStaff = roleNames.some((role) => STAFF_ROLES.includes(role));
|
|
||||||
const isMahasiswa = !isStaff && roleNames.includes('mahasiswa');
|
|
||||||
const isDosen = !isStaff && roleNames.includes('dosen');
|
|
||||||
const navMain = buildNavMain({ isMahasiswa, isDosen });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" {...props}>
|
<Sidebar collapsible="icon" {...props}>
|
||||||
@ -230,8 +209,8 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<NavMain items={navMain} />
|
<NavMain items={data.navMain} />
|
||||||
<NavSecondary items={navSecondary} className="mt-auto" />
|
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,13 +6,6 @@ import { useState } from 'react';
|
|||||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Combobox, ComboboxContent } from '@/components/ui/combobox';
|
import { Combobox, ComboboxContent } from '@/components/ui/combobox';
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { destroy, markAllRead, clearAll, read } from '@/routes/notifications';
|
import { destroy, markAllRead, clearAll, read } from '@/routes/notifications';
|
||||||
import type { Notification, NotificationPage } from '@/types/notification';
|
import type { Notification, NotificationPage } from '@/types/notification';
|
||||||
@ -30,7 +23,6 @@ export function NotificationBell() {
|
|||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [clearAllOpen, setClearAllOpen] = useState(false);
|
const [clearAllOpen, setClearAllOpen] = useState(false);
|
||||||
const [detail, setDetail] = useState<Notification | null>(null);
|
|
||||||
|
|
||||||
const hasMore = notifications.current_page < notifications.last_page;
|
const hasMore = notifications.current_page < notifications.last_page;
|
||||||
const hasNotifications = notifications.data.length > 0;
|
const hasNotifications = notifications.data.length > 0;
|
||||||
@ -55,14 +47,6 @@ export function NotificationBell() {
|
|||||||
router.delete(destroy(notification.id).url, { preserveScroll: true });
|
router.delete(destroy(notification.id).url, { preserveScroll: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenDetail(notification: Notification) {
|
|
||||||
setDetail(notification);
|
|
||||||
|
|
||||||
if (!notification.is_read) {
|
|
||||||
handleMarkRead(notification);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Combobox open={open} onOpenChange={setOpen}>
|
<Combobox open={open} onOpenChange={setOpen}>
|
||||||
@ -86,9 +70,9 @@ export function NotificationBell() {
|
|||||||
|
|
||||||
<ComboboxContent
|
<ComboboxContent
|
||||||
align="end"
|
align="end"
|
||||||
className="flex w-96 max-w-[90vw] min-w-96 flex-col p-0"
|
className="w-96 max-w-[90vw] min-w-96 p-0"
|
||||||
>
|
>
|
||||||
<div className="flex shrink-0 items-center justify-between border-b p-3">
|
<div className="flex items-center justify-between border-b p-3">
|
||||||
<span className="font-medium">Notifikasi</span>
|
<span className="font-medium">Notifikasi</span>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
@ -112,7 +96,7 @@ export function NotificationBell() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
<div className="max-h-96 overflow-y-auto">
|
||||||
{hasNotifications ? (
|
{hasNotifications ? (
|
||||||
<div className="flex flex-col divide-y">
|
<div className="flex flex-col divide-y">
|
||||||
{notifications.data.map((notification) => (
|
{notifications.data.map((notification) => (
|
||||||
@ -124,13 +108,7 @@ export function NotificationBell() {
|
|||||||
'bg-primary/5',
|
'bg-primary/5',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<div className="flex items-start gap-2">
|
||||||
type="button"
|
|
||||||
className="flex flex-1 cursor-pointer items-start gap-2 text-left"
|
|
||||||
onClick={() =>
|
|
||||||
handleOpenDetail(notification)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!notification.is_read && (
|
{!notification.is_read && (
|
||||||
<span className="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-primary" />
|
<span className="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-primary" />
|
||||||
)}
|
)}
|
||||||
@ -139,14 +117,11 @@ export function NotificationBell() {
|
|||||||
{notification.title ?? '-'}
|
{notification.title ?? '-'}
|
||||||
</p>
|
</p>
|
||||||
{notification.content && (
|
{notification.content && (
|
||||||
<p className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
{notification.content}
|
{notification.content}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{notification.creator
|
|
||||||
?.profile?.full_name &&
|
|
||||||
`${notification.creator.profile.full_name} · `}
|
|
||||||
{format(
|
{format(
|
||||||
new Date(
|
new Date(
|
||||||
notification.created_at,
|
notification.created_at,
|
||||||
@ -155,7 +130,7 @@ export function NotificationBell() {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
{!notification.is_read && (
|
{!notification.is_read && (
|
||||||
<Button
|
<Button
|
||||||
@ -227,35 +202,6 @@ export function NotificationBell() {
|
|||||||
confirmLabel="Hapus"
|
confirmLabel="Hapus"
|
||||||
onConfirm={handleClearAll}
|
onConfirm={handleClearAll}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dialog
|
|
||||||
open={detail !== null}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setDetail(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent className="flex max-h-[85vh] flex-col overflow-hidden">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{detail?.title ?? '-'}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{detail?.creator?.profile?.full_name &&
|
|
||||||
`${detail.creator.profile.full_name} · `}
|
|
||||||
{detail &&
|
|
||||||
format(
|
|
||||||
new Date(detail.created_at),
|
|
||||||
'd MMM yyyy, HH:mm',
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{detail?.content && (
|
|
||||||
<p className="overflow-y-auto text-sm whitespace-pre-wrap text-foreground">
|
|
||||||
{detail.content}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
import type { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import type { AcademicAdvisingLog } from '@/types/academic-advising-log';
|
|
||||||
|
|
||||||
export const advisingLogColumns: ColumnDef<AcademicAdvisingLog>[] = [
|
|
||||||
{
|
|
||||||
accessorKey: 'student.student_number',
|
|
||||||
header: () => <span>Mahasiswa</span>,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const student = row.original.student;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">
|
|
||||||
{student?.user?.profile?.full_name ?? 'N/A'}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{student?.student_number}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'topic',
|
|
||||||
header: () => <span>Topik</span>,
|
|
||||||
cell: ({ row }) => row.original.topic || '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'notes',
|
|
||||||
header: () => <span>Catatan</span>,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<p className="line-clamp-2 max-w-sm text-muted-foreground">
|
|
||||||
{row.original.notes || '-'}
|
|
||||||
</p>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'session_date',
|
|
||||||
header: () => <span>Tanggal Sesi</span>,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const sessionDate = row.original.session_date;
|
|
||||||
|
|
||||||
return sessionDate
|
|
||||||
? format(new Date(sessionDate), 'd MMM yyyy, HH:mm')
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
import { Head } from '@inertiajs/react';
|
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { DataTable } from '@/components/data-table';
|
|
||||||
import { DateTimeField } from '@/components/datetime-field';
|
|
||||||
import { FormDialog } from '@/components/form-dialog';
|
|
||||||
import InputError from '@/components/input-error';
|
|
||||||
import { PageHeader } from '@/components/page-header';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
|
||||||
import { store } from '@/routes/lecturer/academic-advising-logs';
|
|
||||||
import type { AcademicAdvisingLog } from '@/types/academic-advising-log';
|
|
||||||
import { advisingLogColumns } from './columns';
|
|
||||||
|
|
||||||
type Advisee = {
|
|
||||||
id: number;
|
|
||||||
student_number: string;
|
|
||||||
user: { profile: { full_name: string } | null } | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
logs: AcademicAdvisingLog[];
|
|
||||||
advisees: Advisee[];
|
|
||||||
};
|
|
||||||
|
|
||||||
function adviseeLabel(advisee: Advisee): string {
|
|
||||||
return `${advisee.user?.profile?.full_name ?? 'N/A'} - ${advisee.student_number}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LecturerAcademicAdvisingLogIndex({
|
|
||||||
logs,
|
|
||||||
advisees,
|
|
||||||
}: Props) {
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head title="Bimbingan Akademik" />
|
|
||||||
|
|
||||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
|
||||||
<PageHeader
|
|
||||||
title="Bimbingan Akademik"
|
|
||||||
actions={
|
|
||||||
<Button asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setCreateOpen(true)}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
Tambah Bimbingan
|
|
||||||
</button>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CreateForm
|
|
||||||
open={createOpen}
|
|
||||||
onOpenChange={setCreateOpen}
|
|
||||||
advisees={advisees}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
columns={advisingLogColumns}
|
|
||||||
data={logs}
|
|
||||||
emptyText="Belum ada log bimbingan akademik."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateForm({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
advisees,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
advisees: Advisee[];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<FormDialog
|
|
||||||
open={open}
|
|
||||||
onOpenChange={onOpenChange}
|
|
||||||
title="Tambah Bimbingan Akademik"
|
|
||||||
action={store()}
|
|
||||||
resetOnSuccess
|
|
||||||
onSuccess={() => onOpenChange(false)}
|
|
||||||
>
|
|
||||||
{({ errors }) => (
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>
|
|
||||||
Mahasiswa{' '}
|
|
||||||
<span className="text-destructive">*</span>
|
|
||||||
</Label>
|
|
||||||
<Select name="student_id">
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Pilih mahasiswa bimbingan" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{advisees.map((advisee) => (
|
|
||||||
<SelectItem
|
|
||||||
key={advisee.id}
|
|
||||||
value={String(advisee.id)}
|
|
||||||
>
|
|
||||||
{adviseeLabel(advisee)}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{advisees.length === 0 && (
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Anda belum memiliki mahasiswa bimbingan.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<InputError message={errors.student_id} />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="topic">Topik</Label>
|
|
||||||
<Input
|
|
||||||
id="topic"
|
|
||||||
name="topic"
|
|
||||||
placeholder="Masukkan topik bimbingan"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.topic} />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="notes">Catatan</Label>
|
|
||||||
<Textarea
|
|
||||||
id="notes"
|
|
||||||
name="notes"
|
|
||||||
placeholder="Masukkan catatan bimbingan"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.notes} />
|
|
||||||
</div>
|
|
||||||
<DateTimeField
|
|
||||||
label="Tanggal Sesi"
|
|
||||||
name="session_date"
|
|
||||||
placeholder="Pilih tanggal sesi"
|
|
||||||
error={errors.session_date}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FormDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -3,7 +3,6 @@ export type Notification = {
|
|||||||
title: string | null;
|
title: string | null;
|
||||||
content: string | null;
|
content: string | null;
|
||||||
is_read: boolean;
|
is_read: boolean;
|
||||||
creator: { profile: { full_name: string } | null } | null;
|
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -85,27 +85,13 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware('role:mahasiswa')->group(function () {
|
Route::middleware('role:mahasiswa')->prefix('course-registrations')->name('student.course-registrations.')->group(function () {
|
||||||
Route::prefix('course-registrations')->name('student.course-registrations.')->group(function () {
|
|
||||||
Route::get('/', [CourseRegistrationController::class, 'mine'])->name('index');
|
Route::get('/', [CourseRegistrationController::class, 'mine'])->name('index');
|
||||||
Route::get('create', [CourseRegistrationController::class, 'create'])->name('create');
|
Route::get('create', [CourseRegistrationController::class, 'create'])->name('create');
|
||||||
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::get('/', [AcademicAdvisingLogController::class, 'mine'])->name('index');
|
|
||||||
Route::post('/', [AcademicAdvisingLogController::class, 'store'])->name('store');
|
|
||||||
});
|
|
||||||
|
|
||||||
Route::prefix('admin/finances')->name('admin.finances.')->group(function () {
|
Route::prefix('admin/finances')->name('admin.finances.')->group(function () {
|
||||||
Route::resource('tuition-invoices', TuitionInvoiceController::class)->except(['create', 'edit', 'show']);
|
Route::resource('tuition-invoices', TuitionInvoiceController::class)->except(['create', 'edit', 'show']);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user