Deploy Laravel rebuild via Coolify

Point Docker and Coolify compose to the Laravel rebuild app so mahasiswa, dosen, and admin flows are served from the new Laravel public entrypoint.
This commit is contained in:
Power BI Dev
2026-05-03 18:50:29 +07:00
parent 89ce9d30a7
commit dab8ea396b
107 changed files with 17544 additions and 20 deletions

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class AdminLegacyController extends Controller
{
public function __invoke(Request $request): RedirectResponse
{
$auth = $request->session()->get('legacy_auth');
abort_unless(($auth['role'] ?? null) === 'admin', 403);
$query = $request->query();
$legacyUrl = 'http://127.0.0.1:8080/admin/dashboard.php';
if (! empty($query)) {
$legacyUrl .= '?'.http_build_query($query);
}
return redirect()->away($legacyUrl);
}
}

View File

@@ -0,0 +1,801 @@
<?php
namespace App\Http\Controllers;
use App\Support\AdminNavigation;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class AdminPageController extends Controller
{
public function mahasiswa(Request $request): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
$rows = DB::table('tbmhs as tm')
->leftJoin('tbprodi as tp', 'tm.idProdi', '=', 'tp.idProdi')
->select(['tm.idmhs', 'tm.nim', 'tm.nmLengkap', 'tp.nmProdi', 'tm.thnmasuk', 'tm.status'])
->where('tm.idProdi', $user['prodi'])
->orderByDesc('tm.idmhs')
->paginate(20);
return $this->table($request, 'Data Mahasiswa', 'Data mahasiswa pada program studi admin aktif.', ['NIM', 'Nama', 'Prodi', 'Angkatan', 'Status'], $rows, fn ($row) => [$row->nim, $row->nmLengkap, $row->nmProdi, $row->thnmasuk ?: '-', $row->status], 'admin.data.mahasiswa', [
'actions' => [
['label' => 'Tambah Mahasiswa', 'href' => route('admin.data.mahasiswa.create', [], false), 'variant' => 'dark'],
['label' => 'Impor CSV Mahasiswa', 'href' => route('admin.data.mahasiswa.import', [], false), 'variant' => 'dark'],
],
'rowActions' => fn ($row) => [
['label' => 'Edit', 'href' => route('admin.data.mahasiswa.edit', ['id' => $row->idmhs], false)],
['label' => 'Hapus', 'href' => route('admin.data.mahasiswa.destroy', ['id' => $row->idmhs], false), 'method' => 'DELETE', 'confirm' => 'Hapus mahasiswa ini?'],
],
]);
}
public function createMahasiswa(Request $request): View
{
return $this->resourceForm($request, 'Tambah Mahasiswa', 'Tambah data mahasiswa pada program studi admin.', route('admin.data.mahasiswa.store'), route('admin.data.mahasiswa', [], false), $this->mahasiswaFields());
}
public function storeMahasiswa(Request $request): RedirectResponse
{
$user = $this->user($request);
$data = $request->validate([
'nim' => ['required', 'string', 'max:30'],
'nmLengkap' => ['required', 'string', 'max:255'],
'email' => ['nullable', 'email', 'max:255'],
'thnmasuk' => ['nullable', 'string', 'max:10'],
'password' => ['required', 'string', 'min:4'],
'status' => ['required', 'in:A,N,P'],
'bolehUploadDraft' => ['required', 'in:0,1'],
'noHP' => ['nullable', 'string', 'max:30'],
'noHPOrtu' => ['nullable', 'string', 'max:30'],
]);
$data['idProdi'] = $user['prodi'];
$data['password'] = md5($data['password']);
DB::table('tbmhs')->insert($data);
return redirect()->route('admin.data.mahasiswa')->with('success', 'Data mahasiswa berhasil ditambahkan.');
}
public function editMahasiswa(Request $request, int $id): View
{
$user = $this->user($request);
$row = DB::table('tbmhs')->where('idmhs', $id)->where('idProdi', $user['prodi'])->first();
abort_unless($row, 404);
return $this->resourceForm($request, 'Edit Mahasiswa', 'Perbarui data mahasiswa.', route('admin.data.mahasiswa.update', ['id' => $id]), route('admin.data.mahasiswa', [], false), $this->mahasiswaFields($row, true), 'PUT');
}
public function updateMahasiswa(Request $request, int $id): RedirectResponse
{
$user = $this->user($request);
abort_unless(DB::table('tbmhs')->where('idmhs', $id)->where('idProdi', $user['prodi'])->exists(), 404);
$data = $request->validate([
'nim' => ['required', 'string', 'max:30'],
'nmLengkap' => ['required', 'string', 'max:255'],
'email' => ['nullable', 'email', 'max:255'],
'thnmasuk' => ['nullable', 'string', 'max:10'],
'password' => ['nullable', 'string', 'min:4'],
'status' => ['required', 'in:A,N,P'],
'bolehUploadDraft' => ['required', 'in:0,1'],
'noHP' => ['nullable', 'string', 'max:30'],
'noHPOrtu' => ['nullable', 'string', 'max:30'],
]);
if (($data['password'] ?? '') !== '') {
$data['password'] = md5($data['password']);
} else {
unset($data['password']);
}
DB::table('tbmhs')->where('idmhs', $id)->update($data);
return redirect()->route('admin.data.mahasiswa')->with('success', 'Data mahasiswa berhasil diperbarui.');
}
public function destroyMahasiswa(Request $request, int $id): RedirectResponse
{
$user = $this->user($request);
DB::table('tbmhs')->where('idmhs', $id)->where('idProdi', $user['prodi'])->delete();
return redirect()->route('admin.data.mahasiswa')->with('success', 'Data mahasiswa berhasil dihapus.');
}
public function importMahasiswa(Request $request): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
return view('admin.pages.mahasiswa-import', [
'title' => 'Impor CSV Mahasiswa | SPOTA Rebuild',
'pageTitle' => 'Impor CSV Mahasiswa',
'pageDescription' => 'Upload CSV untuk menambah atau memperbarui data mahasiswa secara bulk berdasarkan NIM.',
'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'),
'sidebar' => AdminNavigation::build($user),
'user' => $user,
'requiredHeaders' => ['nim', 'nama', 'email', 'thnmasuk'],
'optionalHeaders' => ['password', 'status', 'noHP', 'noHPOrtu', 'bolehUploadDraft'],
]);
}
public function storeImportMahasiswa(Request $request): RedirectResponse
{
$user = $this->user($request);
$this->abortIfSuper($user);
$validated = $request->validate([
'csv' => ['required', 'file', 'mimes:csv,txt', 'max:5120'],
'mode' => ['required', 'in:insert_only,upsert'],
'default_password' => ['nullable', 'string', 'min:4', 'max:50'],
]);
$path = $request->file('csv')->getRealPath();
$handle = fopen($path, 'r');
if (! $handle) {
return back()->with('error', 'File CSV tidak dapat dibaca.');
}
$headers = fgetcsv($handle);
if (! $headers) {
fclose($handle);
return back()->with('error', 'CSV kosong atau header tidak ditemukan.');
}
$headers = array_map(fn ($header) => trim((string) $header), $headers);
$normalized = array_map(fn ($header) => strtolower($header), $headers);
$aliases = [
'nama_lengkap' => 'nama',
'nmlengkap' => 'nama',
'tahunmasuk' => 'thnmasuk',
'tahun_masuk' => 'thnmasuk',
'nohp' => 'noHP',
'no_hp' => 'noHP',
'nohportu' => 'noHPOrtu',
'no_hp_ortu' => 'noHPOrtu',
'bolehuploaddraft' => 'bolehUploadDraft',
'boleh_upload_draft' => 'bolehUploadDraft',
];
$canonical = [];
foreach ($normalized as $index => $header) {
$canonical[$index] = $aliases[$header] ?? $headers[$index];
}
$required = ['nim', 'nama', 'email', 'thnmasuk'];
$missing = array_diff($required, $canonical);
if (! empty($missing)) {
fclose($handle);
return back()->with('error', 'Header CSV wajib belum lengkap: '.implode(', ', $missing).'.');
}
$inserted = 0;
$updated = 0;
$skipped = 0;
$errors = [];
$line = 1;
$defaultPassword = $validated['default_password'] ?: '123456';
DB::beginTransaction();
try {
while (($row = fgetcsv($handle)) !== false) {
$line++;
$data = [];
foreach ($canonical as $index => $header) {
$data[$header] = trim((string) ($row[$index] ?? ''));
}
if (count(array_filter($data, fn ($value) => $value !== '')) === 0) {
continue;
}
$nim = $data['nim'] ?? '';
$nama = $data['nama'] ?? '';
$email = $data['email'] ?? '';
$thnmasuk = $data['thnmasuk'] ?? '';
if ($nim === '' || $nama === '' || $email === '' || $thnmasuk === '') {
$skipped++;
$errors[] = 'Baris '.$line.': nim, nama, email, dan thnmasuk wajib diisi.';
continue;
}
if (! preg_match('/^[A-Za-z0-9._-]+$/', $nim)) {
$skipped++;
$errors[] = 'Baris '.$line.': NIM tidak valid.';
continue;
}
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
$skipped++;
$errors[] = 'Baris '.$line.': email tidak valid.';
continue;
}
$payload = [
'nim' => $nim,
'nmLengkap' => $nama,
'email' => $email,
'idProdi' => $user['prodi'],
'thnmasuk' => $thnmasuk,
'status' => ($data['status'] ?? '') !== '' ? $data['status'] : 'A',
'noHP' => $data['noHP'] ?? '',
'noHPOrtu' => $data['noHPOrtu'] ?? '',
'bolehUploadDraft' => ($data['bolehUploadDraft'] ?? '') !== '' ? $data['bolehUploadDraft'] : '1',
];
$existing = DB::table('tbmhs')
->where('nim', $nim)
->where('idProdi', $user['prodi'])
->first();
if ($existing && $validated['mode'] === 'insert_only') {
$skipped++;
continue;
}
if ($existing) {
if (($data['password'] ?? '') !== '') {
$payload['password'] = md5($data['password']);
}
DB::table('tbmhs')->where('idmhs', $existing->idmhs)->update($payload);
$updated++;
} else {
$payload['password'] = md5(($data['password'] ?? '') !== '' ? $data['password'] : $defaultPassword);
DB::table('tbmhs')->insert($payload);
$inserted++;
}
}
DB::commit();
} catch (\Throwable $exception) {
DB::rollBack();
fclose($handle);
return back()->with('error', 'Import gagal: '.$exception->getMessage());
}
fclose($handle);
return redirect()
->route('admin.data.mahasiswa')
->with('success', 'Import selesai. Baru: '.$inserted.', diperbarui: '.$updated.', dilewati: '.$skipped.'.'.($errors ? ' Catatan: '.implode(' ', array_slice($errors, 0, 5)) : ''));
}
public function dosen(Request $request): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
$rows = DB::table('tbdosen as td')
->leftJoin('tbprodi as tp', 'td.idProdi', '=', 'tp.idProdi')
->select(['td.iddosen', 'td.nip', 'td.nmLengkap', 'td.email', 'tp.nmProdi', 'td.status'])
->where('td.idProdi', $user['prodi'])
->orderBy('td.nmLengkap')
->paginate(20);
return $this->table($request, 'Data Dosen', 'Data dosen pada program studi admin aktif.', ['NIP', 'Nama', 'Email', 'Prodi', 'Status'], $rows, fn ($row) => [$row->nip, $row->nmLengkap, $row->email ?: '-', $row->nmProdi, $row->status], 'admin.data.dosen', [
'actions' => [
['label' => 'Tambah Dosen', 'href' => route('admin.data.dosen.create', [], false), 'variant' => 'dark'],
],
'rowActions' => fn ($row) => [
['label' => 'Edit', 'href' => route('admin.data.dosen.edit', ['id' => $row->iddosen], false)],
['label' => 'Hapus', 'href' => route('admin.data.dosen.destroy', ['id' => $row->iddosen], false), 'method' => 'DELETE', 'confirm' => 'Hapus dosen ini?'],
],
]);
}
public function createDosen(Request $request): View
{
return $this->resourceForm($request, 'Tambah Dosen', 'Tambah data dosen pada program studi admin.', route('admin.data.dosen.store'), route('admin.data.dosen', [], false), $this->dosenFields());
}
public function storeDosen(Request $request): RedirectResponse
{
$user = $this->user($request);
$data = $request->validate([
'nip' => ['required', 'string', 'max:30'],
'nmLengkap' => ['required', 'string', 'max:255'],
'email' => ['nullable', 'email', 'max:255'],
'nohp' => ['nullable', 'string', 'max:30'],
'jenis' => ['required', 'string', 'max:10'],
'password' => ['required', 'string', 'min:4'],
'status' => ['required', 'in:A,N,P'],
]);
$data['idProdi'] = $user['prodi'];
$data['password'] = md5($data['password']);
DB::table('tbdosen')->insert($data);
return redirect()->route('admin.data.dosen')->with('success', 'Data dosen berhasil ditambahkan.');
}
public function editDosen(Request $request, int $id): View
{
$user = $this->user($request);
$row = DB::table('tbdosen')->where('iddosen', $id)->where('idProdi', $user['prodi'])->first();
abort_unless($row, 404);
return $this->resourceForm($request, 'Edit Dosen', 'Perbarui data dosen.', route('admin.data.dosen.update', ['id' => $id]), route('admin.data.dosen', [], false), $this->dosenFields($row, true), 'PUT');
}
public function updateDosen(Request $request, int $id): RedirectResponse
{
$user = $this->user($request);
abort_unless(DB::table('tbdosen')->where('iddosen', $id)->where('idProdi', $user['prodi'])->exists(), 404);
$data = $request->validate([
'nip' => ['required', 'string', 'max:30'],
'nmLengkap' => ['required', 'string', 'max:255'],
'email' => ['nullable', 'email', 'max:255'],
'nohp' => ['nullable', 'string', 'max:30'],
'jenis' => ['required', 'string', 'max:10'],
'password' => ['nullable', 'string', 'min:4'],
'status' => ['required', 'in:A,N,P'],
]);
if (($data['password'] ?? '') !== '') {
$data['password'] = md5($data['password']);
} else {
unset($data['password']);
}
DB::table('tbdosen')->where('iddosen', $id)->update($data);
return redirect()->route('admin.data.dosen')->with('success', 'Data dosen berhasil diperbarui.');
}
public function destroyDosen(Request $request, int $id): RedirectResponse
{
$user = $this->user($request);
DB::table('tbdosen')->where('iddosen', $id)->where('idProdi', $user['prodi'])->delete();
return redirect()->route('admin.data.dosen')->with('success', 'Data dosen berhasil dihapus.');
}
public function kk(Request $request): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
$rows = DB::table('tb_kelompok_keahlian as kk')
->leftJoin('tbdosen as ketua', 'kk.ketuaKK', '=', 'ketua.iddosen')
->leftJoin('tbdosen as sekretaris', 'kk.sekretarisKK', '=', 'sekretaris.iddosen')
->select(['kk.idKK', 'kk.namaKK', 'kk.warnaLabel', 'ketua.nmLengkap as ketua', 'sekretaris.nmLengkap as sekretaris'])
->orderBy('kk.namaKK')
->paginate(20);
return $this->table($request, 'Data Kelompok Keahlian', 'Daftar kelompok keahlian yang dipakai pada praoutline dan penawaran judul.', ['ID', 'Nama KK', 'Label', 'Ketua', 'Sekretaris'], $rows, fn ($row) => [$row->idKK, $row->namaKK, $row->warnaLabel ?: '-', $row->ketua ?: '-', $row->sekretaris ?: '-'], 'admin.data.kk', [
'actions' => [
['label' => 'Tambah KK', 'href' => route('admin.data.kk.create', [], false), 'variant' => 'dark'],
],
'rowActions' => fn ($row) => [
['label' => 'Edit', 'href' => route('admin.data.kk.edit', ['id' => $row->idKK], false)],
['label' => 'Hapus', 'href' => route('admin.data.kk.destroy', ['id' => $row->idKK], false), 'method' => 'DELETE', 'confirm' => 'Hapus kelompok keahlian ini?'],
],
]);
}
public function createKk(Request $request): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
return $this->resourceForm($request, 'Tambah Kelompok Keahlian', 'Tambah data kelompok keahlian.', route('admin.data.kk.store'), route('admin.data.kk', [], false), $this->kkFields());
}
public function storeKk(Request $request): RedirectResponse
{
$user = $this->user($request);
$this->abortIfSuper($user);
$data = $request->validate([
'namaKK' => ['required', 'string', 'max:255'],
'warnaLabel' => ['nullable', 'string', 'max:50'],
'ketuaKK' => ['nullable', 'integer'],
'sekretarisKK' => ['nullable', 'integer'],
]);
DB::table('tb_kelompok_keahlian')->insert($data);
return redirect()->route('admin.data.kk')->with('success', 'Data KK berhasil ditambahkan.');
}
public function editKk(Request $request, int $id): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
$row = DB::table('tb_kelompok_keahlian')->where('idKK', $id)->first();
abort_unless($row, 404);
return $this->resourceForm($request, 'Edit Kelompok Keahlian', 'Perbarui data kelompok keahlian.', route('admin.data.kk.update', ['id' => $id]), route('admin.data.kk', [], false), $this->kkFields($row), 'PUT');
}
public function updateKk(Request $request, int $id): RedirectResponse
{
$user = $this->user($request);
$this->abortIfSuper($user);
$data = $request->validate([
'namaKK' => ['required', 'string', 'max:255'],
'warnaLabel' => ['nullable', 'string', 'max:50'],
'ketuaKK' => ['nullable', 'integer'],
'sekretarisKK' => ['nullable', 'integer'],
]);
DB::table('tb_kelompok_keahlian')->where('idKK', $id)->update($data);
return redirect()->route('admin.data.kk')->with('success', 'Data KK berhasil diperbarui.');
}
public function destroyKk(Request $request, int $id): RedirectResponse
{
$user = $this->user($request);
$this->abortIfSuper($user);
DB::table('tb_kelompok_keahlian')->where('idKK', $id)->delete();
return redirect()->route('admin.data.kk')->with('success', 'Data KK berhasil dihapus.');
}
public function fakultas(Request $request): View
{
$user = $this->user($request);
$this->abortUnlessSuper($user);
$rows = DB::table('tbfakultas')->select(['idFak', 'nmFakultas'])->orderBy('nmFakultas')->paginate(20);
return $this->table($request, 'Data Fakultas', 'Data fakultas untuk superadmin.', ['ID', 'Nama Fakultas'], $rows, fn ($row) => [$row->idFak, $row->nmFakultas], 'admin.data.fakultas');
}
public function jurusan(Request $request): View
{
$user = $this->user($request);
$this->abortUnlessSuper($user);
$rows = DB::table('tbjurusan as tj')
->leftJoin('tbfakultas as tf', 'tj.idFak', '=', 'tf.idFak')
->select(['tj.idJur', 'tj.nmJurusan', 'tf.nmFakultas'])
->orderBy('tj.nmJurusan')
->paginate(20);
return $this->table($request, 'Data Jurusan', 'Data jurusan untuk superadmin.', ['ID', 'Jurusan', 'Fakultas'], $rows, fn ($row) => [$row->idJur, $row->nmJurusan, $row->nmFakultas ?: '-'], 'admin.data.jurusan');
}
public function prodi(Request $request): View
{
$user = $this->user($request);
$this->abortUnlessSuper($user);
$rows = DB::table('tbprodi as tp')
->leftJoin('tbjurusan as tj', 'tp.idJur', '=', 'tj.idJur')
->leftJoin('tbfakultas as tf', 'tp.idFak', '=', 'tf.idFak')
->select(['tp.idProdi', 'tp.nmProdi', 'tj.nmJurusan', 'tf.nmFakultas'])
->orderBy('tp.nmProdi')
->paginate(20);
return $this->table($request, 'Data Program Studi', 'Data program studi untuk superadmin.', ['ID', 'Program Studi', 'Jurusan', 'Fakultas'], $rows, fn ($row) => [$row->idProdi, $row->nmProdi, $row->nmJurusan ?: '-', $row->nmFakultas ?: '-'], 'admin.data.prodi');
}
public function praoutline(Request $request): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
$rows = $this->praoutlineBase($user)
->orderByDesc('tp.id')
->paginate(20);
return $this->table($request, 'Daftar Draft Praoutline', 'Draft praoutline terbaru pada program studi admin.', ['ID', 'Mahasiswa', 'Judul', 'Tanggal Upload', 'Status'], $rows, fn ($row) => [$row->id, $row->mahasiswa.' ('.$row->nim.')', $row->judul, $this->formatDateTime(trim($row->tgl_upload.' '.$row->wkt_upload)), $this->statusLabel($row->status_usulan)], 'admin.praoutline.index');
}
public function praoutlineSearch(Request $request): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
$keyword = trim((string) $request->query('q'));
$query = $this->praoutlineBase($user)->orderByDesc('tp.id');
if ($keyword !== '') {
$query->where(function ($query) use ($keyword) {
$query->where('tp.judul', 'like', '%'.$keyword.'%')
->orWhere('tm.nmLengkap', 'like', '%'.$keyword.'%')
->orWhere('tp.nim', 'like', '%'.$keyword.'%');
});
}
$rows = $query->paginate(20)->withQueryString();
return $this->table($request, 'Pencarian Praoutline', 'Cari draft berdasarkan judul, nama mahasiswa, atau NIM.', ['ID', 'Mahasiswa', 'Judul', 'Tanggal Upload', 'Status'], $rows, fn ($row) => [$row->id, $row->mahasiswa.' ('.$row->nim.')', $row->judul, $this->formatDateTime(trim($row->tgl_upload.' '.$row->wkt_upload)), $this->statusLabel($row->status_usulan)], 'admin.praoutline.search', ['search' => true, 'keyword' => $keyword]);
}
public function keputusan(Request $request): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
$rows = DB::table('tbrekaphasil as trh')
->leftJoin('tbmhs as tm', 'trh.nim', '=', 'tm.nim')
->leftJoin('tbpraoutline as tp', 'trh.idpraoutline', '=', 'tp.id')
->select(['trh.id', 'trh.nim', 'tm.nmLengkap as mahasiswa', 'tp.judul', 'trh.kep_akhir', 'trh.tgl'])
->where('trh.idProdi', $user['prodi'])
->orderByDesc('trh.id')
->paginate(20);
return $this->table($request, 'Kep. Penunjukan Dosen', 'Rekap keputusan dan penunjukan dosen dari data praoutline.', ['ID', 'Mahasiswa', 'Judul', 'Keputusan', 'Tanggal'], $rows, fn ($row) => [$row->id, ($row->mahasiswa ?: '-').' ('.$row->nim.')', $row->judul ?: '-', $this->statusLabel($row->kep_akhir), $this->formatDateTime($row->tgl)], 'admin.praoutline.keputusan');
}
public function kepDraft(Request $request): View
{
return $this->keputusan($request)->with('pageTitle', 'Kep. Draft Praoutline');
}
public function pemberitahuan(Request $request): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
$rows = DB::table('tmp_notif_r as tn')
->select(['tn.id', 'tn.msg', 'tn.tgl', 'tn.jns_usr', 'tn.idkonten', 'tn.read'])
->where('tn.idProdi', $user['prodi'])
->orderByDesc('tn.id')
->paginate(20);
return $this->table($request, 'Pemberitahuan', 'Pemberitahuan praoutline yang tercatat pada sistem.', ['ID', 'Isi', 'Jenis User', 'Konten', 'Status', 'Tanggal'], $rows, fn ($row) => [$row->id, $row->msg, $row->jns_usr, $row->idkonten, $row->read === 'Y' ? 'Dibaca' : 'Belum Dibaca', $this->formatDateTime($row->tgl)], 'admin.praoutline.pemberitahuan');
}
public function pengumuman(Request $request): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
$rows = DB::table('tbpengumuman')
->select(['id', 'judul', 'tujuan', 'publish', 'tgl'])
->where('idProdi', $user['prodi'])
->orderByDesc('id')
->paginate(20);
return $this->table($request, 'Daftar Pengumuman', 'Pengumuman program studi yang ditampilkan di SPOTA.', ['ID', 'Judul', 'Tujuan', 'Publish', 'Tanggal'], $rows, fn ($row) => [$row->id, $row->judul, $row->tujuan, $row->publish, $this->formatDateTime($row->tgl)], 'admin.pengumuman.index', ['actions' => [['label' => 'Buat Pengumuman Baru', 'href' => route('admin.pengumuman.create', [], false), 'variant' => 'dark']]]);
}
public function createPengumuman(Request $request): View
{
return $this->form($request, 'Buat Pengumuman Baru', 'Tambah pengumuman untuk mahasiswa, dosen, atau seluruh pengguna program studi.', 'admin.pengumuman.create');
}
public function storePengumuman(Request $request): RedirectResponse
{
$user = $this->user($request);
$this->abortIfSuper($user);
$validated = $request->validate([
'judul' => ['required', 'string', 'max:255'],
'isi' => ['required', 'string'],
'tujuan' => ['required', 'in:A,M,D'],
'publish' => ['nullable', 'in:Y,N'],
]);
DB::table('tbpengumuman')->insert([
'idProdi' => $user['prodi'],
'judul' => $validated['judul'],
'isi' => $validated['isi'],
'tujuan' => $validated['tujuan'],
'tgl' => Carbon::now()->toDateTimeString(),
'author' => $user['id'],
'publish' => $validated['publish'] ?? 'Y',
]);
return redirect()->route('admin.pengumuman.index')->with('success', 'Pengumuman berhasil dibuat.');
}
public function jadwal(Request $request): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
$rows = DB::table('tbjadwal as tj')
->leftJoin('tbmhs as tm', 'tj.idMhs', '=', 'tm.idmhs')
->select(['tj.id', 'tj.jenis', 'tj.start', 'tj.ruangan', 'tj.publish', 'tm.nmLengkap as mahasiswa'])
->where('tj.idProdi', $user['prodi'])
->orderByDesc('tj.start')
->paginate(20);
return $this->table($request, 'Manajemen Data Jadwal', 'Data jadwal seminar/sidang program studi.', ['ID', 'Jenis', 'Mahasiswa', 'Tanggal', 'Ruangan', 'Publish'], $rows, fn ($row) => [$row->id, $row->jenis, $row->mahasiswa ?: '-', $this->formatDateTime($row->start), $row->ruangan ?: '-', $row->publish], 'admin.jadwal.index');
}
public function kalender(Request $request): View
{
return $this->jadwal($request)->with('pageTitle', 'Kalender Seminar/Sidang');
}
public function profile(Request $request): View
{
$user = $this->user($request);
$rows = collect([(object) [
'username' => $user['username'],
'nama' => $user['nama_lengkap'],
'jabatan' => $user['jabatan'] ?: '-',
'email' => $user['email'] ?: '-',
'prodi' => $user['nmprodi'],
'level' => $user['lvl'] === 'S' ? 'Super Admin' : 'Admin Prodi',
]]);
return $this->staticTable($request, 'Profil Saya', 'Profil admin yang sedang login.', ['Username', 'Nama', 'Jabatan', 'Email', 'Prodi', 'Level'], $rows, fn ($row) => [$row->username, $row->nama, $row->jabatan, $row->email, $row->prodi, $row->level], 'admin.profile');
}
public function users(Request $request): View
{
$user = $this->user($request);
$this->abortUnlessSuper($user);
$rows = DB::table('tbadmin as ta')
->leftJoin('tbprodi as tp', 'ta.idProdi', '=', 'tp.idProdi')
->select(['ta.username', 'ta.nmLengkap', 'ta.jenisAdmin', 'ta.aktif', 'tp.nmProdi'])
->orderBy('ta.username')
->paginate(20);
return $this->table($request, 'Manajemen Admin', 'Daftar akun administrator SPOTA.', ['Username', 'Nama', 'Level', 'Prodi', 'Aktif'], $rows, fn ($row) => [$row->username, $row->nmLengkap, $row->jenisAdmin, $row->nmProdi ?: 'Semua', $row->aktif], 'admin.users');
}
public function pengaturan(Request $request): View
{
$user = $this->user($request);
$this->abortIfSuper($user);
$rows = DB::table('web_setting')
->select(['name', 'values'])
->where('idProdi', $user['prodi'])
->orderBy('name')
->paginate(20);
return $this->table($request, 'Pengaturan Prodi', 'Pengaturan aktif program studi pada SPOTA.', ['Nama', 'Nilai'], $rows, fn ($row) => [$row->name, $row->values], 'admin.pengaturan');
}
private function table(Request $request, string $title, string $description, array $columns, mixed $rows, callable $map, string $activeRoute, array $extra = []): View
{
$user = $this->user($request);
return view('admin.pages.table', array_merge([
'title' => $title.' | SPOTA Rebuild',
'pageTitle' => $title,
'pageDescription' => $description,
'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'),
'sidebar' => AdminNavigation::build($user),
'user' => $user,
'columns' => $columns,
'rows' => $rows,
'map' => $map,
], $extra));
}
private function staticTable(Request $request, string $title, string $description, array $columns, mixed $rows, callable $map, string $activeRoute): View
{
return $this->table($request, $title, $description, $columns, $rows, $map, $activeRoute);
}
private function form(Request $request, string $title, string $description, string $activeRoute): View
{
$user = $this->user($request);
return view('admin.pages.pengumuman-form', [
'title' => $title.' | SPOTA Rebuild',
'pageTitle' => $title,
'pageDescription' => $description,
'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'),
'sidebar' => AdminNavigation::build($user),
'user' => $user,
]);
}
private function resourceForm(Request $request, string $title, string $description, string $action, string $cancel, array $fields, string $method = 'POST'): View
{
$user = $this->user($request);
return view('admin.pages.resource-form', [
'title' => $title.' | SPOTA Rebuild',
'pageTitle' => $title,
'pageDescription' => $description,
'sidebar' => AdminNavigation::build($user),
'user' => $user,
'action' => $action,
'cancel' => $cancel,
'fields' => $fields,
'method' => $method,
]);
}
private function mahasiswaFields(?object $row = null, bool $editing = false): array
{
return [
['name' => 'nim', 'label' => 'NIM', 'value' => $row->nim ?? '', 'required' => true],
['name' => 'nmLengkap', 'label' => 'Nama Lengkap', 'value' => $row->nmLengkap ?? '', 'required' => true],
['name' => 'email', 'label' => 'Email', 'type' => 'email', 'value' => $row->email ?? ''],
['name' => 'thnmasuk', 'label' => 'Tahun Masuk', 'value' => $row->thnmasuk ?? ''],
['name' => 'password', 'label' => 'Password', 'type' => 'password', 'required' => ! $editing, 'help' => $editing ? 'Kosongkan jika tidak mengganti password.' : 'Password awal mahasiswa.'],
['name' => 'status', 'label' => 'Status', 'type' => 'select', 'value' => $row->status ?? 'A', 'options' => ['A' => 'Aktif', 'P' => 'Pending', 'N' => 'Nonaktif']],
['name' => 'bolehUploadDraft', 'label' => 'Boleh Upload Draft', 'type' => 'select', 'value' => $row->bolehUploadDraft ?? '1', 'options' => ['1' => 'Ya', '0' => 'Tidak']],
['name' => 'noHP', 'label' => 'No HP', 'value' => $row->noHP ?? ''],
['name' => 'noHPOrtu', 'label' => 'No HP Orang Tua', 'value' => $row->noHPOrtu ?? ''],
];
}
private function dosenFields(?object $row = null, bool $editing = false): array
{
return [
['name' => 'nip', 'label' => 'NIP', 'value' => $row->nip ?? '', 'required' => true],
['name' => 'nmLengkap', 'label' => 'Nama Lengkap', 'value' => $row->nmLengkap ?? '', 'required' => true],
['name' => 'email', 'label' => 'Email', 'type' => 'email', 'value' => $row->email ?? ''],
['name' => 'nohp', 'label' => 'No HP', 'value' => $row->nohp ?? ''],
['name' => 'jenis', 'label' => 'Jenis/Jabatan Dosen', 'value' => $row->jenis ?? 'D'],
['name' => 'password', 'label' => 'Password', 'type' => 'password', 'required' => ! $editing, 'help' => $editing ? 'Kosongkan jika tidak mengganti password.' : 'Password awal dosen.'],
['name' => 'status', 'label' => 'Status', 'type' => 'select', 'value' => $row->status ?? 'A', 'options' => ['A' => 'Aktif', 'P' => 'Pending', 'N' => 'Nonaktif']],
];
}
private function kkFields(?object $row = null): array
{
$dosen = DB::table('tbdosen')->select(['iddosen', 'nmLengkap'])->orderBy('nmLengkap')->get();
$options = ['' => '- Pilih Dosen -'];
foreach ($dosen as $item) {
$options[$item->iddosen] = $item->nmLengkap;
}
return [
['name' => 'namaKK', 'label' => 'Nama Kelompok Keahlian', 'value' => $row->namaKK ?? '', 'required' => true],
['name' => 'warnaLabel', 'label' => 'Warna Label', 'value' => $row->warnaLabel ?? '', 'help' => 'Opsional, mengikuti data legacy.'],
['name' => 'ketuaKK', 'label' => 'Ketua KK', 'type' => 'select', 'value' => $row->ketuaKK ?? '', 'options' => $options],
['name' => 'sekretarisKK', 'label' => 'Sekretaris KK', 'type' => 'select', 'value' => $row->sekretarisKK ?? '', 'options' => $options],
];
}
private function praoutlineBase(array $user)
{
return DB::table('tbpraoutline as tp')
->leftJoin('tbmhs as tm', 'tp.nim', '=', 'tm.nim')
->select(['tp.id', 'tp.nim', 'tm.nmLengkap as mahasiswa', 'tp.judul', 'tp.tgl_upload', 'tp.wkt_upload', 'tp.status_usulan'])
->where('tp.idProdi', $user['prodi']);
}
private function user(Request $request): array
{
$auth = $request->session()->get('legacy_auth');
abort_unless(($auth['role'] ?? null) === 'admin', 403);
return $auth['user'];
}
private function abortIfSuper(array $user): void
{
abort_if(($user['lvl'] ?? null) === 'S', 403);
}
private function abortUnlessSuper(array $user): void
{
abort_unless(($user['lvl'] ?? null) === 'S', 403);
}
private function statusLabel(?string $status): string
{
return match ($status) {
'0' => 'Dalam Review',
'1' => 'Disetujui',
'2' => 'Ditolak',
default => $status ?? '-',
};
}
private function formatDateTime(?string $value): string
{
if (! $value || trim($value) === '') {
return '-';
}
return Carbon::parse($value)->locale('id')->translatedFormat('j F Y, H:i');
}
}

View File

@@ -0,0 +1,272 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class LegacyAuthController extends Controller
{
public function showRoleLogin(): View
{
return view('auth.role-login', [
'title' => 'Masuk ke SPOTA Rebuild',
'intro' => [
'eyebrow' => 'Portal Masuk SPOTA',
'heading' => 'Satu halaman masuk untuk memahami SPOTA sebelum memilih akun.',
'description' => 'SPOTA adalah Sistem Pendukung Outline Tugas Akhir yang dipakai di Informatika UNTAN untuk mengelola alur usulan judul, review dosen, penawaran judul, bimbingan, pengumuman, dan monitoring progres mahasiswa.',
'details' => [
'Mahasiswa menggunakan SPOTA untuk mengusulkan judul, melihat tanggapan dosen, memantau status, dan mengikuti jadwal akademik terkait tugas akhir.',
'Dosen menggunakan SPOTA untuk meninjau usulan, memantau mahasiswa bimbingan, mengelola penawaran judul, dan melihat peringatan progres studi.',
'Admin menggunakan SPOTA untuk mengelola data dasar, praoutline, pengumuman, jadwal seminar/sidang, dan pengaturan program studi.',
'Halaman masuk rebuild ini tetap terhubung ke data aktif sistem lama agar transisi ke tampilan baru tidak memutus alur kerja yang sudah berjalan.',
],
],
'roles' => [
['slug' => 'mahasiswa', 'name' => 'Masuk sebagai Mahasiswa', 'summary' => 'Untuk akses usulan judul, status review, dan jadwal terkait tugas akhir.'],
['slug' => 'dosen', 'name' => 'Masuk sebagai Dosen', 'summary' => 'Untuk akses review usulan, penawaran judul, bimbingan, dan early warning.'],
['slug' => 'admin', 'name' => 'Masuk sebagai Admin', 'summary' => 'Untuk akses manajemen data, praoutline, pengumuman, jadwal, dan pengaturan prodi.'],
],
]);
}
public function showLegacyLogin(string $role): View
{
abort_unless(in_array($role, ['mahasiswa', 'dosen', 'admin'], true), 404);
$roleMeta = [
'mahasiswa' => [
'title' => 'Login Mahasiswa | SPOTA Rebuild',
'heading' => 'Masuk sebagai Mahasiswa',
'eyebrow' => 'Akses Mahasiswa',
'description' => 'Gunakan akun SPOTA mahasiswa yang sudah ada. Dashboard setelah login akan memakai data real dari basis data legacy.',
'input' => 'NIM',
'placeholder' => 'Masukkan NIM',
'status' => 'Login aktif',
'status_note' => 'Autentikasi mahasiswa sudah terhubung ke data SPOTA lama.',
'summary_title' => 'SPOTA untuk Mahasiswa',
'summary_points' => [
'Mengelola usulan judul tugas akhir dan membaca tanggapan dosen pembimbing atau reviewer.',
'Memantau status proses outline, hasil review, dan informasi penting yang berhubungan dengan tugas akhir.',
'Menggunakan akun mahasiswa yang sama dengan sistem SPOTA sebelumnya agar tidak perlu registrasi ulang.',
],
'help_note' => 'Masukkan NIM dan password akun SPOTA mahasiswa yang sudah aktif.',
],
'dosen' => [
'title' => 'Login Dosen | SPOTA Rebuild',
'heading' => 'Masuk sebagai Dosen',
'eyebrow' => 'Akses Dosen',
'description' => 'Gunakan akun SPOTA dosen yang sudah aktif. Dashboard setelah login akan menampilkan ringkasan review, mahasiswa binaan, dan jadwal terdekat.',
'input' => 'NIP',
'placeholder' => 'Masukkan NIP',
'status' => 'Login aktif',
'status_note' => 'Autentikasi dosen sudah terhubung ke data SPOTA lama.',
'summary_title' => 'SPOTA untuk Dosen',
'summary_points' => [
'Meninjau usulan judul, membaca diskusi review, dan memantau antrean bimbingan mahasiswa.',
'Mengelola penawaran judul, melihat pengumuman, dan memantau early warning progres studi.',
'Tetap memakai akun dosen pada sistem SPOTA yang sudah aktif sebelumnya.',
],
'help_note' => 'Masukkan NIP dan password akun dosen yang aktif pada SPOTA.',
],
'admin' => [
'title' => 'Login Admin | SPOTA Rebuild',
'heading' => 'Masuk sebagai Admin',
'eyebrow' => 'Akses Admin',
'description' => 'Gunakan akun admin SPOTA aktif. Dashboard admin rebuild menjaga link dan fungsi tetap mengikuti halaman administrator lama.',
'input' => 'Username',
'placeholder' => 'Masukkan username',
'status' => 'Login aktif',
'status_note' => 'Autentikasi admin sudah terhubung ke tabel tbadmin legacy.',
'summary_title' => 'SPOTA untuk Admin',
'summary_points' => [
'Mengelola data mahasiswa, dosen, kelompok keahlian, dan data akademik sesuai hak akses admin.',
'Mengelola praoutline, pengumuman, jadwal seminar/sidang, pengguna admin, dan pengaturan prodi.',
'Fungsi detail tetap diarahkan ke modul legacy agar tidak memutus proses yang sudah berjalan.',
],
'help_note' => 'Masukkan username dan password admin SPOTA yang aktif.',
],
];
return view('auth.legacy-login', [
'role' => $role,
'meta' => $roleMeta[$role],
]);
}
public function authenticate(Request $request, string $role): RedirectResponse
{
abort_unless(in_array($role, ['mahasiswa', 'dosen', 'admin'], true), 404);
$credentials = $request->validate([
'identifier' => ['required', 'string'],
'password' => ['required', 'string'],
]);
if ($role === 'mahasiswa') {
$mahasiswa = DB::table('tbmhs as tm')
->leftJoin('tbprodi as tp', 'tm.idProdi', '=', 'tp.idProdi')
->select([
'tm.nim',
'tm.idmhs',
'tm.password',
'tm.nmLengkap',
'tm.idProdi',
'tp.nmProdi',
'tm.status',
])
->where('tm.nim', $credentials['identifier'])
->whereIn('tm.status', ['A', 'P'])
->first();
if (! $mahasiswa) {
return back()->withInput()->withErrors([
'identifier' => 'Username Anda tidak terdaftar.',
]);
}
if ($mahasiswa->password !== md5($credentials['password'])) {
return back()->withInput()->withErrors([
'password' => 'Password anda tidak sesuai atau salah.',
]);
}
$request->session()->regenerate();
$request->session()->put('legacy_auth', [
'role' => 'mahasiswa',
'user' => [
'nim' => $mahasiswa->nim,
'prodi' => (string) $mahasiswa->idProdi,
'nmprodi' => $mahasiswa->nmProdi,
'nama_lengkap' => $mahasiswa->nmLengkap,
'id' => (string) $mahasiswa->idmhs,
'status' => $mahasiswa->status,
],
]);
return redirect()->route('dashboard.mahasiswa');
}
if ($role === 'dosen') {
$dosen = DB::table('tbdosen as td')
->leftJoin('tbprodi as tp', 'td.idProdi', '=', 'tp.idProdi')
->select([
'td.iddosen',
'td.nip',
'td.password',
'td.nmLengkap',
'td.email',
'td.idProdi',
'td.kelompokKeahlian',
'td.jenis',
'td.status',
'tp.nmProdi',
])
->where('td.nip', $credentials['identifier'])
->where('td.status', 'A')
->first();
if (! $dosen) {
return back()->withInput()->withErrors([
'identifier' => 'Username Anda tidak terdaftar.',
]);
}
if ($credentials['identifier'] === '0000' || $credentials['identifier'] === '123456') {
return back()->withInput()->withErrors([
'identifier' => 'Akun dummy tidak dapat digunakan untuk login.',
]);
}
if ($dosen->password !== md5($credentials['password'])) {
return back()->withInput()->withErrors([
'password' => 'Password anda tidak sesuai atau salah.',
]);
}
$request->session()->regenerate();
$request->session()->put('legacy_auth', [
'role' => 'dosen',
'user' => [
'nip' => $dosen->nip,
'prodi' => (string) $dosen->idProdi,
'nmprodi' => $dosen->nmProdi,
'nama_lengkap' => $dosen->nmLengkap,
'id' => (string) $dosen->iddosen,
'jenisdosen' => $dosen->jenis,
'kelompokKeahlian' => $dosen->kelompokKeahlian,
'email' => $dosen->email,
'status' => $dosen->status,
],
]);
return redirect()->route('dashboard.dosen');
}
$admin = DB::table('tbadmin as ta')
->leftJoin('tbprodi as tp', 'ta.idProdi', '=', 'tp.idProdi')
->select([
'ta.idAdmin',
'ta.username',
'ta.password',
'ta.nmLengkap',
'ta.jabatan',
'ta.email',
'ta.idProdi',
'ta.jenisAdmin',
'ta.aktif',
'tp.nmProdi',
])
->where('ta.username', $credentials['identifier'])
->where('ta.aktif', 'Y')
->first();
if (! $admin) {
return back()->withInput()->withErrors([
'identifier' => 'Username Anda tidak terdaftar.',
]);
}
if (strtolower($credentials['identifier']) === 'dummyadmin') {
return back()->withInput()->withErrors([
'identifier' => 'Akun dummy tidak dapat digunakan untuk login.',
]);
}
if ($admin->password !== md5($credentials['password'])) {
return back()->withInput()->withErrors([
'password' => 'Password anda tidak sesuai atau salah.',
]);
}
$request->session()->regenerate();
$request->session()->put('legacy_auth', [
'role' => 'admin',
'user' => [
'username' => $admin->username,
'prodi' => (string) $admin->idProdi,
'nmprodi' => $admin->nmProdi ?: 'Semua Program Studi',
'lvl' => $admin->jenisAdmin,
'nama_lengkap' => $admin->nmLengkap,
'id' => (string) $admin->idAdmin,
'jabatan' => $admin->jabatan,
'email' => $admin->email,
'status' => $admin->aktif,
],
]);
return redirect()->route('dashboard.admin');
}
public function logout(Request $request): RedirectResponse
{
$request->session()->forget('legacy_auth');
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('role-login');
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use App\Support\AdminNavigation;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class AdminDashboardController extends Controller
{
public function __invoke(Request $request): View
{
$auth = $request->session()->get('legacy_auth');
abort_unless(($auth['role'] ?? null) === 'admin', 403);
$user = $auth['user'];
$isSuper = ($user['lvl'] ?? null) === 'S';
$stats = $isSuper ? [
['label' => 'Fakultas', 'value' => (string) DB::table('tbfakultas')->count(), 'note' => 'Total data fakultas pada basis data SPOTA.'],
['label' => 'Jurusan', 'value' => (string) DB::table('tbjurusan')->count(), 'note' => 'Total data jurusan lintas fakultas.'],
['label' => 'Program Studi', 'value' => (string) DB::table('tbprodi')->count(), 'note' => 'Total program studi yang tercatat.'],
['label' => 'Admin Aktif', 'value' => (string) DB::table('tbadmin')->where('aktif', 'Y')->count(), 'note' => 'Akun administrator aktif.'],
] : [
['label' => 'Mahasiswa', 'value' => (string) DB::table('tbmhs')->where('idProdi', $user['prodi'])->count(), 'note' => 'Data mahasiswa pada program studi admin.'],
['label' => 'Dosen', 'value' => (string) DB::table('tbdosen')->where('idProdi', $user['prodi'])->count(), 'note' => 'Data dosen pada program studi admin.'],
['label' => 'Draft Praoutline', 'value' => (string) DB::table('tbpraoutline')->where('idProdi', $user['prodi'])->count(), 'note' => 'Total draft praoutline program studi.'],
['label' => 'Jadwal Publish', 'value' => (string) DB::table('tbjadwal')->where('idProdi', $user['prodi'])->where('publish', 'Y')->count(), 'note' => 'Jadwal seminar/sidang yang dipublikasi.'],
];
$schedules = DB::table('tbjadwal as tj')
->leftJoin('tbmhs as tm', 'tj.idMhs', '=', 'tm.idmhs')
->select(['tj.jenis', 'tj.start', 'tj.ruangan', 'tm.nmLengkap as mahasiswa'])
->when(! $isSuper, fn ($query) => $query->where('tj.idProdi', $user['prodi']))
->whereNotNull('tj.start')
->orderByDesc('tj.start')
->limit(6)
->get()
->map(fn ($item) => [
'jenis' => $this->scheduleLabel($item->jenis),
'tanggal' => $this->formatDateTime($item->start),
'ruangan' => $item->ruangan ?: '-',
'mahasiswa' => $item->mahasiswa ?: '-',
])
->all();
return view('dashboard.admin', [
'title' => 'Dashboard Admin | SPOTA Rebuild',
'user' => $user,
'sidebar' => AdminNavigation::build($user),
'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'),
'stats' => $stats,
'schedules' => $schedules,
]);
}
private function scheduleLabel(?string $jenis): string
{
return match ($jenis) {
'Sidang' => 'Sidang',
'Outline' => 'Outline',
'SidHas' => 'Seminar Hasil',
default => $jenis ?? 'Jadwal',
};
}
private function formatDateTime(?string $value): string
{
if (! $value) {
return '-';
}
return Carbon::parse($value)->locale('id')->translatedFormat('j F Y, H:i');
}
}

View File

@@ -0,0 +1,274 @@
<?php
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use App\Support\DosenNavigation;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class DosenDashboardController extends Controller
{
public function __invoke(Request $request): View
{
$auth = $request->session()->get('legacy_auth');
abort_unless(($auth['role'] ?? null) === 'dosen', 403);
$user = $auth['user'];
$cachePrefix = 'rebuild:dosen:dashboard:'.$user['prodi'].':'.$user['id'];
$today = now();
$startOfMonth = $today->copy()->startOfMonth();
$endOfMonth = $today->copy()->endOfMonth();
$unreadAnnouncements = Cache::remember($cachePrefix.':unread-announcements', 60, function () use ($user) {
return DB::table('tbpengumuman')
->where('idProdi', $user['prodi'])
->whereIn('tujuan', ['A', 'D'])
->whereNotIn('id', function ($query) use ($user) {
$query->select('idkonten')
->from('tmp_notif')
->where('iduser', $user['id'])
->where('typeuser', 'D')
->where('jenis', 'P');
})
->count();
});
$unreadProposals = Cache::remember($cachePrefix.':unread-proposals', 60, function () use ($user) {
return DB::table('tbpraoutline')
->where('status_usulan', '0')
->whereNotIn('id', function ($query) use ($user) {
$query->select('idkonten')
->from('tmp_notif')
->where('iduser', $user['id'])
->where('typeuser', 'D')
->where('jenis', 'J');
})
->count();
});
$scheduleRows = Cache::remember($cachePrefix.':calendar:'.$startOfMonth->format('Y-m'), 60, function () use ($user, $startOfMonth, $endOfMonth) {
return DB::table('tbjadwal as tj')
->leftJoin('tbmhs as tm', 'tm.idmhs', '=', 'tj.idmhs')
->select([
'tj.id',
'tj.jenis',
'tj.start',
'tm.nmLengkap',
])
->where('tj.publish', 'Y')
->where('tj.idProdi', $user['prodi'])
->whereBetween('tj.start', [$startOfMonth->toDateTimeString(), $endOfMonth->copy()->endOfDay()->toDateTimeString()])
->orderBy('tj.start')
->get();
});
$upcomingSchedules = Cache::remember($cachePrefix.':upcoming', 60, function () use ($user, $today) {
return DB::table('tbjadwal as tj')
->leftJoin('tbmhs as tm', 'tm.idmhs', '=', 'tj.idmhs')
->select([
'tj.id',
'tj.jenis',
'tj.start',
'tj.ruangan',
'tj.judul',
'tm.nim',
'tm.nmLengkap',
])
->where('tj.publish', 'Y')
->where('tj.idProdi', $user['prodi'])
->whereNotNull('tj.start')
->where('tj.start', '>=', $today->copy()->startOfDay()->toDateTimeString())
->orderBy('tj.start')
->limit(5)
->get()
->map(fn ($row) => [
'id' => $row->id,
'jenis' => $this->scheduleLabel($row->jenis),
'jenisClass' => $this->scheduleBadgeClass($row->jenis),
'nama' => $row->nmLengkap ?: 'Mahasiswa tidak ditemukan',
'nim' => $row->nim ?: '-',
'judul' => $row->judul ?: 'Judul belum tersedia',
'ruangan' => $row->ruangan ?: '-',
'tanggal' => $this->formatIndonesianDateTime($row->start),
])
->all();
});
$calendar = $this->buildCalendar($scheduleRows);
$dashboard = [
'dateLabel' => $this->formatIndonesianDateTime(now()->toDateTimeString()),
'pageTitle' => 'Dashboard',
'sidebar' => DosenNavigation::build('dashboard.dosen'),
'menus' => [
['title' => 'Dashboard', 'href' => route('dashboard.dosen'), 'icon' => 'home', 'active' => true],
],
'legacyMenus' => [
'Utama' => [
['title' => 'Penawaran Judul', 'href' => $this->legacyUrl('dosen/dashboard.php?page=penawaran&menu=list-judul-saya'), 'icon' => 'briefcase'],
],
'Tugas Akhir 1' => [
['title' => 'Daftar Usulan', 'href' => $this->legacyUrl('dosen/dashboard.php?page=praoutline&menu=new'), 'icon' => 'folder'],
['title' => 'Review Saya', 'href' => $this->legacyUrl('dosen/dashboard.php?page=praoutline&menu=myreview'), 'icon' => 'chat'],
['title' => 'Pencarian Usulan', 'href' => $this->legacyUrl('dosen/dashboard.php?page=praoutline&menu=cari'), 'icon' => 'search'],
['title' => 'Daftar Bimbingan Saya', 'href' => $this->legacyUrl('dosen/dashboard.php?page=praoutline&menu=keputusan'), 'icon' => 'users'],
['title' => 'Statistik Usulan', 'href' => $this->legacyUrl('dosen/dashboard.php?page=praoutline&menu=statistik'), 'icon' => 'chart'],
['title' => 'Pemberitahuan', 'href' => $this->legacyUrl('dosen/dashboard.php?page=praoutline&menu=pemberitahuan'), 'icon' => 'bell'],
],
'Lainnya' => [
['title' => 'Pengumuman', 'href' => $this->legacyUrl('dosen/dashboard.php?page=pengumuman'), 'icon' => 'megaphone'],
['title' => 'Akun Pengguna', 'href' => $this->legacyUrl('dosen/dashboard.php?page=user&menu=my-profile'), 'icon' => 'user'],
['title' => 'Dokumen Sidang', 'href' => 'https://edoxid.untan.ac.id/', 'icon' => 'document'],
['title' => 'Early Warning', 'href' => $this->legacyUrl('dosen/dashboard.php?page=early-warning'), 'icon' => 'warning'],
['title' => 'Konsultasi Skripsi', 'href' => 'https://spota.untan.ac.id/konsultasi/', 'icon' => 'chat'],
['title' => 'Statistik Seminar', 'href' => 'https://spota.untan.ac.id/cek_banyak_sidang.php', 'icon' => 'chart'],
['title' => 'Konsultasi KP', 'href' => 'https://informatika.untan.ac.id/konsultasi/', 'icon' => 'chat'],
['title' => 'Pra LIRS (Dosen PA)', 'href' => $this->legacyUrl('dosen/dashboard.php?page=pra-lirs'), 'icon' => 'clipboard'],
['title' => 'Evaluasi Mahasiswa', 'href' => 'https://spota.untan.ac.id/steven/rekapMahasiswaEvaluasi.php?angkatan='.(date('Y') - 5).'&show=belumlulus', 'icon' => 'chart'],
],
],
'welcomeTitle' => 'Yth. Bapak/Ibu '.$user['nama_lengkap'],
'welcomeText' => 'Selamat datang di Sistem Pendukung Outline Tugas Akhir (SPOTA) Universitas Tanjungpura',
'androidLink' => $user['prodi'] === '2' ? $this->legacyUrl('spotaif.apk') : null,
'announcementNotice' => [
'count' => $unreadAnnouncements,
'title' => 'Pengumuman Terbaru',
'message' => $unreadAnnouncements > 0
? 'Terdapat '.$unreadAnnouncements.' Pengumuman Terbaru Yang Belum Dibaca'
: 'Tidak Ada Pengumuman Terbaru',
'primaryLabel' => 'Lihat Semua Pengumuman',
'primaryHref' => $this->legacyUrl('dosen/dashboard.php?page=pengumuman'),
],
'proposalNotice' => [
'count' => $unreadProposals,
'title' => 'Usulan Terbaru',
'message' => $unreadProposals > 0
? 'Terdapat '.$unreadProposals.' Usulan Terbaru.'
: 'Tidak terdapat Usulan terbaru.',
'primaryLabel' => $unreadProposals > 0 ? 'Lihat Usulan Terbaru' : 'Lihat Semua Usulan',
'primaryHref' => $this->legacyUrl('dosen/dashboard.php?page=praoutline&menu=new'),
'secondaryLabel' => $unreadProposals > 0 ? null : 'Cari Usulan',
'secondaryHref' => $unreadProposals > 0 ? null : $this->legacyUrl('dosen/dashboard.php?page=praoutline&menu=cari'),
],
'calendar' => $calendar,
'upcomingSchedules' => $upcomingSchedules,
];
return view('dashboard.dosen', [
'title' => 'Dashboard Dosen | SPOTA Rebuild',
'dashboard' => $dashboard,
'user' => $user,
]);
}
private function buildCalendar($scheduleRows): array
{
$today = now();
$startOfMonth = $today->copy()->startOfMonth();
$endOfMonth = $today->copy()->endOfMonth();
$startOfGrid = $startOfMonth->copy()->startOfWeek(Carbon::MONDAY);
$endOfGrid = $endOfMonth->copy()->endOfWeek(Carbon::SUNDAY);
$eventsByDate = [];
foreach ($scheduleRows as $row) {
if (! $row->start || strtotime($row->start) === false) {
continue;
}
$dateKey = Carbon::parse($row->start)->toDateString();
$eventsByDate[$dateKey][] = [
'id' => $row->id,
'title' => ucwords(strtolower((string) ($row->nmLengkap ?: 'Tanpa Nama'))),
'jenis' => $this->scheduleLabel($row->jenis),
'className' => $this->scheduleEventClass($row->jenis),
];
}
$weeks = [];
$cursor = $startOfGrid->copy();
while ($cursor->lessThanOrEqualTo($endOfGrid)) {
$week = [];
for ($day = 0; $day < 7; $day++) {
$dateKey = $cursor->toDateString();
$week[] = [
'date' => $dateKey,
'day' => $cursor->day,
'isCurrentMonth' => $cursor->month === $today->month,
'isToday' => $cursor->isToday(),
'events' => $eventsByDate[$dateKey] ?? [],
];
$cursor->addDay();
}
$weeks[] = $week;
}
return [
'monthLabel' => $this->formatMonthYear($today),
'weekdays' => ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'],
'weeks' => $weeks,
];
}
private function scheduleBadgeClass(?string $jenis): string
{
return match ($jenis) {
'Sidang' => 'bg-orange-100 text-orange-700',
'Outline' => 'bg-emerald-100 text-emerald-700',
'SidHas' => 'bg-amber-100 text-amber-700',
default => 'bg-slate-200 text-slate-700',
};
}
private function scheduleLabel(?string $jenis): string
{
return match ($jenis) {
'Sidang' => 'Sidang',
'Outline' => 'Outline',
'SidHas' => 'Seminar Hasil',
default => $jenis ?? 'Jadwal',
};
}
private function scheduleEventClass(?string $jenis): string
{
return match ($jenis) {
'Sidang' => 'bg-orange-100 text-orange-700 border-orange-200',
'Outline' => 'bg-emerald-100 text-emerald-700 border-emerald-200',
'SidHas' => 'bg-amber-100 text-amber-700 border-amber-200',
default => 'bg-slate-100 text-slate-700 border-slate-200',
};
}
private function formatIndonesianDateTime(?string $value): string
{
if (! $value) {
return '-';
}
return Carbon::parse($value)->locale('id')->translatedFormat('j F Y, H:i');
}
private function formatMonthYear(Carbon $date): string
{
return $date->copy()->locale('id')->translatedFormat('F Y');
}
private function legacyUrl(string $path): string
{
$baseUrl = rtrim((string) env('LEGACY_BASE_URL', 'http://127.0.0.1:8080'), '/');
return $baseUrl.'/'.ltrim($path, '/');
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class MahasiswaDashboardController extends Controller
{
public function __invoke(Request $request): View
{
$auth = $request->session()->get('legacy_auth');
abort_unless(($auth['role'] ?? null) === 'mahasiswa', 403);
$user = $auth['user'];
$latestPraoutline = DB::table('tbpraoutline as tp')
->leftJoin('tbrekaphasil as trh', 'tp.id', '=', 'trh.idpraoutline')
->select(['tp.id', 'tp.judul', 'tp.status_usulan', 'trh.ket'])
->where('tp.nim', $user['nim'])
->where('tp.idProdi', $user['prodi'])
->orderByDesc('tp.id')
->first();
$unreadAnnouncements = DB::table('tbpengumuman')
->where('idProdi', $user['prodi'])
->whereIn('tujuan', ['A', 'M'])
->whereNotIn('id', function ($query) use ($user) {
$query->select('idkonten')
->from('tmp_notif')
->where('iduser', $user['id'])
->where('typeuser', 'M')
->where('jenis', 'P');
})
->count();
$publishedSchedules = DB::table('tbjadwal')
->where('idProdi', $user['prodi'])
->where('publish', 'Y')
->count();
$nextSchedule = DB::table('tbjadwal')
->select(['jenis', 'start', 'ruangan'])
->where('idMhs', $user['id'])
->where('publish', 'Y')
->whereNotNull('start')
->orderBy('start')
->first();
$activeDrafts = DB::table('tbpraoutline')
->where('nim', $user['nim'])
->whereIn('status_usulan', ['0', '1'])
->count();
$statusAlert = match ($latestPraoutline?->status_usulan) {
'0' => [
'title' => 'Draft Praoutline Masih Dalam Proses Review',
'description' => 'Silakan cek menu review untuk melihat tanggapan dosen terhadap draft yang sedang berjalan.',
'button' => 'Lihat Review',
'buttonHref' => route('mahasiswa.status-usulan', [], false),
'class' => 'border-sky-200 bg-sky-50 text-sky-900',
'buttonClass' => 'bg-sky-600 text-white hover:opacity-90',
],
'1' => [
'title' => 'Draft Praoutline Anda Telah Disetujui',
'description' => 'Judul terakhir sudah memperoleh persetujuan. Silakan pantau tahapan berikutnya pada modul yang terkait.',
'button' => 'Lihat Putusan',
'buttonHref' => route('mahasiswa.status-usulan', [], false),
'class' => 'border-emerald-200 bg-emerald-50 text-emerald-900',
'buttonClass' => 'bg-emerald-600 text-white hover:opacity-90',
],
'2' => [
'title' => 'Judul yang Anda Ajukan Tidak Disetujui',
'description' => $latestPraoutline?->ket ?: 'Silakan periksa catatan keputusan dan siapkan upload judul baru.',
'button' => 'Lihat Catatan Putusan',
'buttonHref' => route('mahasiswa.status-usulan', [], false),
'class' => 'border-rose-200 bg-rose-50 text-rose-900',
'buttonClass' => 'bg-rose-600 text-white hover:opacity-90',
],
default => [
'title' => 'Belum Ada Draft Praoutline Aktif',
'description' => 'Mahasiswa ini belum memiliki usulan judul aktif yang tercatat pada data SPOTA saat ini.',
'button' => 'Ajukan Outline Baru',
'buttonHref' => route('mahasiswa.praoutline.upload', [], false),
'class' => 'border-slate-200 bg-slate-50 text-slate-900',
'buttonClass' => 'bg-[#15171A] text-white hover:opacity-90',
],
};
$announcementAlert = [
'title' => 'Pengumuman Terbaru',
'description' => $unreadAnnouncements > 0
? 'Terdapat '.$unreadAnnouncements.' pengumuman program studi yang belum dibaca.'
: 'Tidak terdapat pengumuman baru yang perlu dibaca saat ini.',
'button' => 'Lihat Semua Pengumuman',
'buttonHref' => route('mahasiswa.pengumuman.index', [], false),
'class' => 'border-amber-200 bg-amber-50 text-amber-900',
'buttonClass' => 'bg-amber-400 text-[#15171A] hover:bg-amber-300',
];
$dashboard = [
'eyebrow' => 'Dashboard Mahasiswa',
'title' => 'Halo, '.$user['nama_lengkap'].'.',
'description' => 'Selamat datang di Sistem Pendukung Outline Tugas Akhir (SPOTA) Universitas Tanjungpura.',
'menus' => [
['title' => 'Dashboard', 'href' => route('dashboard.mahasiswa'), 'icon' => 'home', 'active' => true],
['title' => 'Informasi', 'icon' => 'folder', 'children' => [
['title' => 'Status Usulan', 'href' => route('mahasiswa.status-usulan', [], false), 'icon' => 'chart'],
['title' => 'Ajukan Outline Baru', 'href' => route('mahasiswa.praoutline.upload', [], false), 'icon' => 'clipboard'],
['title' => 'Penawaran Judul', 'href' => route('mahasiswa.penawaran.index', [], false), 'icon' => 'briefcase'],
['title' => 'Pengumuman', 'href' => route('mahasiswa.pengumuman.index', [], false), 'icon' => 'bell'],
['title' => 'Jadwal Terdekat', 'href' => '#jadwal', 'icon' => 'clock'],
]],
],
'stats' => [
['label' => 'Status Usulan Terakhir', 'value' => $this->statusLabel($latestPraoutline?->status_usulan), 'delta' => $latestPraoutline ? 'Draft #'.$latestPraoutline->id : 'Belum ada draft', 'deltaClass' => $this->statusDeltaClass($latestPraoutline?->status_usulan), 'note' => $latestPraoutline?->judul ? $latestPraoutline->judul : 'Belum ada judul yang tercatat di sistem.'],
['label' => 'Pengumuman Belum Dibaca', 'value' => (string) $unreadAnnouncements, 'delta' => $unreadAnnouncements > 0 ? 'Perlu dibaca' : 'Sudah aman', 'deltaClass' => $unreadAnnouncements > 0 ? 'bg-amber-100 text-amber-800' : 'bg-emerald-100 text-emerald-800', 'note' => 'Jumlah pengumuman program studi yang belum dibaca.'],
['label' => 'Jadwal Terdekat', 'value' => $nextSchedule ? $this->scheduleLabel($nextSchedule->jenis) : '-', 'delta' => $nextSchedule ? 'Terjadwal' : 'Belum ada jadwal', 'deltaClass' => $nextSchedule ? 'bg-sky-100 text-sky-800' : 'bg-slate-200 text-slate-700', 'note' => $nextSchedule ? $this->formatDateTime($nextSchedule->start).' · '.$nextSchedule->ruangan : 'Belum ada jadwal seminar yang dipublikasikan untuk mahasiswa ini.'],
['label' => 'Draft Aktif', 'value' => (string) $activeDrafts, 'delta' => $activeDrafts > 0 ? 'Masih berjalan' : 'Tidak ada', 'deltaClass' => $activeDrafts > 0 ? 'bg-violet-100 text-violet-800' : 'bg-slate-200 text-slate-700', 'note' => 'Jumlah draft yang masih aktif pada data SPOTA.'],
],
'latestTitle' => $latestPraoutline?->judul,
'statusAlert' => $statusAlert,
'announcementAlert' => $announcementAlert,
'nextSchedule' => $nextSchedule ? [
'jenis' => $this->scheduleLabel($nextSchedule->jenis),
'tanggal' => $this->formatDateTime($nextSchedule->start),
'ruangan' => $nextSchedule->ruangan ?: '-',
] : null,
'publishedSchedules' => $publishedSchedules,
];
return view('dashboard.mahasiswa', [
'title' => 'Dashboard Mahasiswa | SPOTA Rebuild',
'dashboard' => $dashboard,
'user' => $user,
]);
}
private function statusLabel(?string $status): string
{
return match ($status) {
'0' => 'Dalam Review',
'1' => 'Disetujui',
'2' => 'Ditolak',
default => 'Belum Ada Draft',
};
}
private function statusDeltaClass(?string $status): string
{
return match ($status) {
'0' => 'bg-sky-100 text-sky-800',
'1' => 'bg-emerald-100 text-emerald-800',
'2' => 'bg-rose-100 text-rose-800',
default => 'bg-slate-200 text-slate-700',
};
}
private function scheduleLabel(?string $jenis): string
{
return match ($jenis) {
'Sidang' => 'Sidang',
'Outline' => 'Outline',
'SidHas' => 'Seminar Hasil',
default => $jenis ?? 'Jadwal',
};
}
private function formatDateTime(?string $value): string
{
if (! $value) {
return '-';
}
return Carbon::parse($value)->locale('id')->translatedFormat('j F Y, H:i');
}
}

View File

@@ -0,0 +1,962 @@
<?php
namespace App\Http\Controllers;
use App\Support\DosenNavigation;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class DosenPageController extends Controller
{
public function penawaran(Request $request): View
{
$user = $this->dosenUser($request);
$source = (string) $request->query('sumber', '0');
$statusFilter = (string) $request->query('status', 'Semua');
$kkFilter = (string) $request->query('kk', 'all');
$kkOptions = DB::table('tb_kelompok_keahlian')
->where('idKK', '!=', '8')
->orderBy('namaKK')
->get(['idKK', 'namaKK'])
->map(fn ($item) => [
'value' => (string) $item->idKK,
'label' => $item->namaKK,
])
->all();
$penawaranQuery = DB::table('tb_penawaran_judul as tpj')
->leftJoin('tb_ambil_judul as taj', function ($join) {
$join->on('taj.idPenawaranAmbil', '=', 'tpj.idPenawaran')
->whereRaw('taj.waktuPengambilan = (SELECT MAX(t2.waktuPengambilan) FROM tb_ambil_judul t2 WHERE t2.idPenawaranAmbil = tpj.idPenawaran)');
})
->leftJoin('tbmhs as tm', 'tm.idmhs', '=', 'taj.idMhs')
->leftJoin('tb_kelompok_keahlian as tkk', 'tkk.idKK', '=', 'tpj.kk')
->leftJoin('tbdosen as td', 'td.iddosen', '=', 'tpj.idDosen')
->select([
'tpj.idPenawaran',
'tpj.idDosen',
'tpj.judul',
'tpj.deskripsi',
'tpj.waktuInput',
'taj.statusPengambilan',
'tm.nim',
'tm.nmLengkap',
'tkk.namaKK',
'td.nmLengkap as namaDosen',
])
->where('td.idProdi', $user['prodi']);
if ($source === '0') {
$penawaranQuery->where('tpj.idDosen', $user['id']);
}
if ($kkFilter !== 'all') {
$penawaranQuery->where('tpj.kk', $kkFilter);
}
if ($statusFilter === 'Belum Diambil') {
$penawaranQuery->where(function ($query) {
$query->whereNull('taj.statusPengambilan')
->orWhere('taj.statusPengambilan', '2');
});
} elseif ($statusFilter === 'Belum Diproses') {
$penawaranQuery->where('taj.statusPengambilan', '0');
} elseif ($statusFilter === 'Diterima') {
$penawaranQuery->where('taj.statusPengambilan', '1');
}
$penawaran = $penawaranQuery
->orderByDesc('tpj.waktuInput')
->limit(100)
->get()
->map(function ($item) use ($user) {
$takenBy = $item->nmLengkap ? $item->nmLengkap.' ('.$item->nim.')' : '-';
if ((string) $item->statusPengambilan === '2') {
$takenBy = '-';
}
return [
'id' => $item->idPenawaran,
'judul' => $item->judul,
'deskripsi' => $item->deskripsi ?: '-',
'waktu' => $this->formatDateTime($item->waktuInput),
'kk' => $item->namaKK ?: '-',
'ditawarkanOleh' => $item->namaDosen ?: '-',
'mahasiswa' => $takenBy,
'status' => $this->statusPenawaran($item->statusPengambilan),
'editHref' => route('dosen.penawaran.edit', $item->idPenawaran),
'isMine' => (string) $item->idDosen === (string) $user['id'],
'destroyHref' => route('dosen.penawaran.destroy', $item->idPenawaran),
'approveHref' => route('dosen.penawaran.approve', $item->idPenawaran),
'rejectHref' => route('dosen.penawaran.reject', $item->idPenawaran),
'canApprove' => (string) $item->statusPengambilan === '0',
];
})
->all();
return $this->renderPage('dosen.pages.penawaran', [
'title' => 'Penawaran Judul | SPOTA Rebuild',
'pageTitle' => 'Penawaran Judul',
'pageDescription' => 'Daftar penawaran judul milik dosen yang sudah masuk pada sistem lama.',
'routeName' => 'dosen.penawaran.index',
'user' => $user,
'penawaran' => $penawaran,
'source' => $source,
'statusFilter' => $statusFilter,
'kkFilter' => $kkFilter,
'kkOptions' => $kkOptions,
'pageActions' => [
['label' => 'Tambah Data', 'href' => route('dosen.penawaran.create'), 'variant' => 'dark'],
],
]);
}
public function createPenawaran(Request $request): View
{
$user = $this->dosenUser($request);
return $this->renderPage('dosen.pages.penawaran-form', [
'title' => 'Tambah Penawaran Judul | SPOTA Rebuild',
'pageTitle' => 'Tambah Data Penawaran Judul',
'pageDescription' => 'Tambahkan data penawaran judul baru langsung dari halaman rebuild.',
'routeName' => 'dosen.penawaran.index',
'user' => $user,
'formMode' => 'create',
'formAction' => route('dosen.penawaran.store'),
'formMethod' => 'POST',
'penawaranItem' => [
'judul' => old('judul_penawaran', ''),
'deskripsi' => old('keterangan_penawaran', ''),
],
]);
}
public function storePenawaran(Request $request): RedirectResponse
{
$user = $this->dosenUser($request);
$data = $request->validate([
'judul_penawaran' => ['required', 'string', 'max:255'],
'keterangan_penawaran' => ['nullable', 'string'],
]);
$kk = DB::table('tbdosen')
->where('iddosen', $user['id'])
->value('kelompokKeahlian');
DB::table('tb_penawaran_judul')->insert([
'idDosen' => $user['id'],
'kk' => $kk ?: 0,
'judul' => trim($data['judul_penawaran']),
'deskripsi' => trim((string) ($data['keterangan_penawaran'] ?? '')),
'waktuInput' => now(),
]);
return redirect()->route('dosen.penawaran.index')->with('success', 'Data Penawaran Judul Berhasil Disimpan');
}
public function editPenawaran(Request $request, int $id): View
{
$user = $this->dosenUser($request);
$penawaran = DB::table('tb_penawaran_judul')
->where('idPenawaran', $id)
->where('idDosen', $user['id'])
->first();
abort_unless($penawaran, 404);
return $this->renderPage('dosen.pages.penawaran-form', [
'title' => 'Edit Penawaran Judul | SPOTA Rebuild',
'pageTitle' => 'Edit Data Penawaran Judul',
'pageDescription' => 'Perbarui data penawaran judul milik dosen ini.',
'routeName' => 'dosen.penawaran.index',
'user' => $user,
'formMode' => 'edit',
'formAction' => route('dosen.penawaran.update', $id),
'formMethod' => 'PUT',
'penawaranItem' => [
'judul' => old('judul_penawaran', $penawaran->judul),
'deskripsi' => old('keterangan_penawaran', $penawaran->deskripsi),
],
]);
}
public function updatePenawaran(Request $request, int $id): RedirectResponse
{
$user = $this->dosenUser($request);
$data = $request->validate([
'judul_penawaran' => ['required', 'string', 'max:255'],
'keterangan_penawaran' => ['nullable', 'string'],
]);
$updated = DB::table('tb_penawaran_judul')
->where('idPenawaran', $id)
->where('idDosen', $user['id'])
->update([
'judul' => trim($data['judul_penawaran']),
'deskripsi' => trim((string) ($data['keterangan_penawaran'] ?? '')),
]);
abort_unless($updated !== 0, 404);
return redirect()->route('dosen.penawaran.index')->with('success', 'Data berhasil diubah');
}
public function destroyPenawaran(Request $request, int $id): RedirectResponse
{
$user = $this->dosenUser($request);
$hasBooking = DB::table('tb_ambil_judul')
->where('idPenawaranAmbil', $id)
->exists();
if ($hasBooking) {
return redirect()->route('dosen.penawaran.index')->with('error', 'Tidak dapat menghapus judul ini, judul ini pernah di booking mahasiswa sebelumnya');
}
$deleted = DB::table('tb_penawaran_judul')
->where('idPenawaran', $id)
->where('idDosen', $user['id'])
->delete();
abort_unless($deleted !== 0, 404);
return redirect()->route('dosen.penawaran.index')->with('success', 'Data Penawaran Judul Ini Telah Dihapus.');
}
public function approvePenawaran(Request $request, int $id): RedirectResponse
{
$user = $this->dosenUser($request);
$booking = $this->latestPenawaranBooking($id, $user['id']);
if (! $booking) {
return redirect()->route('dosen.penawaran.index')->with('error', 'Aksi gagal, data pengambilan judul tidak ditemukan.');
}
DB::table('tb_ambil_judul')
->where('idAmbil', $booking->idAmbil)
->update([
'statusPengambilan' => '1',
'waktuVerifikasi' => now(),
]);
return redirect()->route('dosen.penawaran.index')->with('success', 'Berhasil menyetujui pengambilan judul.');
}
public function rejectPenawaran(Request $request, int $id): RedirectResponse
{
$user = $this->dosenUser($request);
$booking = $this->latestPenawaranBooking($id, $user['id']);
if (! $booking) {
return redirect()->route('dosen.penawaran.index')->with('error', 'Aksi gagal, data pengambilan judul tidak ditemukan.');
}
DB::table('tb_ambil_judul')
->where('idAmbil', $booking->idAmbil)
->update([
'statusPengambilan' => '2',
'waktuVerifikasi' => now(),
]);
return redirect()->route('dosen.penawaran.index')->with('success', 'Berhasil menolak pengambilan judul.');
}
public function daftarUsulan(Request $request): View
{
$user = $this->dosenUser($request);
$usulan = DB::table('tbpraoutline as tp')
->leftJoin('tbmhs as tm', 'tm.nim', '=', 'tp.nim')
->leftJoin('tb_kelompok_keahlian as tkk', 'tkk.idKK', '=', 'tp.kelompokKeahlian')
->select([
'tp.id',
'tp.judul',
'tp.nim',
'tm.nmLengkap',
'tp.thn_ajaran',
'tp.semester',
'tp.tgl_upload',
'tp.wkt_upload',
'tp.status_usulan',
'tkk.namaKK',
])
->where('tp.idProdi', $user['prodi'])
->where('tp.status_usulan', '0')
->orderByDesc('tp.tgl_upload')
->orderByDesc('tp.wkt_upload')
->limit(30)
->get()
->map(fn ($item) => [
'id' => $item->id,
'judul' => $item->judul,
'mahasiswa' => ($item->nmLengkap ?: 'Mahasiswa tidak ditemukan').' ('.$item->nim.')',
'periode' => trim(($item->thn_ajaran ?: '-').' / '.($item->semester ?: '-')),
'tanggal' => $this->formatDateTime(trim(($item->tgl_upload ?: '').' '.($item->wkt_upload ?: ''))),
'kk' => $item->namaKK ?: '-',
'status' => $this->statusUsulan($item->status_usulan),
'reviewHref' => route('dosen.praoutline.review', $item->id, false),
])
->all();
return $this->renderPage('dosen.pages.daftar-usulan', [
'title' => 'Daftar Usulan | SPOTA Rebuild',
'pageTitle' => 'Daftar Usulan',
'pageDescription' => 'Usulan judul terbaru yang masih dalam proses pada program studi dosen.',
'routeName' => 'dosen.praoutline.index',
'user' => $user,
'usulan' => $usulan,
'pageActions' => [
['label' => 'Pencarian Usulan', 'href' => route('dosen.praoutline.cari'), 'variant' => 'light'],
],
]);
}
public function reviewSaya(Request $request): View
{
$user = $this->dosenUser($request);
$reviews = DB::table('tbreview as tr')
->join('tbpraoutline as tp', 'tp.id', '=', 'tr.idpraoutline')
->leftJoin('tbmhs as tm', 'tm.nim', '=', 'tp.nim')
->select([
'tp.id',
'tp.judul',
'tp.nim',
'tm.nmLengkap',
'tp.thn_ajaran',
'tp.semester',
'tp.tgl_upload',
'tp.status_usulan',
])
->where('tr.reviewer', $user['nip'])
->distinct()
->orderByDesc('tp.tgl_upload')
->limit(30)
->get()
->map(fn ($item) => [
'id' => $item->id,
'judul' => $item->judul,
'mahasiswa' => ($item->nmLengkap ?: 'Mahasiswa tidak ditemukan').' ('.$item->nim.')',
'periode' => trim(($item->thn_ajaran ?: '-').' / '.($item->semester ?: '-')),
'tanggal' => $this->formatDateTime($item->tgl_upload),
'status' => $this->statusUsulan($item->status_usulan),
'reviewHref' => route('dosen.praoutline.review', $item->id, false),
])
->all();
return $this->renderPage('dosen.pages.review-saya', [
'title' => 'Review Saya | SPOTA Rebuild',
'pageTitle' => 'Review Saya',
'pageDescription' => 'Usulan judul TA yang pernah diberi komentar atau tanggapan oleh dosen ini.',
'routeName' => 'dosen.praoutline.review-saya',
'user' => $user,
'reviews' => $reviews,
]);
}
public function cari(Request $request): View
{
$user = $this->dosenUser($request);
$keyword = trim((string) $request->query('q', ''));
$by = (string) $request->query('by', 'judul');
$results = [];
if ($keyword !== '') {
$query = DB::table('tbpraoutline as tp')
->join('tbmhs as tm', 'tm.nim', '=', 'tp.nim')
->leftJoin('tbrekaphasil as trh', 'trh.idpraoutline', '=', 'tp.id')
->select([
'tp.id',
'tp.judul',
'tp.deskripsi',
'tp.nim',
'tm.nmLengkap',
'tp.tgl_upload',
'tp.status_usulan',
'trh.judul_final',
'trh.pemb1',
'trh.pemb2',
'trh.peng1',
'trh.peng2',
]);
if ($by === 'nim') {
$query->where('tp.nim', 'like', '%'.$keyword.'%');
} elseif ($by === 'dosen') {
$query->where(function ($builder) use ($keyword) {
$builder->where('trh.pemb1', 'like', '%'.$keyword.'%')
->orWhere('trh.pemb2', 'like', '%'.$keyword.'%')
->orWhere('trh.peng1', 'like', '%'.$keyword.'%')
->orWhere('trh.peng2', 'like', '%'.$keyword.'%');
});
} else {
$query->where('tp.judul', 'like', '%'.$keyword.'%');
}
$results = $query
->orderByDesc('tp.tgl_upload')
->limit(20)
->get()
->map(fn ($item) => [
'id' => $item->id,
'judul' => $item->judul,
'mahasiswa' => $item->nmLengkap.' ('.$item->nim.')',
'deskripsi' => $item->deskripsi ? mb_strimwidth(strip_tags($item->deskripsi), 0, 200, '...') : '-',
'tanggal' => $this->formatDateTime($item->tgl_upload),
'status' => $this->statusUsulan($item->status_usulan),
'reviewHref' => route('dosen.praoutline.review', $item->id, false),
])
->all();
}
return $this->renderPage('dosen.pages.cari', [
'title' => 'Pencarian Usulan | SPOTA Rebuild',
'pageTitle' => 'Pencarian Usulan',
'pageDescription' => 'Cari usulan berdasarkan NIM, judul, atau keterkaitan pembimbing dan penguji.',
'routeName' => 'dosen.praoutline.cari',
'user' => $user,
'keyword' => $keyword,
'searchBy' => $by,
'results' => $results,
]);
}
public function bimbingan(Request $request): View
{
$user = $this->dosenUser($request);
$bimbingan = DB::table('tbrekaphasil as trh')
->leftJoin('tbmhs as tm', 'tm.nim', '=', 'trh.nim')
->select([
'trh.id',
'trh.idpraoutline',
'trh.judul_final',
'trh.nim',
'tm.nmLengkap',
'trh.tahun_ajaran',
'trh.semester',
'trh.tgl_kep',
])
->where('trh.idProdi', $user['prodi'])
->where('trh.kep_akhir', '1')
->where(function ($query) use ($user) {
$query->where('trh.pemb1', $user['nip'])
->orWhere('trh.pemb2', $user['nip'])
->orWhere('trh.peng1', $user['nip'])
->orWhere('trh.peng2', $user['nip']);
})
->orderByDesc('trh.tgl_kep')
->limit(30)
->get()
->map(fn ($item) => [
'id' => $item->id,
'judul' => $item->judul_final,
'mahasiswa' => ($item->nmLengkap ?: 'Mahasiswa tidak ditemukan').' ('.$item->nim.')',
'periode' => trim(($item->tahun_ajaran ?: '-').' / '.($item->semester ?: '-')),
'tanggal' => $this->formatDateTime($item->tgl_kep),
'reviewHref' => route('dosen.praoutline.review', $item->idpraoutline, false),
])
->all();
return $this->renderPage('dosen.pages.bimbingan', [
'title' => 'Daftar Bimbingan Saya | SPOTA Rebuild',
'pageTitle' => 'Daftar Bimbingan Saya',
'pageDescription' => 'Mahasiswa yang telah ditetapkan pada dosen ini sebagai pembimbing atau penguji.',
'routeName' => 'dosen.praoutline.bimbingan',
'user' => $user,
'bimbingan' => $bimbingan,
]);
}
public function statistik(Request $request): View
{
$user = $this->dosenUser($request);
$draftStats = DB::table('tbpraoutline')
->selectRaw('semester')
->selectRaw("COUNT(IF(status_usulan='0',1,NULL)) as proses")
->selectRaw("COUNT(IF(status_usulan='1',1,NULL)) as terima")
->selectRaw("COUNT(IF(status_usulan='2',1,NULL)) as tolak")
->selectRaw("COUNT(IF(status_usulan='3',1,NULL)) as gugur")
->selectRaw('COUNT(*) as totaldraft')
->where('idProdi', $user['prodi'])
->groupBy('semester')
->orderByDesc('semester')
->get();
$dosenStats = DB::table('tbrekaphasil')
->selectRaw("COUNT(IF(pemb1 = ?, 1, NULL)) as pemb1", [$user['nip']])
->selectRaw("COUNT(IF(pemb2 = ?, 1, NULL)) as pemb2", [$user['nip']])
->selectRaw("COUNT(IF(peng1 = ?, 1, NULL)) as peng1", [$user['nip']])
->selectRaw("COUNT(IF(peng2 = ?, 1, NULL)) as peng2", [$user['nip']])
->where('idProdi', $user['prodi'])
->first();
return $this->renderPage('dosen.pages.statistik', [
'title' => 'Statistik Usulan | SPOTA Rebuild',
'pageTitle' => 'Statistik Usulan',
'pageDescription' => 'Ringkasan statistik draft praoutline dan peran dosen pada data rekap aktif.',
'routeName' => 'dosen.praoutline.statistik',
'user' => $user,
'draftStats' => $draftStats,
'dosenStats' => $dosenStats,
]);
}
public function pemberitahuan(Request $request): View
{
$user = $this->dosenUser($request);
$pemberitahuan = DB::table('tmp_notif_r as tnr')
->leftJoin('tbpraoutline as tp', 'tp.id', '=', 'tnr.idkonten')
->select(['tnr.msg', 'tnr.tgl', 'tnr.idkonten', 'tp.judul'])
->where('tnr.read', 'N')
->where('tnr.jns_usr', 'D')
->where('tnr.user', $user['nip'])
->where('tnr.idProdi', $user['prodi'])
->orderByDesc('tnr.tgl')
->limit(30)
->get()
->map(fn ($item) => [
'msg' => $item->msg,
'tgl' => $item->tgl,
'title' => $item->judul,
'reviewHref' => $item->idkonten ? route('dosen.praoutline.review', $item->idkonten, false).'#post_review' : null,
]);
return $this->renderPage('dosen.pages.pemberitahuan', [
'title' => 'Pemberitahuan | SPOTA Rebuild',
'pageTitle' => 'Pemberitahuan',
'pageDescription' => 'Daftar tanggapan dan review baru yang belum dibaca oleh dosen ini.',
'routeName' => 'dosen.praoutline.pemberitahuan',
'user' => $user,
'pemberitahuan' => $pemberitahuan,
]);
}
public function pengumuman(Request $request): View
{
$user = $this->dosenUser($request);
$pengumuman = DB::table('tbpengumuman as tp')
->select([
'tp.id',
'tp.judul',
'tp.isi',
'tp.tgl',
])
->where('tp.idProdi', $user['prodi'])
->whereIn('tp.tujuan', ['A', 'D'])
->orderByDesc('tp.tgl')
->limit(30)
->get();
$pengumuman = $pengumuman->map(fn ($item) => [
'id' => $item->id,
'judul' => $item->judul,
'tgl' => $item->tgl,
'detailHref' => route('dosen.pengumuman.show', $item->id),
]);
return $this->renderPage('dosen.pages.pengumuman', [
'title' => 'Pengumuman | SPOTA Rebuild',
'pageTitle' => 'Daftar Pengumuman',
'pageDescription' => 'Pengumuman program studi yang ditujukan untuk dosen dan akses umum.',
'routeName' => 'dosen.pengumuman.index',
'user' => $user,
'pengumuman' => $pengumuman,
]);
}
public function reviewDetail(Request $request, int $id): View
{
$user = $this->dosenUser($request);
$outline = DB::table('tbpraoutline as tp')
->leftJoin('tbmhs as tm', 'tm.nim', '=', 'tp.nim')
->leftJoin('tb_kelompok_keahlian as tkk', 'tkk.idKK', '=', 'tp.kelompokKeahlian')
->select([
'tp.id',
'tp.judul',
'tp.deskripsi',
'tp.nim',
'tp.thn_ajaran',
'tp.semester',
'tp.tgl_upload',
'tp.wkt_upload',
'tp.status_usulan',
'tm.nmLengkap',
'tkk.namaKK',
])
->where('tp.id', $id)
->where('tp.idProdi', $user['prodi'])
->first();
abort_unless($outline, 404);
$reviews = DB::table('tbreview as tr')
->leftJoin('tbdosen as td', 'td.nip', '=', 'tr.reviewer')
->leftJoin('tbmhs as tm', 'tm.nim', '=', 'tr.reviewer')
->select([
'tr.id',
'tr.review_text',
'tr.jenis_review',
'tr.putusan',
'tr.tgl',
'tr.wkt',
'tr.reviewer',
'td.nmLengkap as namaDosen',
'tm.nmLengkap as namaMahasiswa',
])
->where('tr.idpraoutline', $outline->id)
->orderBy('tr.tgl')
->orderBy('tr.wkt')
->orderBy('tr.id')
->get()
->map(function ($item) {
$isDecision = (string) $item->jenis_review === '1';
return [
'id' => $item->id,
'author' => $item->namaDosen ?: ($item->namaMahasiswa ?: $item->reviewer),
'role' => $item->namaDosen ? 'Dosen' : 'Mahasiswa',
'timestamp' => $this->formatDateTime(trim(($item->tgl ?: '').' '.($item->wkt ?: ''))),
'body' => $item->review_text,
'type' => $isDecision ? 'Putusan' : 'Komentar',
'decision' => ! $isDecision ? null : ((string) $item->putusan === '1' ? 'Setuju' : 'Tidak Setuju'),
];
})
->all();
return $this->renderPage('dosen.pages.review-detail', [
'title' => 'Detail Review | SPOTA Rebuild',
'pageTitle' => 'Detail Review Usulan',
'pageDescription' => 'Riwayat komentar dan putusan pada usulan judul mahasiswa.',
'routeName' => 'dosen.praoutline.review',
'user' => $user,
'outline' => [
'id' => $outline->id,
'judul' => $outline->judul,
'deskripsi' => $outline->deskripsi,
'mahasiswa' => ($outline->nmLengkap ?: 'Mahasiswa tidak ditemukan').' ('.$outline->nim.')',
'periode' => trim(($outline->thn_ajaran ?: '-').' / '.($outline->semester ?: '-')),
'tanggal' => $this->formatDateTime(trim(($outline->tgl_upload ?: '').' '.($outline->wkt_upload ?: ''))),
'status' => $this->statusUsulan($outline->status_usulan),
'kk' => $outline->namaKK ?: '-',
],
'reviews' => $reviews,
'pageActions' => [
['label' => 'Kembali ke Review Saya', 'href' => route('dosen.praoutline.review-saya', [], false), 'variant' => 'light'],
],
]);
}
public function showPengumuman(Request $request, int $id): View
{
$user = $this->dosenUser($request);
$pengumuman = DB::table('tbpengumuman')
->where('id', $id)
->where('idProdi', $user['prodi'])
->whereIn('tujuan', ['A', 'D'])
->first();
abort_unless($pengumuman, 404);
$alreadyRead = DB::table('tmp_notif')
->where('idkonten', $id)
->where('iduser', $user['id'])
->where('typeuser', 'D')
->where('jenis', 'P')
->exists();
if (! $alreadyRead) {
DB::table('tmp_notif')->insert([
'idkonten' => $id,
'idProdi' => $user['prodi'],
'iduser' => $user['id'],
'typeuser' => 'D',
'date' => now(),
'jenis' => 'P',
]);
}
return $this->renderPage('dosen.pages.pengumuman-detail', [
'title' => 'Lihat Pengumuman | SPOTA Rebuild',
'pageTitle' => 'Lihat Pengumuman',
'pageDescription' => 'Detail pengumuman dosen dari sistem SPOTA lama.',
'routeName' => 'dosen.pengumuman.show',
'user' => $user,
'pengumumanItem' => $pengumuman,
'pageActions' => [
['label' => 'Kembali ke Pengumuman', 'href' => route('dosen.pengumuman.index'), 'variant' => 'light'],
],
]);
}
public function profile(Request $request): View
{
$user = $this->dosenUser($request);
$profile = DB::table('tbdosen')
->where('iddosen', $user['id'])
->first();
return $this->renderPage('dosen.pages.profile', [
'title' => 'Profil Dosen | SPOTA Rebuild',
'pageTitle' => 'Profil Dosen',
'pageDescription' => 'Kelola data akun dasar dosen yang aktif pada SPOTA.',
'routeName' => 'dosen.profile',
'user' => $user,
'profile' => $profile,
]);
}
public function updateProfile(Request $request): RedirectResponse
{
$user = $this->dosenUser($request);
$data = $request->validate([
'nmLengkap' => ['required', 'string', 'max:150'],
'email' => ['nullable', 'email', 'max:150'],
'nohp' => ['nullable', 'string', 'max:75'],
'password' => ['nullable', 'string', 'min:6', 'max:75', 'same:password_again'],
'password_again' => ['nullable', 'string', 'min:6', 'max:75'],
], [
'password.same' => 'Konfirmasi password tidak sesuai.',
]);
$payload = [
'nmLengkap' => trim($data['nmLengkap']),
'email' => trim((string) ($data['email'] ?? '')),
'nohp' => trim((string) ($data['nohp'] ?? '')),
];
if (! empty($data['password'])) {
$payload['password'] = md5($data['password']);
}
DB::table('tbdosen')
->where('iddosen', $user['id'])
->update($payload);
$request->session()->put('legacy_auth.user', array_merge($user, [
'nama_lengkap' => $payload['nmLengkap'],
]));
return redirect()->route('dosen.profile')->with('success', 'Profil dosen berhasil diperbarui.');
}
public function earlyWarning(Request $request): View
{
$user = $this->dosenUser($request);
$records = DB::table('tbrekaphasil as trh')
->leftJoin('tbmhs as tm', 'tm.nim', '=', 'trh.nim')
->select([
'trh.nim',
'tm.nmLengkap',
'tm.thnmasuk',
'trh.judul_final',
'trh.tgl_kep',
])
->where('trh.idProdi', $user['prodi'])
->where(function ($query) use ($user) {
$query->where('trh.pemb1', $user['nip'])
->orWhere('trh.pemb2', $user['nip'])
->orWhere('trh.peng1', $user['nip'])
->orWhere('trh.peng2', $user['nip']);
})
->orderBy('trh.tgl_kep')
->limit(50)
->get()
->map(function ($item) {
$days = $item->tgl_kep ? Carbon::parse($item->tgl_kep)->diffInDays(now()) : null;
$angkatan = $this->resolveAngkatan($item->nim, $item->thnmasuk ?? null);
$tahunStudi = $angkatan ? (int) now()->format('Y') - $angkatan : null;
if ($tahunStudi === null) {
$severity = 'unknown';
$status = 'Angkatan Tidak Diketahui';
} elseif ($tahunStudi >= 8) {
$severity = 'dropout';
$status = 'Ancaman DO';
} elseif ($tahunStudi >= 7) {
$severity = 'critical';
$status = 'Kritis Akhir Studi';
} elseif ($tahunStudi >= 6) {
$severity = 'warning';
$status = 'Warning';
} elseif ($tahunStudi >= 5) {
$severity = 'watch';
$status = 'Perlu Pantau';
} else {
$severity = 'safe';
$status = 'Aman';
}
return [
'mahasiswa' => ($item->nmLengkap ?: 'Mahasiswa tidak ditemukan').' ('.$item->nim.')',
'nim' => $item->nim,
'judul' => $item->judul_final ?: '-',
'tanggal' => $this->formatDateTime($item->tgl_kep),
'status' => $status,
'days' => $days,
'angkatan' => $angkatan,
'tahunStudi' => $tahunStudi,
'severity' => $severity,
'statusClass' => match ($severity) {
'dropout' => 'bg-[#3B0A0A] text-white border border-[#3B0A0A]',
'critical' => 'bg-[#7F1D1D] text-white border border-[#7F1D1D]',
'warning' => 'bg-rose-100 text-rose-700 border border-rose-200',
'watch' => 'bg-amber-100 text-amber-700 border border-amber-200',
'safe' => 'bg-emerald-100 text-emerald-700 border border-emerald-200',
default => 'bg-slate-100 text-slate-700 border border-slate-200',
},
'warningText' => match ($severity) {
'dropout' => 'Mahasiswa sudah masuk ambang akhir masa studi dan perlu penanganan prioritas tertinggi.',
'critical' => 'Mahasiswa berada di tahun akhir masa studi dan berisiko tinggi jika progres tersendat.',
'warning' => 'Mahasiswa sudah melewati fase aman studi dan perlu tindak lanjut aktif.',
'watch' => 'Mahasiswa memasuki tahun studi lanjut, sebaiknya dipantau lebih ketat.',
'safe' => 'Mahasiswa masih dalam rentang studi yang relatif aman untuk monitoring rutin.',
default => 'Angkatan mahasiswa belum dapat ditentukan dari data yang tersedia.',
},
'detailHref' => $this->legacyUrl('dosen/dashboard.php?page=early-warning'),
];
})
->all();
$dropoutCount = collect($records)->where('severity', 'dropout')->count();
$criticalCount = collect($records)->where('severity', 'critical')->count();
$warningCount = collect($records)->where('severity', 'warning')->count();
$watchCount = collect($records)->where('severity', 'watch')->count();
$safeCount = collect($records)->where('severity', 'safe')->count();
return $this->renderPage('dosen.pages.early-warning', [
'title' => 'Early Warning | SPOTA Rebuild',
'pageTitle' => 'Early Warning',
'pageDescription' => 'Pantauan mahasiswa bimbingan berdasarkan tanggal keputusan outline yang tersimpan.',
'routeName' => 'dosen.early-warning',
'user' => $user,
'records' => $records,
'summary' => [
'dropoutCount' => $dropoutCount,
'criticalCount' => $criticalCount,
'warningCount' => $warningCount,
'watchCount' => $watchCount,
'safeCount' => $safeCount,
'totalCount' => count($records),
],
]);
}
public function praLirs(Request $request): View
{
$user = $this->dosenUser($request);
return $this->renderPage('dosen.pages.pra-lirs', [
'title' => 'Pra LIRS | SPOTA Rebuild',
'pageTitle' => 'Pra LIRS (Dosen PA)',
'pageDescription' => 'Integrasi Pra LIRS saat ini masih memakai sumber data eksternal yang sama dengan modul lama.',
'routeName' => 'dosen.pra-lirs',
'user' => $user,
'externalUrl' => 'https://informatika.untan.ac.id/API/public/getListMahasiswaPralirsPASaya.php?nip='.$user['nip'],
]);
}
private function renderPage(string $view, array $data): View
{
$data['sidebar'] = DosenNavigation::build($data['routeName']);
$data['pageDate'] = $this->formatDateTime(now()->toDateTimeString());
return view($view, $data);
}
private function dosenUser(Request $request): array
{
$auth = $request->session()->get('legacy_auth');
abort_unless(($auth['role'] ?? null) === 'dosen', 403);
return $auth['user'];
}
private function formatDateTime(?string $value): string
{
if (! $value || strtotime($value) === false) {
return '-';
}
return Carbon::parse($value)->locale('id')->translatedFormat('j F Y, H:i');
}
private function statusPenawaran($status): string
{
return match ((string) $status) {
'0' => 'Belum Diproses',
'1' => 'Diterima',
'2' => 'Belum Diambil',
default => 'Belum Diambil',
};
}
private function statusUsulan($status): string
{
return match ((string) $status) {
'1' => 'Judul Diterima',
'2' => 'Judul Ditolak',
'3' => 'Judul Gugur',
default => 'Dalam Proses',
};
}
private function latestPenawaranBooking(int $idPenawaran, string $idDosen): ?object
{
return DB::table('tb_ambil_judul as taj')
->join('tb_penawaran_judul as tpj', 'tpj.idPenawaran', '=', 'taj.idPenawaranAmbil')
->select(['taj.idAmbil', 'taj.statusPengambilan'])
->where('taj.idPenawaranAmbil', $idPenawaran)
->where('tpj.idDosen', $idDosen)
->orderByDesc('taj.waktuPengambilan')
->first();
}
private function resolveAngkatan(?string $nim, $thnMasuk): ?int
{
$thnMasuk = is_numeric($thnMasuk) ? (int) $thnMasuk : null;
if ($thnMasuk && $thnMasuk > 2000 && $thnMasuk <= ((int) date('Y') + 1)) {
return $thnMasuk;
}
$nim = (string) $nim;
if (strlen($nim) >= 7 && preg_match('/^(?:[A-Z])(\d{6,})/i', $nim, $matches)) {
$digits = $matches[1];
$angkatan2Digit = substr($digits, 4, 2);
if (ctype_digit($angkatan2Digit)) {
$year = 2000 + (int) $angkatan2Digit;
if ($year <= ((int) date('Y') + 1)) {
return $year;
}
}
}
return null;
}
private function legacyUrl(string $path): string
{
$baseUrl = rtrim((string) env('LEGACY_BASE_URL', 'http://127.0.0.1:8080'), '/');
return $baseUrl.'/'.ltrim($path, '/');
}
}

View File

@@ -0,0 +1,467 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class MahasiswaPageController extends Controller
{
public function statusUsulan(Request $request): View
{
$user = $this->getMahasiswaUser($request);
$outline = DB::table('tbpraoutline as tp')
->leftJoin('tbrekaphasil as trh', 'tp.id', '=', 'trh.idpraoutline')
->leftJoin('tb_kelompok_keahlian as kk', 'tp.kelompokKeahlian', '=', 'kk.idKK')
->select([
'tp.id',
'tp.judul',
'tp.deskripsi',
'tp.status_usulan',
'tp.tgl_upload',
'tp.wkt_upload',
'trh.ket',
'kk.namaKK as kelompok_keahlian',
])
->where('tp.nim', $user['nim'])
->where('tp.idProdi', $user['prodi'])
->orderByDesc('tp.id')
->first();
$reviews = [];
if ($outline) {
$reviews = DB::table('tbreview as tr')
->leftJoin('tbdosen as td', 'tr.reviewer', '=', 'td.nip')
->leftJoin('tbmhs as tm', 'tr.reviewer', '=', 'tm.nim')
->select([
'tr.review_text',
'tr.jenis_review',
'tr.putusan',
'tr.tgl',
'tr.wkt',
'tr.reviewer',
'td.nmLengkap as reviewer_name',
'tm.nmLengkap as mahasiswa_name',
])
->where('tr.idpraoutline', $outline->id)
->orderBy('tr.tgl')
->orderBy('tr.wkt')
->get()
->map(function ($item) {
$timestamp = trim(($item->tgl ?? '').' '.($item->wkt ?? ''));
return [
'author' => $item->reviewer_name ?: ($item->mahasiswa_name ?: $item->reviewer),
'role' => $item->reviewer_name ? 'Dosen' : 'Mahasiswa',
'timestamp' => $this->formatDateTime($timestamp),
'type' => $item->jenis_review === 'P' ? 'Putusan' : 'Review',
'decision' => $this->decisionLabel($item->putusan),
'body' => $item->review_text ?: '<p class="text-[#6B7280]">Tidak ada isi review.</p>',
];
})
->all();
}
return view('mahasiswa.pages.status-usulan', [
'title' => 'Status Usulan Mahasiswa | SPOTA Rebuild',
'pageTitle' => 'Status Usulan',
'pageDescription' => 'Ringkasan draft praoutline terakhir, status keputusan, dan riwayat review dosen untuk mahasiswa yang sedang login.',
'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'),
'sidebar' => $this->buildSidebar('mahasiswa.status-usulan'),
'user' => $user,
'outline' => $outline ? [
'id' => $outline->id,
'judul' => $outline->judul,
'deskripsi' => $outline->deskripsi,
'status' => $this->statusLabel($outline->status_usulan),
'statusClass' => $this->statusBadgeClass($outline->status_usulan),
'tanggal' => $this->formatDateTime(trim(($outline->tgl_upload ?? '').' '.($outline->wkt_upload ?? ''))),
'catatan' => $outline->ket,
'kelompokKeahlian' => $outline->kelompok_keahlian ?: '-',
] : null,
'reviews' => $reviews,
'pageActions' => $this->statusPageActions($outline),
]);
}
public function uploadPraoutline(Request $request): View
{
$user = $this->getMahasiswaUser($request);
$hasActiveDraft = DB::table('tbpraoutline')
->where('nim', $user['nim'])
->whereIn('status_usulan', ['0', '1'])
->exists();
$dosen = DB::table('tbdosen')
->select(['nmLengkap'])
->where('idProdi', $user['prodi'])
->where('status', 'A')
->orderBy('nmLengkap')
->get();
$kelompokKeahlian = DB::table('tb_kelompok_keahlian')
->select(['idKK', 'namaKK'])
->orderBy('namaKK')
->get();
return view('mahasiswa.pages.upload-praoutline', [
'title' => 'Ajukan Praoutline | SPOTA Rebuild',
'pageTitle' => 'Ajukan Outline Baru',
'pageDescription' => 'Form pengajuan draft praoutline mengikuti field utama pada SPOTA lama. File PDF tetap divalidasi sebelum disimpan.',
'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'),
'sidebar' => $this->buildSidebar('mahasiswa.praoutline.upload'),
'user' => $user,
'hasActiveDraft' => $hasActiveDraft,
'dosen' => $dosen,
'kelompokKeahlian' => $kelompokKeahlian,
]);
}
public function storePraoutline(Request $request): RedirectResponse
{
$user = $this->getMahasiswaUser($request);
$hasActiveDraft = DB::table('tbpraoutline')
->where('nim', $user['nim'])
->whereIn('status_usulan', ['0', '1'])
->exists();
if ($hasActiveDraft) {
return back()->with('error', 'Draft praoutline aktif masih ada. Silakan lihat status usulan/review terlebih dahulu.');
}
$validated = $request->validate([
'judul' => ['required', 'string', 'max:255'],
'deskripsi' => ['nullable', 'string'],
'berkas' => ['required', 'file', 'mimes:pdf', 'max:10240'],
'dosenpa' => ['required', 'string', 'max:255'],
'pilpemb1' => ['nullable', 'string', 'max:255'],
'pilpemb2' => ['nullable', 'string', 'max:255'],
'pilpemb3' => ['nullable', 'string', 'max:255'],
'pilpemb4' => ['nullable', 'string', 'max:255'],
'drekomjudul' => ['nullable', 'string', 'max:255'],
'kelompokKeahlian' => ['required', 'integer'],
]);
$file = $request->file('berkas');
$filename = $user['nim'].'-'.time().'.'.$file->getClientOriginalExtension();
$file->move(base_path('../files'), $filename);
DB::table('tbpraoutline')->insert([
'nim' => $user['nim'],
'judul' => $validated['judul'],
'deskripsi' => $validated['deskripsi'] ?? '',
'berkas' => $filename,
'idProdi' => $user['prodi'],
'tgl_upload' => Carbon::now()->toDateString(),
'wkt_upload' => Carbon::now()->format('H:i:s'),
'semester' => $this->currentSemester(),
'thn_ajaran' => $this->currentAcademicYear(),
'status_usulan' => '0',
'ket' => '',
'kelompokKeahlian' => (string) $validated['kelompokKeahlian'],
'kkTerkait' => '',
]);
return redirect()->route('mahasiswa.status-usulan')->with('success', 'Draft praoutline berhasil diajukan.');
}
public function pengumuman(Request $request): View
{
$user = $this->getMahasiswaUser($request);
$pengumuman = DB::table('tbpengumuman')
->select(['id', 'judul', 'isi', 'tgl'])
->where('idProdi', $user['prodi'])
->whereIn('tujuan', ['A', 'M'])
->orderByDesc('id')
->limit(25)
->get()
->map(function ($item) {
return [
'id' => $item->id,
'judul' => $item->judul,
'preview' => str($item->isi)->stripTags()->squish()->limit(180)->toString(),
'tgl' => $item->tgl,
'detailHref' => route('mahasiswa.pengumuman.show', ['id' => $item->id], false),
];
})
->all();
return view('mahasiswa.pages.pengumuman', [
'title' => 'Pengumuman Mahasiswa | SPOTA Rebuild',
'pageTitle' => 'Pengumuman',
'pageDescription' => 'Daftar pengumuman program studi yang ditujukan untuk mahasiswa pada data SPOTA aktif.',
'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'),
'sidebar' => $this->buildSidebar('mahasiswa.pengumuman.index'),
'user' => $user,
'pengumuman' => $pengumuman,
]);
}
public function showPengumuman(Request $request, int $id): View
{
$user = $this->getMahasiswaUser($request);
$pengumuman = DB::table('tbpengumuman')
->select(['id', 'judul', 'isi', 'tgl'])
->where('id', $id)
->where('idProdi', $user['prodi'])
->whereIn('tujuan', ['A', 'M'])
->first();
abort_unless($pengumuman, 404);
return view('mahasiswa.pages.pengumuman-detail', [
'title' => 'Detail Pengumuman Mahasiswa | SPOTA Rebuild',
'pageTitle' => 'Detail Pengumuman',
'pageDescription' => 'Isi pengumuman mahasiswa dari basis data SPOTA aktif.',
'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'),
'sidebar' => $this->buildSidebar('mahasiswa.pengumuman.index'),
'user' => $user,
'pageActions' => [
['label' => 'Kembali ke Pengumuman', 'href' => route('mahasiswa.pengumuman.index', [], false)],
],
'pengumuman' => [
'judul' => $pengumuman->judul,
'isi' => $pengumuman->isi,
'tgl' => $this->formatDateTime($pengumuman->tgl),
],
]);
}
public function penawaran(Request $request): View
{
$user = $this->getMahasiswaUser($request);
$status = $request->query('status', '0');
$kk = $request->query('kk', 'Semua');
$query = DB::table('tb_penawaran_judul as tpj')
->leftJoin('tbdosen as td', 'tpj.idDosen', '=', 'td.iddosen')
->leftJoin('tb_kelompok_keahlian as kk', 'tpj.kk', '=', 'kk.idKK')
->leftJoin('tb_ambil_judul as taj', function ($join) {
$join->on('tpj.idPenawaran', '=', 'taj.idPenawaranAmbil')
->whereRaw('taj.idAmbil = (select max(taj2.idAmbil) from tb_ambil_judul taj2 where taj2.idPenawaranAmbil = tpj.idPenawaran)');
})
->leftJoin('tbmhs as tm', 'taj.idMhs', '=', 'tm.idmhs')
->select([
'tpj.idPenawaran',
'tpj.judul',
'tpj.deskripsi',
'tpj.waktuInput',
'td.nmLengkap as dosen',
'kk.namaKK as kk',
'taj.statusPengambilan',
'tm.nmLengkap as diambil_oleh',
])
->orderByDesc('tpj.idPenawaran');
if ($kk !== 'Semua') {
$query->where('tpj.kk', $kk);
}
if ($status === '0') {
$query->where(function ($query) {
$query->whereNull('taj.idAmbil')->orWhere('taj.statusPengambilan', '2');
});
} elseif ($status === '1') {
$query->whereNotNull('taj.idAmbil')->where('taj.statusPengambilan', '!=', '2');
}
$penawaran = $query->paginate(15)->withQueryString();
$kelompokKeahlian = DB::table('tb_kelompok_keahlian')
->select(['idKK', 'namaKK'])
->where('idKK', '!=', '8')
->orderBy('namaKK')
->get();
return view('mahasiswa.pages.penawaran', [
'title' => 'Penawaran Judul Mahasiswa | SPOTA Rebuild',
'pageTitle' => 'Penawaran Judul',
'pageDescription' => 'Daftar judul yang ditawarkan dosen. Mahasiswa dapat melihat detail dan booking judul yang masih tersedia.',
'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'),
'sidebar' => $this->buildSidebar('mahasiswa.penawaran.index'),
'user' => $user,
'penawaran' => $penawaran,
'kelompokKeahlian' => $kelompokKeahlian,
'filters' => ['status' => $status, 'kk' => $kk],
]);
}
public function showPenawaran(Request $request, int $id): View
{
$user = $this->getMahasiswaUser($request);
$penawaran = DB::table('tb_penawaran_judul as tpj')
->leftJoin('tbdosen as td', 'tpj.idDosen', '=', 'td.iddosen')
->leftJoin('tb_kelompok_keahlian as kk', 'tpj.kk', '=', 'kk.idKK')
->select([
'tpj.idPenawaran',
'tpj.judul',
'tpj.deskripsi',
'tpj.waktuInput',
'td.nmLengkap as dosen',
'kk.namaKK as kk',
])
->where('tpj.idPenawaran', $id)
->first();
abort_unless($penawaran, 404);
return view('mahasiswa.pages.penawaran-detail', [
'title' => 'Detail Penawaran Judul | SPOTA Rebuild',
'pageTitle' => 'Detail Penawaran Judul',
'pageDescription' => 'Detail judul yang ditawarkan dosen dan tombol booking apabila mahasiswa ingin mengambil judul ini.',
'pageDate' => Carbon::now()->locale('id')->translatedFormat('j F Y, H:i'),
'sidebar' => $this->buildSidebar('mahasiswa.penawaran.index'),
'user' => $user,
'pageActions' => [
['label' => 'Kembali ke Penawaran', 'href' => route('mahasiswa.penawaran.index', [], false)],
],
'penawaran' => $penawaran,
]);
}
public function bookPenawaran(Request $request, int $id): RedirectResponse
{
$user = $this->getMahasiswaUser($request);
$latestStudentBooking = DB::table('tb_ambil_judul')
->where('idMhs', $user['id'])
->orderByDesc('waktuPengambilan')
->first();
if ($latestStudentBooking?->statusPengambilan === '0') {
return back()->with('error', 'Tidak dapat booking judul ini karena masih ada booking lain yang menunggu verifikasi dosen.');
}
if ($latestStudentBooking?->statusPengambilan === '1' && strtotime($latestStudentBooking->waktuPengambilan) >= strtotime('-30 days')) {
return back()->with('error', 'Tidak dapat booking judul ini karena booking sebelumnya sudah disetujui dosen.');
}
$latestTitleBooking = DB::table('tb_ambil_judul')
->where('idPenawaranAmbil', $id)
->orderByDesc('waktuPengambilan')
->first();
if ($latestTitleBooking && $latestTitleBooking->statusPengambilan !== '2') {
return back()->with('error', 'Judul ini sudah dibooking mahasiswa lain.');
}
DB::table('tb_ambil_judul')->insert([
'idPenawaranAmbil' => $id,
'idMhs' => $user['id'],
'statusPengambilan' => '0',
'waktuPengambilan' => Carbon::now()->toDateTimeString(),
]);
return redirect()->route('mahasiswa.penawaran.index')->with('success', 'Berhasil membooking judul ini. Tunggu verifikasi dari dosen penawar.');
}
private function getMahasiswaUser(Request $request): array
{
$auth = $request->session()->get('legacy_auth');
abort_unless(($auth['role'] ?? null) === 'mahasiswa', 403);
return $auth['user'];
}
private function buildSidebar(string $activeRoute): array
{
return [
'main' => [
['title' => 'Dashboard', 'href' => route('dashboard.mahasiswa'), 'icon' => 'home', 'active' => $activeRoute === 'dashboard.mahasiswa'],
],
'sections' => [
[
'title' => 'Praoutline',
'icon' => 'folder',
'items' => [
['title' => 'Status Usulan', 'href' => route('mahasiswa.status-usulan', [], false), 'icon' => 'chart', 'active' => $activeRoute === 'mahasiswa.status-usulan'],
['title' => 'Ajukan Outline Baru', 'href' => route('mahasiswa.praoutline.upload', [], false), 'icon' => 'clipboard', 'active' => $activeRoute === 'mahasiswa.praoutline.upload'],
['title' => 'Penawaran Judul', 'href' => route('mahasiswa.penawaran.index', [], false), 'icon' => 'briefcase', 'active' => $activeRoute === 'mahasiswa.penawaran.index'],
],
],
[
'title' => 'Informasi',
'icon' => 'bell',
'items' => [
['title' => 'Pengumuman', 'href' => route('mahasiswa.pengumuman.index', [], false), 'icon' => 'megaphone', 'active' => $activeRoute === 'mahasiswa.pengumuman.index'],
],
],
],
];
}
private function formatDateTime(?string $value): string
{
if (! $value) {
return '-';
}
return Carbon::parse($value)->locale('id')->translatedFormat('j F Y, H:i');
}
private function statusLabel(?string $status): string
{
return match ($status) {
'0' => 'Dalam Review',
'1' => 'Disetujui',
'2' => 'Ditolak',
default => 'Belum Ada Draft',
};
}
private function statusBadgeClass(?string $status): string
{
return match ($status) {
'0' => 'bg-sky-100 text-sky-800',
'1' => 'bg-emerald-100 text-emerald-800',
'2' => 'bg-rose-100 text-rose-800',
default => 'bg-slate-100 text-slate-700',
};
}
private function decisionLabel(?string $status): ?string
{
return match ($status) {
'1' => 'Setuju',
'0' => 'Tidak Setuju',
'2' => 'Tolak',
default => null,
};
}
private function statusPageActions(?object $outline): array
{
$actions = [
['label' => 'Ajukan Outline Baru', 'href' => route('mahasiswa.praoutline.upload', [], false), 'variant' => ! $outline || $outline->status_usulan === '2' ? 'dark' : 'light'],
['label' => 'Lihat Penawaran Judul', 'href' => route('mahasiswa.penawaran.index', [], false)],
];
return $actions;
}
private function currentSemester(): string
{
return (int) date('n') >= 8 || (int) date('n') <= 1 ? 'Ganjil' : 'Genap';
}
private function currentAcademicYear(): string
{
$year = (int) date('Y');
if ((int) date('n') >= 8) {
return $year.'/'.($year + 1);
}
return ($year - 1).'/'.$year;
}
}