Compare commits
2 Commits
93c5514c22
...
93ddee8fd6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93ddee8fd6 | ||
|
|
a6c6966cf1 |
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,40 +9,17 @@ 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_id' => ['required', 'integer', Rule::in($adviseeIds)],
|
'student_ids' => ['required', 'array', 'min:1'],
|
||||||
'topic' => ['nullable', 'string', 'max:150'],
|
'student_ids.*' => ['integer', Rule::exists('students', 'id')],
|
||||||
'notes' => ['nullable', 'string'],
|
|
||||||
'session_date' => ['nullable', 'date'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'student_id' => ['required', 'integer', Rule::exists('students', 'id')],
|
|
||||||
'lecturer_id' => ['required', 'integer', Rule::exists('lecturers', 'id')],
|
'lecturer_id' => ['required', 'integer', Rule::exists('lecturers', 'id')],
|
||||||
'topic' => ['nullable', 'string', 'max:150'],
|
'topic' => ['required', 'string', 'max:150'],
|
||||||
'notes' => ['nullable', 'string'],
|
'notes' => ['nullable', 'string'],
|
||||||
'session_date' => ['nullable', 'date'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function messages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'student_id.in' => 'Mahasiswa yang dipilih bukan mahasiswa bimbingan Anda.',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,22 +6,16 @@
|
|||||||
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;
|
||||||
|
|
||||||
protected function casts(): array
|
public function students(): BelongsToMany
|
||||||
{
|
{
|
||||||
return [
|
return $this->belongsToMany(Student::class);
|
||||||
'session_date' => 'datetime',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function student(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Student::class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function lecturer(): BelongsTo
|
public function lecturer(): BelongsTo
|
||||||
|
|||||||
@ -3,61 +3,53 @@
|
|||||||
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', 'student_id', 'lecturer_id', 'topic', 'notes', 'session_date', 'created_at'])
|
->select(['id', 'lecturer_id', 'topic', 'notes', 'created_at'])
|
||||||
->with([
|
->with([
|
||||||
'student.user.profile',
|
'students.user.profile',
|
||||||
'student.department',
|
'students.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('student', fn ($q) => $q->where('student_number', 'like', "%{$search}%")
|
->orWhereHas('students', 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
|
||||||
{
|
{
|
||||||
return AcademicAdvisingLog::create([
|
$log = 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,11 +10,9 @@ 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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
<?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,25 +9,30 @@ class AcademicAdvisingLogSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
AcademicAdvisingLog::insert([
|
$logs = [
|
||||||
[
|
[
|
||||||
'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,6 +50,8 @@ 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,7 +54,6 @@ 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';
|
||||||
|
|
||||||
@ -62,11 +61,9 @@ 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);
|
||||||
@ -206,13 +203,11 @@ function buildNavMain({
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(isDosen || can('view-academic-advising-logs')
|
...(can('view-academic-advising-logs')
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: 'Bimbingan Akademik',
|
name: 'Bimbingan Akademik',
|
||||||
url: isDosen
|
url: academicAdvisingLogsRoute.url(),
|
||||||
? lecturerAcademicAdvisingLogsRoute.url()
|
|
||||||
: academicAdvisingLogsRoute.url(),
|
|
||||||
icon: MessageCircle,
|
icon: MessageCircle,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -297,11 +292,9 @@ 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,7 +1,3 @@
|
|||||||
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';
|
||||||
@ -31,6 +27,10 @@ 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,6 +155,7 @@ 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>
|
||||||
@ -163,6 +164,7 @@ 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 { format } from 'date-fns';
|
import { Eye, Pencil, Trash2 } from 'lucide-react';
|
||||||
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,25 +16,43 @@ type CreateColumnsParams = {
|
|||||||
export function createAcademicAdvisingLogColumns(
|
export function createAcademicAdvisingLogColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<AcademicAdvisingLog>[] {
|
): ColumnDef<AcademicAdvisingLog>[] {
|
||||||
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
const { handleView, handleEdit, handleDeleteClick, canUpdate, canDelete } =
|
||||||
|
params;
|
||||||
|
|
||||||
const columns: ColumnDef<AcademicAdvisingLog>[] = [
|
const columns: ColumnDef<AcademicAdvisingLog>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'student.student_number',
|
accessorKey: 'students',
|
||||||
header: () => <span>Mahasiswa</span>,
|
header: () => <span>Mahasiswa</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const student = row.original.student;
|
const students = row.original.students;
|
||||||
|
|
||||||
|
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>
|
<div className="space-y-1">
|
||||||
|
{visible.map((student) => (
|
||||||
|
<div key={student.id}>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{student?.user?.profile?.full_name ?? 'N/A'}
|
{student.user?.profile?.full_name ??
|
||||||
|
'N/A'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{student?.student_number} ·{' '}
|
{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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -61,20 +79,8 @@ 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>,
|
||||||
@ -85,6 +91,11 @@ 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" />,
|
||||||
@ -103,7 +114,6 @@ 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,12 +13,22 @@ 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';
|
||||||
@ -71,6 +81,7 @@ 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');
|
||||||
@ -117,6 +128,7 @@ 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,
|
||||||
@ -172,6 +184,16 @@ 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}
|
||||||
@ -199,7 +221,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.student?.user?.profile?.full_name ?? 'mahasiswa ini'}"? Tindakan ini tidak dapat dibatalkan.`
|
`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.`
|
||||||
}
|
}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
/>
|
/>
|
||||||
@ -208,6 +230,84 @@ 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,
|
||||||
@ -219,49 +319,23 @@ 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>
|
||||||
@ -274,7 +348,10 @@ function AdvisingLogFields({
|
|||||||
<Combobox
|
<Combobox
|
||||||
items={lecturers}
|
items={lecturers}
|
||||||
value={lecturer}
|
value={lecturer}
|
||||||
onValueChange={setLecturer}
|
onValueChange={(value) => {
|
||||||
|
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}
|
||||||
>
|
>
|
||||||
@ -296,7 +373,86 @@ function AdvisingLogFields({
|
|||||||
<InputError message={errors.lecturer_id} />
|
<InputError message={errors.lecturer_id} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor={editing ? 'edit-topic' : 'topic'}>Topik</Label>
|
<div className="flex items-center justify-between">
|
||||||
|
<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"
|
||||||
@ -317,13 +473,6 @@ 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,6 +1,3 @@
|
|||||||
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';
|
||||||
@ -16,15 +13,18 @@ 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 { createStudentColumns } from './columns';
|
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 type { Student } from './columns';
|
||||||
|
import { createStudentColumns } 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,6 +210,7 @@ export default function StudentIndex({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{canUpdateAcademicStatus && (
|
||||||
<Alert>
|
<Alert>
|
||||||
<Info />
|
<Info />
|
||||||
<AlertTitle>Ubah status mahasiswa</AlertTitle>
|
<AlertTitle>Ubah status mahasiswa</AlertTitle>
|
||||||
@ -218,6 +219,7 @@ export default function StudentIndex({
|
|||||||
mahasiswa secara langsung.
|
mahasiswa secara langsung.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -9,17 +9,16 @@ 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;
|
||||||
student_id: number;
|
students: AcademicAdvisingLogStudent[];
|
||||||
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,11 +139,6 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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