Compare commits
No commits in common. "93ddee8fd6f47b2e2b6331c558f47785521e75e2" and "93c5514c2265d9f22d1071f9461b1140a6985cb5" have entirely different histories.
93ddee8fd6
...
93c5514c22
@ -6,10 +6,12 @@
|
|||||||
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;
|
||||||
|
|
||||||
@ -34,8 +36,36 @@ 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.']);
|
||||||
@ -58,4 +88,13 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,17 +9,40 @@ class AcademicAdvisingLogRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
|
if ($this->routeIs('lecturer.*')) {
|
||||||
|
return (bool) $this->user()->lecturer;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->user()->can($this->isMethod('post') ? 'create-academic-advising-logs' : 'update-academic-advising-logs');
|
return $this->user()->can($this->isMethod('post') ? 'create-academic-advising-logs' : 'update-academic-advising-logs');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
|
if ($this->routeIs('lecturer.*')) {
|
||||||
|
$lecturer = $this->user()->lecturer;
|
||||||
|
$adviseeIds = $lecturer ? $lecturer->advisees()->pluck('id') : collect();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'student_ids' => ['required', 'array', 'min:1'],
|
'student_id' => ['required', 'integer', Rule::in($adviseeIds)],
|
||||||
'student_ids.*' => ['integer', Rule::exists('students', 'id')],
|
'topic' => ['nullable', 'string', 'max:150'],
|
||||||
'lecturer_id' => ['required', 'integer', Rule::exists('lecturers', 'id')],
|
|
||||||
'topic' => ['required', 'string', 'max:150'],
|
|
||||||
'notes' => ['nullable', 'string'],
|
'notes' => ['nullable', 'string'],
|
||||||
|
'session_date' => ['nullable', 'date'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'student_id' => ['required', 'integer', Rule::exists('students', 'id')],
|
||||||
|
'lecturer_id' => ['required', 'integer', Rule::exists('lecturers', 'id')],
|
||||||
|
'topic' => ['nullable', 'string', 'max:150'],
|
||||||
|
'notes' => ['nullable', 'string'],
|
||||||
|
'session_date' => ['nullable', 'date'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'student_id.in' => 'Mahasiswa yang dipilih bukan mahasiswa bimbingan Anda.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,16 +6,22 @@
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
||||||
|
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class AcademicAdvisingLog extends Model
|
class AcademicAdvisingLog extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
public function students(): BelongsToMany
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Student::class);
|
return [
|
||||||
|
'session_date' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function student(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Student::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function lecturer(): BelongsTo
|
public function lecturer(): BelongsTo
|
||||||
|
|||||||
@ -3,53 +3,61 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
|
||||||
|
|
||||||
return AcademicAdvisingLog::query()
|
return AcademicAdvisingLog::query()
|
||||||
->select(['id', 'lecturer_id', 'topic', 'notes', 'created_at'])
|
->select(['id', 'student_id', 'lecturer_id', 'topic', 'notes', 'session_date', 'created_at'])
|
||||||
->with([
|
->with([
|
||||||
'students.user.profile',
|
'student.user.profile',
|
||||||
'students.department',
|
'student.department',
|
||||||
'lecturer.user.profile',
|
'lecturer.user.profile',
|
||||||
'lecturer.departments',
|
|
||||||
])
|
])
|
||||||
->when($search, fn ($q) => $q->where('topic', 'like', "%{$search}%")
|
->when($search, fn ($q) => $q->where('topic', 'like', "%{$search}%")
|
||||||
->orWhereHas('students', fn ($q) => $q->where('student_number', 'like', "%{$search}%")
|
->orWhereHas('student', fn ($q) => $q->where('student_number', 'like', "%{$search}%")
|
||||||
->orWhereHas('user.profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%"))))
|
->orWhereHas('user.profile', fn ($q) => $q->where('full_name', 'like', "%{$search}%"))))
|
||||||
->when($lecturerId, fn ($q) => $q->where('lecturer_id', $lecturerId))
|
->when($lecturerId, fn ($q) => $q->where('lecturer_id', $lecturerId))
|
||||||
->when($user->hasRole('dosen'), fn ($q) => $q->where('lecturer_id', $user->lecturer?->id))
|
|
||||||
->latest()
|
->latest()
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data): AcademicAdvisingLog
|
public function create(array $data): AcademicAdvisingLog
|
||||||
{
|
{
|
||||||
$log = AcademicAdvisingLog::create([
|
return AcademicAdvisingLog::create([
|
||||||
|
'student_id' => $data['student_id'],
|
||||||
'lecturer_id' => $data['lecturer_id'],
|
'lecturer_id' => $data['lecturer_id'],
|
||||||
'topic' => $data['topic'] ?? null,
|
'topic' => $data['topic'] ?? null,
|
||||||
'notes' => $data['notes'] ?? null,
|
'notes' => $data['notes'] ?? null,
|
||||||
|
'session_date' => $data['session_date'] ?? now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$log->students()->attach($data['student_ids']);
|
|
||||||
|
|
||||||
return $log;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(AcademicAdvisingLog $log, array $data): AcademicAdvisingLog
|
public function update(AcademicAdvisingLog $log, array $data): AcademicAdvisingLog
|
||||||
{
|
{
|
||||||
|
$log->student_id = $data['student_id'];
|
||||||
$log->lecturer_id = $data['lecturer_id'];
|
$log->lecturer_id = $data['lecturer_id'];
|
||||||
$log->topic = $data['topic'] ?? null;
|
$log->topic = $data['topic'] ?? null;
|
||||||
$log->notes = $data['notes'] ?? null;
|
$log->notes = $data['notes'] ?? null;
|
||||||
|
$log->session_date = $data['session_date'] ?? now();
|
||||||
$log->update();
|
$log->update();
|
||||||
|
|
||||||
$log->students()->sync($data['student_ids']);
|
|
||||||
|
|
||||||
return $log;
|
return $log;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,9 +10,11 @@ public function up(): void
|
|||||||
{
|
{
|
||||||
Schema::create('academic_advising_logs', function (Blueprint $table) {
|
Schema::create('academic_advising_logs', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
|
$table->foreignId('student_id')->constrained()->cascadeOnDelete();
|
||||||
$table->foreignId('lecturer_id')->constrained()->cascadeOnDelete();
|
$table->foreignId('lecturer_id')->constrained()->cascadeOnDelete();
|
||||||
$table->string('topic', 150)->nullable();
|
$table->string('topic', 150)->nullable();
|
||||||
$table->text('notes')->nullable();
|
$table->text('notes')->nullable();
|
||||||
|
$table->timestamp('session_date')->nullable()->useCurrent();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +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::create('academic_advising_log_student', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('academic_advising_log_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->foreignId('student_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('academic_advising_log_student');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -9,30 +9,25 @@ class AcademicAdvisingLogSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$logs = [
|
AcademicAdvisingLog::insert([
|
||||||
[
|
[
|
||||||
|
'student_id' => 1,
|
||||||
'lecturer_id' => 1,
|
'lecturer_id' => 1,
|
||||||
'topic' => 'Konsultasi KRS Semester 5',
|
'topic' => 'Konsultasi KRS Semester 5',
|
||||||
'notes' => 'Mahasiswa mengambil 21 SKS, disetujui',
|
'notes' => 'Mahasiswa mengambil 21 SKS, disetujui',
|
||||||
|
'session_date' => '2026-01-20 13:00:00',
|
||||||
'created_at' => '2026-01-20 13:00:00',
|
'created_at' => '2026-01-20 13:00:00',
|
||||||
'updated_at' => '2026-01-20 13:00:00',
|
'updated_at' => '2026-01-20 13:00:00',
|
||||||
'student_ids' => [1],
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
'student_id' => 2,
|
||||||
'lecturer_id' => 2,
|
'lecturer_id' => 2,
|
||||||
'topic' => 'Bimbingan Tugas Akhir Bab 1',
|
'topic' => 'Bimbingan Tugas Akhir Bab 1',
|
||||||
'notes' => 'Perlu perbaikan rumusan masalah',
|
'notes' => 'Perlu perbaikan rumusan masalah',
|
||||||
|
'session_date' => '2026-07-15 13:00:00',
|
||||||
'created_at' => '2026-07-15 13:00:00',
|
'created_at' => '2026-07-15 13:00:00',
|
||||||
'updated_at' => '2026-07-15 13:00:00',
|
'updated_at' => '2026-07-15 13:00:00',
|
||||||
'student_ids' => [2],
|
|
||||||
],
|
],
|
||||||
];
|
]);
|
||||||
|
|
||||||
foreach ($logs as $log) {
|
|
||||||
$studentIds = $log['student_ids'];
|
|
||||||
unset($log['student_ids']);
|
|
||||||
|
|
||||||
AcademicAdvisingLog::create($log)->students()->attach($studentIds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,8 +50,6 @@ public function run(): void
|
|||||||
'dosen' => [
|
'dosen' => [
|
||||||
'view-dashboard',
|
'view-dashboard',
|
||||||
'view-courses',
|
'view-courses',
|
||||||
'view-academic-advising-logs',
|
|
||||||
'view-students',
|
|
||||||
...$feedbackSelfService,
|
...$feedbackSelfService,
|
||||||
],
|
],
|
||||||
'staff-admin' => [
|
'staff-admin' => [
|
||||||
|
|||||||
@ -54,6 +54,7 @@ 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 studentCourseRegistrationsRoute } from '@/routes/student/course-registrations';
|
||||||
import type { Auth } from '@/types/auth';
|
import type { Auth } from '@/types/auth';
|
||||||
|
|
||||||
@ -61,9 +62,11 @@ const STAFF_ROLES = ['developer', 'staff-admin', 'staff-keuangan', 'kaprodi'];
|
|||||||
|
|
||||||
function buildNavMain({
|
function buildNavMain({
|
||||||
isMahasiswa,
|
isMahasiswa,
|
||||||
|
isDosen,
|
||||||
permissions,
|
permissions,
|
||||||
}: {
|
}: {
|
||||||
isMahasiswa: boolean;
|
isMahasiswa: boolean;
|
||||||
|
isDosen: boolean;
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
}): (NavGroup | NavItem)[] {
|
}): (NavGroup | NavItem)[] {
|
||||||
const can = (permission: string) => permissions.includes(permission);
|
const can = (permission: string) => permissions.includes(permission);
|
||||||
@ -203,11 +206,13 @@ function buildNavMain({
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(can('view-academic-advising-logs')
|
...(isDosen || can('view-academic-advising-logs')
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: 'Bimbingan Akademik',
|
name: 'Bimbingan Akademik',
|
||||||
url: academicAdvisingLogsRoute.url(),
|
url: isDosen
|
||||||
|
? lecturerAcademicAdvisingLogsRoute.url()
|
||||||
|
: academicAdvisingLogsRoute.url(),
|
||||||
icon: MessageCircle,
|
icon: MessageCircle,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -292,9 +297,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
const roleNames = auth?.user?.roles?.map((role) => role.name) ?? [];
|
const roleNames = auth?.user?.roles?.map((role) => role.name) ?? [];
|
||||||
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 permissions = auth?.permissions ?? [];
|
const permissions = auth?.permissions ?? [];
|
||||||
const navMain = buildNavMain({
|
const navMain = buildNavMain({
|
||||||
isMahasiswa,
|
isMahasiswa,
|
||||||
|
isDosen,
|
||||||
permissions,
|
permissions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
|
import { Head, Link, router } from '@inertiajs/react';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { Info, 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';
|
||||||
@ -27,10 +31,6 @@ import {
|
|||||||
} from '@/routes/admin/feedback';
|
} from '@/routes/admin/feedback';
|
||||||
import type { Feedback } from '@/types/feedback';
|
import type { Feedback } from '@/types/feedback';
|
||||||
import { FeedbackStatusLabels, FeedbackTypeLabels } from '@/types/feedback';
|
import { FeedbackStatusLabels, FeedbackTypeLabels } from '@/types/feedback';
|
||||||
import { Head, Link, router } from '@inertiajs/react';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { Info, Plus } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { createFeedbackColumns, FeedbackStatusVariants } from './columns';
|
import { createFeedbackColumns, FeedbackStatusVariants } from './columns';
|
||||||
|
|
||||||
type FeedbackTypeOption = { value: string; label: string };
|
type FeedbackTypeOption = { value: string; label: string };
|
||||||
@ -155,7 +155,6 @@ export default function FeedbackIndex({
|
|||||||
feedback={viewing}
|
feedback={viewing}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{canUpdateStatus && (
|
|
||||||
<Alert>
|
<Alert>
|
||||||
<Info />
|
<Info />
|
||||||
<AlertTitle>Ubah status Kritik dan Saran</AlertTitle>
|
<AlertTitle>Ubah status Kritik dan Saran</AlertTitle>
|
||||||
@ -164,7 +163,6 @@ export default function FeedbackIndex({
|
|||||||
Kritik dan Saran secara langsung.
|
Kritik dan Saran secara langsung.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import { Eye, Pencil, Trash2 } from 'lucide-react';
|
import { format } from 'date-fns';
|
||||||
|
import { Pencil, Trash2 } from 'lucide-react';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import type { AcademicAdvisingLog } from '@/types/academic-advising-log';
|
import type { AcademicAdvisingLog } from '@/types/academic-advising-log';
|
||||||
|
|
||||||
export type { AcademicAdvisingLog } from '@/types/academic-advising-log';
|
export type { AcademicAdvisingLog } from '@/types/academic-advising-log';
|
||||||
|
|
||||||
type CreateColumnsParams = {
|
type CreateColumnsParams = {
|
||||||
handleView: (log: AcademicAdvisingLog) => void;
|
|
||||||
handleEdit: (log: AcademicAdvisingLog) => void;
|
handleEdit: (log: AcademicAdvisingLog) => void;
|
||||||
handleDeleteClick: (log: AcademicAdvisingLog) => void;
|
handleDeleteClick: (log: AcademicAdvisingLog) => void;
|
||||||
canUpdate: boolean;
|
canUpdate: boolean;
|
||||||
@ -16,43 +16,25 @@ type CreateColumnsParams = {
|
|||||||
export function createAcademicAdvisingLogColumns(
|
export function createAcademicAdvisingLogColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<AcademicAdvisingLog>[] {
|
): ColumnDef<AcademicAdvisingLog>[] {
|
||||||
const { handleView, handleEdit, handleDeleteClick, canUpdate, canDelete } =
|
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
||||||
params;
|
|
||||||
|
|
||||||
const columns: ColumnDef<AcademicAdvisingLog>[] = [
|
const columns: ColumnDef<AcademicAdvisingLog>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'students',
|
accessorKey: 'student.student_number',
|
||||||
header: () => <span>Mahasiswa</span>,
|
header: () => <span>Mahasiswa</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const students = row.original.students;
|
const student = row.original.student;
|
||||||
|
|
||||||
if (students.length === 0) {
|
|
||||||
return <span className="text-muted-foreground">-</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visible = students.slice(0, 2);
|
|
||||||
const remaining = students.length - visible.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div>
|
||||||
{visible.map((student) => (
|
|
||||||
<div key={student.id}>
|
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{student.user?.profile?.full_name ??
|
{student?.user?.profile?.full_name ?? 'N/A'}
|
||||||
'N/A'}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{student.student_number} ·{' '}
|
{student?.student_number} ·{' '}
|
||||||
{student.department?.name ?? '-'}
|
{student?.department?.name ?? '-'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
{remaining > 0 && (
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
+{remaining} mahasiswa lainnya
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -79,8 +61,20 @@ export function createAcademicAdvisingLogColumns(
|
|||||||
header: () => <span>Topik</span>,
|
header: () => <span>Topik</span>,
|
||||||
cell: ({ row }) => row.original.topic ?? '-',
|
cell: ({ row }) => row.original.topic ?? '-',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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')
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (canUpdate || canDelete) {
|
||||||
columns.push({
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
@ -91,11 +85,6 @@ export function createAcademicAdvisingLogColumns(
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<RowActions
|
<RowActions
|
||||||
actions={[
|
actions={[
|
||||||
{
|
|
||||||
label: 'Lihat Detail',
|
|
||||||
icon: <Eye className="h-4 w-4" />,
|
|
||||||
onClick: () => handleView(row.original),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
@ -114,6 +103,7 @@ export function createAcademicAdvisingLogColumns(
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { Head, router } from '@inertiajs/react';
|
import { Head, router } from '@inertiajs/react';
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useState } from '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 { DateTimeField } from '@/components/datetime-field';
|
||||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||||
import type { FilterField } from '@/components/filter-dialog';
|
import type { FilterField } from '@/components/filter-dialog';
|
||||||
import { FilterDialog } from '@/components/filter-dialog';
|
import { FilterDialog } from '@/components/filter-dialog';
|
||||||
@ -13,22 +13,12 @@ import { PageHeader } from '@/components/page-header';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Combobox,
|
Combobox,
|
||||||
ComboboxChip,
|
|
||||||
ComboboxChips,
|
|
||||||
ComboboxChipsInput,
|
|
||||||
ComboboxContent,
|
ComboboxContent,
|
||||||
ComboboxEmpty,
|
ComboboxEmpty,
|
||||||
ComboboxInput,
|
ComboboxInput,
|
||||||
ComboboxItem,
|
ComboboxItem,
|
||||||
ComboboxList,
|
ComboboxList,
|
||||||
useComboboxAnchor,
|
|
||||||
} from '@/components/ui/combobox';
|
} from '@/components/ui/combobox';
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
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 { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
@ -81,7 +71,6 @@ export default function AcademicAdvisingLogIndex({
|
|||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<AcademicAdvisingLog | null>(null);
|
const [editing, setEditing] = useState<AcademicAdvisingLog | null>(null);
|
||||||
const [deleting, setDeleting] = useState<AcademicAdvisingLog | null>(null);
|
const [deleting, setDeleting] = useState<AcademicAdvisingLog | null>(null);
|
||||||
const [viewing, setViewing] = useState<AcademicAdvisingLog | null>(null);
|
|
||||||
const { hasPermission } = usePermissions();
|
const { hasPermission } = usePermissions();
|
||||||
const canCreate = hasPermission('create-academic-advising-logs');
|
const canCreate = hasPermission('create-academic-advising-logs');
|
||||||
const canUpdate = hasPermission('update-academic-advising-logs');
|
const canUpdate = hasPermission('update-academic-advising-logs');
|
||||||
@ -128,7 +117,6 @@ export default function AcademicAdvisingLogIndex({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns = createAcademicAdvisingLogColumns({
|
const columns = createAcademicAdvisingLogColumns({
|
||||||
handleView: (log) => setViewing(log),
|
|
||||||
handleEdit: (log) => setEditing(log),
|
handleEdit: (log) => setEditing(log),
|
||||||
handleDeleteClick: (log) => setDeleting(log),
|
handleDeleteClick: (log) => setDeleting(log),
|
||||||
canUpdate,
|
canUpdate,
|
||||||
@ -184,16 +172,6 @@ export default function AcademicAdvisingLogIndex({
|
|||||||
lecturers={lecturers}
|
lecturers={lecturers}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ViewDetailDialog
|
|
||||||
open={viewing !== null}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setViewing(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
log={viewing}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={logs.data}
|
data={logs.data}
|
||||||
@ -221,7 +199,7 @@ export default function AcademicAdvisingLogIndex({
|
|||||||
}}
|
}}
|
||||||
title="Hapus Log Bimbingan"
|
title="Hapus Log Bimbingan"
|
||||||
description={(log) =>
|
description={(log) =>
|
||||||
`Apakah Anda yakin ingin menghapus log bimbingan untuk "${log.students.map((s) => s.user?.profile?.full_name ?? 'N/A').join(', ') || 'mahasiswa ini'}"? Tindakan ini tidak dapat dibatalkan.`
|
`Apakah Anda yakin ingin menghapus log bimbingan untuk "${log.student?.user?.profile?.full_name ?? 'mahasiswa ini'}"? Tindakan ini tidak dapat dibatalkan.`
|
||||||
}
|
}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
/>
|
/>
|
||||||
@ -230,84 +208,6 @@ export default function AcademicAdvisingLogIndex({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewDetailDialog({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
log,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
log: AcademicAdvisingLog | null;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="flex max-h-[85vh] flex-col overflow-hidden sm:max-w-lg">
|
|
||||||
<DialogHeader className="shrink-0">
|
|
||||||
<DialogTitle>Detail Bimbingan Akademik</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{log && (
|
|
||||||
<div className="grid min-h-0 flex-1 gap-4 overflow-x-hidden overflow-y-auto">
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
Dicatat{' '}
|
|
||||||
{format(
|
|
||||||
new Date(log.created_at),
|
|
||||||
'd MMM yyyy, HH:mm',
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label className="text-muted-foreground">
|
|
||||||
Dosen Wali
|
|
||||||
</Label>
|
|
||||||
<p>
|
|
||||||
{log.lecturer?.user?.profile?.full_name ??
|
|
||||||
'N/A'}{' '}
|
|
||||||
· {log.lecturer?.lecturer_number}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label className="text-muted-foreground">
|
|
||||||
Mahasiswa
|
|
||||||
</Label>
|
|
||||||
{log.students.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground">-</p>
|
|
||||||
) : (
|
|
||||||
<ul className="grid gap-1">
|
|
||||||
{log.students.map((student) => (
|
|
||||||
<li key={student.id}>
|
|
||||||
{student.user?.profile
|
|
||||||
?.full_name ?? 'N/A'}{' '}
|
|
||||||
· {student.student_number}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label className="text-muted-foreground">
|
|
||||||
Topik
|
|
||||||
</Label>
|
|
||||||
<p className="font-medium">{log.topic ?? '-'}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label className="text-muted-foreground">
|
|
||||||
Catatan
|
|
||||||
</Label>
|
|
||||||
<p className="whitespace-pre-line">
|
|
||||||
{log.notes ?? '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AdvisingLogFields({
|
function AdvisingLogFields({
|
||||||
errors,
|
errors,
|
||||||
editing,
|
editing,
|
||||||
@ -319,23 +219,49 @@ function AdvisingLogFields({
|
|||||||
students: AcademicAdvisingLogStudent[];
|
students: AcademicAdvisingLogStudent[];
|
||||||
lecturers: AcademicAdvisingLogLecturer[];
|
lecturers: AcademicAdvisingLogLecturer[];
|
||||||
}) {
|
}) {
|
||||||
|
const [student, setStudent] = useState<AcademicAdvisingLogStudent | null>(
|
||||||
|
editing?.student ?? null,
|
||||||
|
);
|
||||||
const [lecturer, setLecturer] =
|
const [lecturer, setLecturer] =
|
||||||
useState<AcademicAdvisingLogLecturer | null>(editing?.lecturer ?? null);
|
useState<AcademicAdvisingLogLecturer | null>(editing?.lecturer ?? null);
|
||||||
const [selectedStudents, setSelectedStudents] = useState<
|
|
||||||
AcademicAdvisingLogStudent[]
|
|
||||||
>(editing?.students ?? []);
|
|
||||||
const studentAnchor = useComboboxAnchor();
|
|
||||||
|
|
||||||
const availableStudents = lecturer
|
|
||||||
? students.filter((s) =>
|
|
||||||
lecturer.departments.some(
|
|
||||||
(department) => department.id === s.department?.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
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: AcademicAdvisingLogStudent) => (
|
||||||
|
<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>
|
<Label>
|
||||||
Dosen Wali <span className="text-destructive">*</span>
|
Dosen Wali <span className="text-destructive">*</span>
|
||||||
@ -348,10 +274,7 @@ function AdvisingLogFields({
|
|||||||
<Combobox
|
<Combobox
|
||||||
items={lecturers}
|
items={lecturers}
|
||||||
value={lecturer}
|
value={lecturer}
|
||||||
onValueChange={(value) => {
|
onValueChange={setLecturer}
|
||||||
setLecturer(value);
|
|
||||||
setSelectedStudents([]);
|
|
||||||
}}
|
|
||||||
itemToStringLabel={(l) => lecturerLabel(l)}
|
itemToStringLabel={(l) => lecturerLabel(l)}
|
||||||
isItemEqualToValue={(a, b) => a.id === b.id}
|
isItemEqualToValue={(a, b) => a.id === b.id}
|
||||||
>
|
>
|
||||||
@ -373,86 +296,7 @@ function AdvisingLogFields({
|
|||||||
<InputError message={errors.lecturer_id} />
|
<InputError message={errors.lecturer_id} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<div className="flex items-center justify-between">
|
<Label htmlFor={editing ? 'edit-topic' : 'topic'}>Topik</Label>
|
||||||
<Label>
|
|
||||||
Mahasiswa <span className="text-destructive">*</span>
|
|
||||||
</Label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!lecturer}
|
|
||||||
className="text-xs text-primary underline underline-offset-4 hover:text-primary/80 disabled:pointer-events-none disabled:opacity-50"
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedStudents(
|
|
||||||
selectedStudents.length ===
|
|
||||||
availableStudents.length
|
|
||||||
? []
|
|
||||||
: availableStudents,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{selectedStudents.length ===
|
|
||||||
availableStudents.length &&
|
|
||||||
availableStudents.length > 0
|
|
||||||
? 'Batalkan Semua'
|
|
||||||
: 'Pilih Semua'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{selectedStudents.map((s) => (
|
|
||||||
<input
|
|
||||||
key={s.id}
|
|
||||||
type="hidden"
|
|
||||||
name="student_ids[]"
|
|
||||||
value={s.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Combobox
|
|
||||||
items={availableStudents}
|
|
||||||
multiple
|
|
||||||
disabled={!lecturer}
|
|
||||||
value={selectedStudents}
|
|
||||||
onValueChange={setSelectedStudents}
|
|
||||||
itemToStringLabel={(s) => studentLabel(s)}
|
|
||||||
isItemEqualToValue={(a, b) => a.id === b.id}
|
|
||||||
>
|
|
||||||
<ComboboxChips ref={studentAnchor}>
|
|
||||||
{selectedStudents.map((s) => (
|
|
||||||
<ComboboxChip
|
|
||||||
key={s.id}
|
|
||||||
aria-label={studentLabel(s)}
|
|
||||||
>
|
|
||||||
{studentLabel(s)}
|
|
||||||
</ComboboxChip>
|
|
||||||
))}
|
|
||||||
<ComboboxChipsInput
|
|
||||||
disabled={!lecturer}
|
|
||||||
placeholder={
|
|
||||||
selectedStudents.length > 0
|
|
||||||
? ''
|
|
||||||
: lecturer
|
|
||||||
? 'Pilih mahasiswa'
|
|
||||||
: 'Pilih dosen terlebih dahulu'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ComboboxChips>
|
|
||||||
<ComboboxContent anchor={studentAnchor}>
|
|
||||||
<ComboboxEmpty>
|
|
||||||
Mahasiswa tidak ditemukan.
|
|
||||||
</ComboboxEmpty>
|
|
||||||
<ComboboxList>
|
|
||||||
{(s: AcademicAdvisingLogStudent) => (
|
|
||||||
<ComboboxItem key={s.id} value={s}>
|
|
||||||
{studentLabel(s)}
|
|
||||||
</ComboboxItem>
|
|
||||||
)}
|
|
||||||
</ComboboxList>
|
|
||||||
</ComboboxContent>
|
|
||||||
</Combobox>
|
|
||||||
<InputError message={errors.student_ids} />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor={editing ? 'edit-topic' : 'topic'}>
|
|
||||||
Topik <span className="text-destructive">*</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
<Input
|
||||||
id={editing ? 'edit-topic' : 'topic'}
|
id={editing ? 'edit-topic' : 'topic'}
|
||||||
name="topic"
|
name="topic"
|
||||||
@ -473,6 +317,13 @@ function AdvisingLogFields({
|
|||||||
/>
|
/>
|
||||||
<InputError message={errors.notes} />
|
<InputError message={errors.notes} />
|
||||||
</div>
|
</div>
|
||||||
|
<DateTimeField
|
||||||
|
label="Tanggal Sesi"
|
||||||
|
name="session_date"
|
||||||
|
defaultValue={editing?.session_date}
|
||||||
|
placeholder="Pilih tanggal sesi"
|
||||||
|
error={errors.session_date}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
import { Head, Link, router } from '@inertiajs/react';
|
||||||
|
import { FileSpreadsheet, Info, Plus, RotateCcw } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
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';
|
||||||
@ -13,18 +16,15 @@ import {
|
|||||||
create,
|
create,
|
||||||
destroy,
|
destroy,
|
||||||
edit,
|
edit,
|
||||||
exportMethod as exportStudents,
|
|
||||||
reset_password,
|
reset_password,
|
||||||
index as studentsIndex,
|
|
||||||
update_status,
|
update_status,
|
||||||
update_user_status,
|
update_user_status,
|
||||||
|
exportMethod as exportStudents,
|
||||||
|
index as studentsIndex,
|
||||||
} from '@/routes/admin/users/students';
|
} from '@/routes/admin/users/students';
|
||||||
import { formatDepartmentLabel } from '@/types/department';
|
import { formatDepartmentLabel } from '@/types/department';
|
||||||
import { Head, Link, router } from '@inertiajs/react';
|
|
||||||
import { FileSpreadsheet, Info, Plus, RotateCcw } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import type { Student } from './columns';
|
|
||||||
import { createStudentColumns } from './columns';
|
import { createStudentColumns } from './columns';
|
||||||
|
import type { Student } from './columns';
|
||||||
|
|
||||||
type Department = { id: number; name: string; degree_level: string | null };
|
type Department = { id: number; name: string; degree_level: string | null };
|
||||||
type StatusOption = { value: string; label: string };
|
type StatusOption = { value: string; label: string };
|
||||||
@ -210,7 +210,6 @@ export default function StudentIndex({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{canUpdateAcademicStatus && (
|
|
||||||
<Alert>
|
<Alert>
|
||||||
<Info />
|
<Info />
|
||||||
<AlertTitle>Ubah status mahasiswa</AlertTitle>
|
<AlertTitle>Ubah status mahasiswa</AlertTitle>
|
||||||
@ -219,7 +218,6 @@ export default function StudentIndex({
|
|||||||
mahasiswa secara langsung.
|
mahasiswa secara langsung.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
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')
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
156
resources/js/pages/lecturer/academic-advising-logs/index.tsx
Normal file
156
resources/js/pages/lecturer/academic-advising-logs/index.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -9,16 +9,17 @@ export type AcademicAdvisingLogLecturer = {
|
|||||||
id: number;
|
id: number;
|
||||||
lecturer_number: string;
|
lecturer_number: string;
|
||||||
user: { profile: { full_name: string } | null } | null;
|
user: { profile: { full_name: string } | null } | null;
|
||||||
departments: { id: number; name: string }[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AcademicAdvisingLog = {
|
export type AcademicAdvisingLog = {
|
||||||
id: number;
|
id: number;
|
||||||
students: AcademicAdvisingLogStudent[];
|
student_id: number;
|
||||||
|
student: AcademicAdvisingLogStudent | null;
|
||||||
lecturer_id: number;
|
lecturer_id: number;
|
||||||
lecturer: AcademicAdvisingLogLecturer | null;
|
lecturer: AcademicAdvisingLogLecturer | null;
|
||||||
topic: string | null;
|
topic: string | null;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
|
session_date: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -139,6 +139,11 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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)
|
Route::resource('tuition-invoices', TuitionInvoiceController::class)
|
||||||
->except(['create', 'edit', 'show'])
|
->except(['create', 'edit', 'show'])
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user