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

@@ -23,3 +23,13 @@ files
files/**
!files/.gitkeep
img/curiga
rebuild/.env
rebuild/.env.*
!rebuild/.env.example
rebuild/node_modules
rebuild/vendor
rebuild/storage/logs/*
rebuild/storage/framework/cache/*
rebuild/storage/framework/sessions/*
rebuild/storage/framework/views/*
rebuild/bootstrap/cache/*.php

48
Dockerfile.rebuild Normal file
View File

@@ -0,0 +1,48 @@
FROM node:22-bookworm AS assets
WORKDIR /app
COPY rebuild/package*.json ./
RUN npm ci --ignore-scripts
COPY rebuild/ ./
RUN npm run build
FROM composer:2 AS vendor
WORKDIR /app
COPY rebuild/composer.json rebuild/composer.lock ./
RUN composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --no-scripts
COPY rebuild/ ./
RUN composer dump-autoload --optimize --no-scripts
FROM php:8.4-apache
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libfreetype6-dev \
libjpeg62-turbo-dev \
libonig-dev \
libpng-dev \
libzip-dev \
unzip \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j"$(nproc)" pdo_mysql mysqli gd zip mbstring exif \
&& a2enmod rewrite headers \
&& rm -rf /var/lib/apt/lists/*
COPY docker/php.ini /usr/local/etc/php/conf.d/spota.ini
COPY docker/apache-laravel-vhost.conf /etc/apache2/sites-available/000-default.conf
WORKDIR /var/www/html
COPY rebuild/ /var/www/html
COPY --from=vendor /app/vendor /var/www/html/vendor
COPY --from=assets /app/public/build /var/www/html/public/build
COPY docker/laravel-entrypoint.sh /usr/local/bin/laravel-entrypoint
RUN chmod +x /usr/local/bin/laravel-entrypoint \
&& mkdir -p storage/app storage/framework/cache storage/framework/sessions storage/framework/views storage/logs bootstrap/cache public/build \
&& rm -f bootstrap/cache/*.php \
&& APP_ENV=production APP_DEBUG=false APP_KEY=base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= DB_CONNECTION=mysql DB_HOST=db DB_PORT=3306 DB_DATABASE=spota_spotadb DB_USERNAME=spota_user DB_PASSWORD=spota_password SESSION_DRIVER=file CACHE_STORE=file QUEUE_CONNECTION=sync php artisan package:discover --ansi \
&& chown -R www-data:www-data storage bootstrap/cache public/build
ENTRYPOINT ["laravel-entrypoint"]
CMD ["apache2-foreground"]

View File

@@ -2,13 +2,17 @@ services:
app:
build:
context: .
dockerfile: Dockerfile.coolify
dockerfile: Dockerfile.rebuild
restart: unless-stopped
environment:
APP_URL: ${APP_URL}
APP_URL: ${APP_URL:-http://localhost}
APP_ENV: ${APP_ENV:-production}
APP_DEBUG: ${APP_DEBUG:-false}
APP_KEY: ${APP_KEY:-}
DB_HOST: db
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
DB_PORT: 3306
DB_USER: ${DB_USER:-spota_user}
DB_PASSWORD: ${DB_PASSWORD:-spota_password}
DB_NAME: ${DB_NAME:-spota_spotadb}
DB_SPOTA: ${DB_SPOTA:-spota_spotadb}
DB_KONSULTASI: ${DB_KONSULTASI:-spota_konsultasi}
@@ -16,10 +20,9 @@ services:
DB_DOSEN: ${DB_DOSEN:-spota_spotadb}
SERVICE_DB_NAME: ${SERVICE_DB_NAME:-spota_spotadb}
PHP_DISPLAY_ERRORS: ${PHP_DISPLAY_ERRORS:-0}
FILES_STORAGE_PATH: ${FILES_STORAGE_PATH:-/var/www/html/files}
FILES_STORAGE_PATH: ${FILES_STORAGE_PATH:-/var/www/html/storage/app/files}
volumes:
- spota_files:/var/www/html/files
- spota_img:/var/www/html/img
- spota_storage:/var/www/html/storage
depends_on:
db:
condition: service_healthy
@@ -31,9 +34,9 @@ services:
restart: unless-stopped
command: --default-authentication-plugin=mysql_native_password --character-set-server=latin1 --collation-server=latin1_swedish_ci --sql-mode=NO_ENGINE_SUBSTITUTION
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_password}
MYSQL_USER: ${DB_USER:-spota_user}
MYSQL_PASSWORD: ${DB_PASSWORD:-spota_password}
MYSQL_DATABASE: ${DB_NAME:-spota_spotadb}
volumes:
- spota_db_data:/var/lib/mysql
@@ -45,5 +48,4 @@ services:
volumes:
spota_db_data:
spota_files:
spota_img:
spota_storage:

View File

@@ -3,8 +3,7 @@ services:
ports:
- "${APP_PORT:-8080}:80"
volumes:
- ./files:/var/www/html/files
- ./img:/var/www/html/img
- ./rebuild/storage:/var/www/html/storage
db:
ports:

View File

@@ -1,13 +1,19 @@
services:
app:
build: .
build:
context: .
dockerfile: Dockerfile.rebuild
container_name: spota-app
restart: unless-stopped
expose:
- "80"
environment:
APP_URL: ${APP_URL:-http://localhost}
APP_ENV: ${APP_ENV:-production}
APP_DEBUG: ${APP_DEBUG:-false}
APP_KEY: ${APP_KEY:-}
DB_HOST: ${DB_HOST:-db}
DB_PORT: ${DB_PORT:-3306}
DB_USER: ${DB_USER:-spota_user}
DB_PASSWORD: ${DB_PASSWORD:-spota_password}
DB_NAME: ${DB_NAME:-spota_spotadb}
@@ -17,10 +23,9 @@ services:
DB_DOSEN: ${DB_DOSEN:-spota_spotadb}
SERVICE_DB_NAME: ${SERVICE_DB_NAME:-spota_spotadb}
PHP_DISPLAY_ERRORS: ${PHP_DISPLAY_ERRORS:-1}
FILES_STORAGE_PATH: ${FILES_STORAGE_PATH:-/var/www/html/files}
FILES_STORAGE_PATH: ${FILES_STORAGE_PATH:-/var/www/html/storage/app/files}
volumes:
- spota_files:/var/www/html/files
- spota_img:/var/www/html/img
- spota_storage:/var/www/html/storage
depends_on:
db:
condition: service_healthy
@@ -47,5 +52,4 @@ services:
volumes:
spota_db_data:
spota_files:
spota_img:
spota_storage:

View File

@@ -0,0 +1,13 @@
<VirtualHost *:80>
ServerName localhost
DocumentRoot /var/www/html/public
<Directory /var/www/html/public>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env sh
set -e
cd /var/www/html
mkdir -p storage/app/files storage/framework/cache storage/framework/sessions storage/framework/views storage/logs bootstrap/cache public/build
if [ ! -f .env ]; then
cat > .env <<EOF
APP_NAME="SPOTA Rebuild"
APP_ENV=${APP_ENV:-production}
APP_KEY=${APP_KEY:-}
APP_DEBUG=${APP_DEBUG:-false}
APP_URL=${APP_URL:-http://localhost}
APP_LOCALE=id
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=id_ID
LOG_CHANNEL=stack
LOG_STACK=single
LOG_LEVEL=${LOG_LEVEL:-debug}
DB_CONNECTION=mysql
DB_HOST=${DB_HOST:-db}
DB_PORT=${DB_PORT:-3306}
DB_DATABASE=${DB_NAME:-spota_spotadb}
DB_USERNAME=${DB_USER:-spota_user}
DB_PASSWORD=${DB_PASSWORD:-spota_password}
SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=${SESSION_DOMAIN:-}
CACHE_STORE=file
QUEUE_CONNECTION=sync
FILESYSTEM_DISK=local
VITE_APP_NAME="SPOTA Rebuild"
EOF
fi
if [ -z "$(grep '^APP_KEY=base64:' .env || true)" ]; then
php artisan key:generate --force
fi
php artisan optimize:clear || true
php artisan config:cache || true
php artisan route:cache || true
php artisan view:cache || true
chown -R www-data:www-data storage bootstrap/cache public/build 2>/dev/null || true
exec "$@"

BIN
link6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

18
rebuild/.editorconfig Normal file
View File

@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[{compose,docker-compose}.{yml,yaml}]
indent_size = 4

65
rebuild/.env.example Normal file
View File

@@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
rebuild/.gitattributes vendored Normal file
View File

@@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

26
rebuild/.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
*.log
.DS_Store
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.codex
/.cursor/
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
_ide_helper.php
Homestead.json
Homestead.yaml
Thumbs.db

2
rebuild/.npmrc Normal file
View File

@@ -0,0 +1,2 @@
ignore-scripts=true
audit=true

70
rebuild/README.md Normal file
View File

@@ -0,0 +1,70 @@
# SPOTA Rebuild
Laravel Blade + Tailwind prototype untuk rebuild penuh antarmuka SPOTA.
## Current scope
- Landing page dan gateway masuk untuk mahasiswa, dosen, dan admin
- Auth rebuild yang membaca tabel legacy `tbmhs`, `tbdosen`, dan `tbadmin`
- Dashboard mahasiswa, dosen, dan admin berbasis data legacy
- Modul native awal untuk praoutline, pengumuman, penawaran judul, early warning, dan CRUD admin prioritas
- Fondasi Laravel terpisah dari aplikasi legacy supaya migrasi bisa bertahap
## Run locally
```powershell
copy .env.example .env
php artisan serve
npm run dev
```
Atau build asset production:
```powershell
npm run build
```
## Docker and Coolify
Compose utama dan compose Coolify sekarang menjalankan Laravel rebuild dari `Dockerfile.rebuild` dengan document root Apache ke `rebuild/public`.
Untuk Coolify, gunakan compose file:
```text
docker-compose.coolify.yml
```
Environment yang sebaiknya diisi di Coolify:
```dotenv
APP_URL=https://domain-spota.example
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:isi_dengan_key_production
DB_USER=spota_user
DB_PASSWORD=spota_password
MYSQL_ROOT_PASSWORD=root_password_yang_kuat
DB_NAME=spota_spotadb
```
Jika `APP_KEY` kosong, container akan generate key otomatis saat start. Untuk production lebih aman isi `APP_KEY` permanen supaya session tidak berubah antar redeploy.
## Legacy database setup
Ubah `.env` agar rebuild memakai database SPOTA legacy, misalnya:
```dotenv
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3307
DB_DATABASE=spota_spotadb
DB_USERNAME=spota_user
DB_PASSWORD=spota_password
SESSION_DRIVER=file
```
Catatan:
- Dashboard mahasiswa membaca data real dari `tbmhs`, `tbpraoutline`, `tbrekaphasil`, `tbpengumuman`, `tmp_notif`, dan `tbjadwal`
- Dashboard dosen membaca data real dari modul praoutline, pengumuman, jadwal, penawaran judul, dan early warning
- Dashboard admin membaca data real dari master data, praoutline, pengumuman, jadwal, dan pengaturan prodi

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;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureLegacyRole
{
public function handle(Request $request, Closure $next, string $role): Response
{
$auth = $request->session()->get('legacy_auth');
if (! $auth || ($auth['role'] ?? null) !== $role) {
return redirect()->route('legacy.login', $role)
->withErrors(['identifier' => 'Silakan login terlebih dahulu untuk mengakses dashboard ini.']);
}
return $next($request);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Support;
class AdminNavigation
{
public static function build(array $user): array
{
$isSuper = ($user['lvl'] ?? null) === 'S';
$sections = [
[
'title' => 'Manajemen Data',
'icon' => 'folder',
'items' => $isSuper ? [
self::page('Data Fakultas', 'admin.data.fakultas', 'document'),
self::page('Data Jurusan', 'admin.data.jurusan', 'document'),
self::page('Data Program Studi', 'admin.data.prodi', 'document'),
] : [
self::page('Data Mahasiswa', 'admin.data.mahasiswa', 'users'),
self::page('Data Dosen', 'admin.data.dosen', 'users'),
self::page('Data Kelompok Keahlian', 'admin.data.kk', 'folder'),
],
],
[
'title' => 'User',
'icon' => 'user',
'items' => array_values(array_filter([
self::page('Profil Saya', 'admin.profile', 'user'),
$isSuper ? self::page('Manajemen Admin', 'admin.users', 'users') : null,
])),
],
[
'title' => 'Lainnya',
'icon' => 'briefcase',
'items' => [
['title' => 'Dokumen Sidang', 'href' => 'https://edoxid.untan.ac.id/', 'icon' => 'document', 'external' => true],
],
],
];
if (! $isSuper) {
array_splice($sections, 1, 0, [[
'title' => 'Praoutline',
'icon' => 'clipboard',
'items' => [
self::page('Daftar Draft Praoutline', 'admin.praoutline.index', 'document'),
self::page('Pencarian', 'admin.praoutline.search', 'search'),
self::page('Kep. Penunjukan Dosen', 'admin.praoutline.keputusan', 'clipboard'),
self::page('Kep. Draft Praoutline', 'admin.praoutline.kep-draft', 'clipboard'),
self::page('Pemberitahuan', 'admin.praoutline.pemberitahuan', 'bell'),
],
], [
'title' => 'Pengumuman',
'icon' => 'megaphone',
'items' => [
self::page('Daftar Pengumuman', 'admin.pengumuman.index', 'megaphone'),
self::page('Buat Pengumuman Baru', 'admin.pengumuman.create', 'document'),
],
], [
'title' => 'Jadwal Seminar/Sidang',
'icon' => 'clock',
'items' => [
self::page('Manajemen Data', 'admin.jadwal.index', 'document'),
self::page('Kalender', 'admin.jadwal.kalender', 'clock'),
],
], [
'title' => 'Pengaturan',
'icon' => 'warning',
'items' => [
self::page('Pengaturan Prodi', 'admin.pengaturan', 'warning'),
],
]]);
}
return [
'main' => [
['title' => 'Dashboard', 'href' => route('dashboard.admin'), 'icon' => 'home', 'active' => true],
],
'sections' => $sections,
];
}
private static function page(string $title, string $route, string $icon): array
{
return [
'title' => $title,
'href' => route($route, [], false),
'icon' => $icon,
];
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Support;
class DosenNavigation
{
public static function build(string $activeRoute): array
{
return [
'main' => [
['title' => 'Dashboard', 'href' => route('dashboard.dosen'), 'icon' => 'home', 'active' => $activeRoute === 'dashboard.dosen'],
],
'sections' => [
[
'title' => 'Utama',
'icon' => 'briefcase',
'items' => [
['title' => 'Penawaran Judul', 'href' => route('dosen.penawaran.index'), 'icon' => 'briefcase', 'active' => $activeRoute === 'dosen.penawaran.index'],
],
],
[
'title' => 'Tugas Akhir 1',
'icon' => 'folder',
'items' => [
['title' => 'Daftar Usulan', 'href' => route('dosen.praoutline.index'), 'icon' => 'folder', 'active' => $activeRoute === 'dosen.praoutline.index'],
['title' => 'Review Saya', 'href' => route('dosen.praoutline.review-saya'), 'icon' => 'chat', 'active' => $activeRoute === 'dosen.praoutline.review-saya'],
['title' => 'Pencarian Usulan', 'href' => route('dosen.praoutline.cari'), 'icon' => 'search', 'active' => $activeRoute === 'dosen.praoutline.cari'],
['title' => 'Daftar Bimbingan Saya', 'href' => route('dosen.praoutline.bimbingan'), 'icon' => 'users', 'active' => $activeRoute === 'dosen.praoutline.bimbingan'],
['title' => 'Statistik Usulan', 'href' => route('dosen.praoutline.statistik'), 'icon' => 'chart', 'active' => $activeRoute === 'dosen.praoutline.statistik'],
['title' => 'Pemberitahuan', 'href' => route('dosen.praoutline.pemberitahuan'), 'icon' => 'bell', 'active' => $activeRoute === 'dosen.praoutline.pemberitahuan'],
],
],
[
'title' => 'Lainnya',
'icon' => 'grid',
'items' => [
['title' => 'Pengumuman', 'href' => route('dosen.pengumuman.index'), 'icon' => 'megaphone', 'active' => $activeRoute === 'dosen.pengumuman.index' || $activeRoute === 'dosen.pengumuman.show'],
['title' => 'Akun Pengguna', 'href' => route('dosen.profile'), 'icon' => 'user', 'active' => $activeRoute === 'dosen.profile'],
['title' => 'Dokumen Sidang', 'href' => 'https://edoxid.untan.ac.id/', 'icon' => 'document', 'external' => true],
['title' => 'Early Warning', 'href' => route('dosen.early-warning'), 'icon' => 'warning', 'active' => $activeRoute === 'dosen.early-warning'],
['title' => 'Konsultasi Skripsi', 'href' => 'https://spota.untan.ac.id/konsultasi/', 'icon' => 'chat', 'external' => true],
['title' => 'Statistik Seminar', 'href' => 'https://spota.untan.ac.id/cek_banyak_sidang.php', 'icon' => 'chart', 'external' => true],
['title' => 'Konsultasi KP', 'href' => 'https://informatika.untan.ac.id/konsultasi/', 'icon' => 'chat', 'external' => true],
['title' => 'Pra LIRS (Dosen PA)', 'href' => route('dosen.pra-lirs'), 'icon' => 'clipboard', 'active' => $activeRoute === 'dosen.pra-lirs'],
['title' => 'Evaluasi Mahasiswa', 'href' => 'https://spota.untan.ac.id/steven/rekapMahasiswaEvaluasi.php?angkatan='.(date('Y') - 5).'&show=belumlulus', 'icon' => 'chart', 'external' => true],
],
],
],
];
}
}

18
rebuild/artisan Normal file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);

21
rebuild/bootstrap/app.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
use App\Http\Middleware\EnsureLegacyRole;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'legacy.role' => EnsureLegacyRole::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();

View File

@@ -0,0 +1,7 @@
<?php
use App\Providers\AppServiceProvider;
return [
AppServiceProvider::class,
];

86
rebuild/composer.json Normal file
View File

@@ -0,0 +1,86 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.3",
"laravel/framework": "^13.0",
"laravel/tinker": "^3.0"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.5",
"laravel/pao": "^1.0.6",
"laravel/pint": "^1.27",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^12.5.12"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"setup": [
"composer install",
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"npm install --ignore-scripts",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"test": [
"@php artisan config:clear --ansi @no_additional_args",
"@php artisan test"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

8253
rebuild/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

126
rebuild/config/app.php Normal file
View File

@@ -0,0 +1,126 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];

117
rebuild/config/auth.php Normal file
View File

@@ -0,0 +1,117 @@
<?php
use App\Models\User;
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the number of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];

130
rebuild/config/cache.php Normal file
View File

@@ -0,0 +1,130 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "octane",
| "failover", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
'failover' => [
'driver' => 'failover',
'stores' => [
'database',
'array',
],
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
/*
|--------------------------------------------------------------------------
| Serializable Classes
|--------------------------------------------------------------------------
|
| This value determines the classes that can be unserialized from cache
| storage. By default, no PHP classes will be unserialized from your
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
*/
'serializable_classes' => false,
];

184
rebuild/config/database.php Normal file
View File

@@ -0,0 +1,184 @@
<?php
use Illuminate\Support\Str;
use Pdo\Mysql;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
'transaction_mode' => 'DEFERRED',
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => env('DB_SSLMODE', 'prefer'),
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
],
];

View File

@@ -0,0 +1,80 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem disks as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];

132
rebuild/config/logging.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
|
| Available drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'handler_with' => [
'stream' => 'php://stderr',
],
'formatter' => env('LOG_STDERR_FORMATTER'),
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

118
rebuild/config/mail.php Normal file
View File

@@ -0,0 +1,118 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
*/
'default' => env('MAIL_MAILER', 'log'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
'retry_after' => 60,
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
'retry_after' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all emails sent by your application to be sent from
| the same address. Here you may specify a name and address that is
| used globally for all emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
],
];

129
rebuild/config/queue.php Normal file
View File

@@ -0,0 +1,129 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue supports a variety of backends via a single, unified
| API, giving you convenient access to each backend using identical
| syntax for each. The default queue connection is defined below.
|
*/
'default' => env('QUEUE_CONNECTION', 'database'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Laravel. You're also free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
| "deferred", "background", "failover", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
'deferred' => [
'driver' => 'deferred',
],
'background' => [
'driver' => 'background',
],
'failover' => [
'driver' => 'failover',
'connections' => [
'database',
'deferred',
],
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
];

View File

@@ -0,0 +1,38 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'key' => env('POSTMARK_API_KEY'),
],
'resend' => [
'key' => env('RESEND_API_KEY'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
];

233
rebuild/config/session.php Normal file
View File

@@ -0,0 +1,233 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option determines the default session driver that is utilized for
| incoming requests. Laravel supports a variety of storage options to
| persist session data. Database storage is a great default choice.
|
| Supported: "file", "cookie", "database", "memcached",
| "redis", "dynamodb", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env(
'SESSION_COOKIE',
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application, but you're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain without subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
/*
|--------------------------------------------------------------------------
| Session Serialization
|--------------------------------------------------------------------------
|
| This value controls the serialization strategy for session data, which
| is JSON by default. Setting this to "php" allows the storage of PHP
| objects in the session but can make an application vulnerable to
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
| Supported: "json", "php"
|
*/
'serialization' => 'json',
];

1
rebuild/database/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.sqlite*

View File

@@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}

View File

@@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedSmallInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};

View File

@@ -0,0 +1,25 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
}
}

1618
rebuild/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
rebuild/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"$schema": "https://www.schemastore.org/package.json",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^3.0.0",
"tailwindcss": "^4.0.0",
"vite": "^8.0.0"
}
}

36
rebuild/phpunit.xml Normal file
View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_URL" value=""/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="TELESCOPE_ENABLED" value="false"/>
<env name="NIGHTWATCH_ENABLED" value="false"/>
</php>
</phpunit>

25
rebuild/public/.htaccess Normal file
View File

@@ -0,0 +1,25 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Handle X-XSRF-Token Header
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

View File

20
rebuild/public/index.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
// Bootstrap Laravel and handle the request...
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@@ -0,0 +1,91 @@
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';
@theme {
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}
@layer base {
html {
scroll-behavior: smooth;
}
body {
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
}
::selection {
background: rgba(15, 23, 42, 0.12);
}
}
@layer components {
.section-card {
@apply rounded-xl bg-white shadow-[0_8px_12px_rgba(13,10,44,0.04)];
}
.muted-label {
@apply text-sm font-semibold text-[#2D68F8];
}
.metric-card {
@apply rounded-[20px] border border-slate-200 bg-slate-50 p-5;
}
.template-input {
@apply w-full rounded-md border border-[#D1D5DB] bg-white px-6 py-3.5 text-[#15171A] outline-hidden duration-200 placeholder:text-[#979797] focus:border-transparent focus:ring-2 focus:ring-[#5C6A78]/20;
}
.template-button-dark {
@apply inline-flex items-center justify-center rounded-md bg-[#15171A] px-5 py-3.5 font-medium text-white transition duration-200 hover:opacity-90;
}
.dashboard-shell {
@apply mx-auto flex max-w-[1400px] gap-6 px-4 pb-10 pt-24 sm:px-6 xl:px-8;
}
.dashboard-sidebar {
@apply hidden w-full max-w-[290px] shrink-0 lg:block;
}
.dashboard-sidebar-card {
@apply sticky top-24 rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)];
}
.dashboard-sidebar-link {
@apply flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-[#374151] transition hover:bg-[#F9FAFB] hover:text-[#15171A];
}
.dashboard-sidebar-link-active {
@apply bg-[#15171A] text-white hover:bg-[#15171A] hover:text-white;
}
.dashboard-sidebar-group {
@apply mt-5;
}
.dashboard-sidebar-group-label {
@apply flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-semibold text-[#15171A];
}
.dashboard-sidebar-submenu {
@apply ml-4 mt-1 space-y-1 border-l border-[#E5E7EB] pl-3.5;
}
.dashboard-sidebar-icon {
@apply inline-flex h-4.5 w-4.5 shrink-0 items-center justify-center text-[#7A8594];
}
.dashboard-sidebar-link-active .dashboard-sidebar-icon {
@apply text-white;
}
.dashboard-content {
@apply min-w-0 flex-1;
}
}

View File

@@ -0,0 +1 @@
//

View File

@@ -0,0 +1,94 @@
<x-admin.partials.page-shell :title="$title" :sidebar="$sidebar">
<section class="rounded-xl bg-white p-4 shadow-[0_8px_12px_rgba(13,10,44,0.04)] lg:p-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<ol class="flex flex-wrap items-center gap-2 text-sm text-[#6B7280]">
<li class="inline-flex items-center gap-2">
<span class="text-[#625DF5]">@include('dashboard.partials.icon', ['icon' => 'home'])</span>
<span>Home</span>
</li>
<li>/</li>
<li class="font-medium text-[#15171A]">{{ $pageTitle }}</li>
</ol>
<h1 class="mt-4 text-[26px] font-bold leading-[34px] text-[#15171A]">{{ $pageTitle }}</h1>
<p class="mt-2 max-w-[880px] text-sm leading-7 text-[#4B5563]">{{ $pageDescription }}</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $pageDate }}</div>
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $user['username'] }} · {{ $user['nmprodi'] }}</div>
</div>
</div>
</section>
@if (session('error'))
<div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">{{ session('error') }}</div>
@endif
@if ($errors->any())
<div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">
<p class="font-semibold">Import belum bisa diproses.</p>
<ul class="mt-2 list-disc space-y-1 pl-5">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="grid gap-5 lg:grid-cols-[minmax(0,1fr)_420px]">
<form method="POST" action="{{ route('admin.data.mahasiswa.import.store') }}" enctype="multipart/form-data" class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
@csrf
<div class="grid gap-5">
<div>
<label class="text-sm font-semibold text-[#15171A]">File CSV</label>
<input type="file" name="csv" accept=".csv,text/csv,text/plain" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm" required>
<p class="mt-2 text-xs leading-5 text-[#6B7280]">Maksimal 5 MB. Baris pertama harus berisi header.</p>
</div>
<div>
<label class="text-sm font-semibold text-[#15171A]">Mode Import</label>
<select name="mode" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm">
<option value="insert_only">Tambah data baru saja</option>
<option value="upsert">Tambah dan update jika NIM sudah ada</option>
</select>
</div>
<div>
<label class="text-sm font-semibold text-[#15171A]">Password Default</label>
<input type="text" name="default_password" value="123456" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm">
<p class="mt-2 text-xs leading-5 text-[#6B7280]">Dipakai jika kolom `password` kosong atau tidak ada pada baris mahasiswa baru.</p>
</div>
<div class="flex flex-wrap gap-3 border-t border-[#E5E7EB] pt-5">
<button class="rounded-md bg-[#15171A] px-5 py-2.5 text-sm font-medium text-white hover:opacity-90">Import CSV</button>
<a href="{{ route('admin.data.mahasiswa', [], false) }}" class="rounded-md border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#15171A] hover:bg-[#F9FAFB]">Batal</a>
</div>
</div>
</form>
<aside class="space-y-5">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h2 class="text-lg font-semibold text-[#15171A]">Format Header</h2>
<p class="mt-3 text-sm leading-7 text-[#6B7280]">Header wajib:</p>
<div class="mt-3 flex flex-wrap gap-2">
@foreach ($requiredHeaders as $header)
<span class="rounded-full bg-[#625DF5]/10 px-3 py-1 text-xs font-semibold text-[#625DF5]">{{ $header }}</span>
@endforeach
</div>
<p class="mt-5 text-sm leading-7 text-[#6B7280]">Header opsional:</p>
<div class="mt-3 flex flex-wrap gap-2">
@foreach ($optionalHeaders as $header)
<span class="rounded-full bg-[#F9FAFB] px-3 py-1 text-xs font-semibold text-[#4B5563]">{{ $header }}</span>
@endforeach
</div>
</section>
<section class="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h2 class="text-lg font-semibold text-[#15171A]">Contoh CSV</h2>
<pre class="mt-4 overflow-x-auto rounded-lg bg-[#15171A] p-4 text-xs leading-6 text-white"><code>nim,nama,email,thnmasuk,password,status,noHP,noHPOrtu,bolehUploadDraft
D1041231001,Budi Saputra,budi@example.com,2023,123456,A,08123456789,08129876543,1
D1041231002,Siti Aminah,siti@example.com,2023,,A,,,1</code></pre>
</section>
</aside>
</div>
</x-admin.partials.page-shell>

View File

@@ -0,0 +1,68 @@
<x-admin.partials.page-shell :title="$title" :sidebar="$sidebar">
<section class="rounded-xl bg-white p-4 shadow-[0_8px_12px_rgba(13,10,44,0.04)] lg:p-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<ol class="flex flex-wrap items-center gap-2 text-sm text-[#6B7280]">
<li class="inline-flex items-center gap-2">
<span class="text-[#625DF5]">@include('dashboard.partials.icon', ['icon' => 'home'])</span>
<span>Home</span>
</li>
<li>/</li>
<li class="font-medium text-[#15171A]">{{ $pageTitle }}</li>
</ol>
<h1 class="mt-4 text-[26px] font-bold leading-[34px] text-[#15171A]">{{ $pageTitle }}</h1>
<p class="mt-2 max-w-[880px] text-sm leading-7 text-[#4B5563]">{{ $pageDescription }}</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $pageDate }}</div>
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $user['username'] }} · {{ $user['nmprodi'] }}</div>
</div>
</div>
</section>
@if ($errors->any())
<div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">
<p class="font-semibold">Form belum bisa disimpan.</p>
<ul class="mt-2 list-disc space-y-1 pl-5">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('admin.pengumuman.store') }}" class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
@csrf
<div class="grid gap-5">
<div>
<label class="text-sm font-semibold text-[#15171A]">Judul</label>
<input name="judul" value="{{ old('judul') }}" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm focus:border-[#625DF5] focus:outline-none" required>
</div>
<div>
<label class="text-sm font-semibold text-[#15171A]">Isi Pengumuman</label>
<textarea name="isi" rows="8" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm focus:border-[#625DF5] focus:outline-none" required>{{ old('isi') }}</textarea>
</div>
<div class="grid gap-5 lg:grid-cols-2">
<div>
<label class="text-sm font-semibold text-[#15171A]">Tujuan</label>
<select name="tujuan" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm">
<option value="A" @selected(old('tujuan', 'A') === 'A')>Semua</option>
<option value="M" @selected(old('tujuan') === 'M')>Mahasiswa</option>
<option value="D" @selected(old('tujuan') === 'D')>Dosen</option>
</select>
</div>
<div>
<label class="text-sm font-semibold text-[#15171A]">Publish</label>
<select name="publish" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm">
<option value="Y" @selected(old('publish', 'Y') === 'Y')>Ya</option>
<option value="N" @selected(old('publish') === 'N')>Tidak</option>
</select>
</div>
</div>
<div class="flex flex-wrap gap-3 border-t border-[#E5E7EB] pt-5">
<button class="rounded-md bg-[#15171A] px-5 py-2.5 text-sm font-medium text-white hover:opacity-90">Simpan Pengumuman</button>
<a href="{{ route('admin.pengumuman.index', [], false) }}" class="rounded-md border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#15171A] hover:bg-[#F9FAFB]">Batal</a>
</div>
</div>
</form>
</x-admin.partials.page-shell>

View File

@@ -0,0 +1,59 @@
<x-admin.partials.page-shell :title="$title" :sidebar="$sidebar">
<section class="rounded-xl bg-white p-4 shadow-[0_8px_12px_rgba(13,10,44,0.04)] lg:p-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<ol class="flex flex-wrap items-center gap-2 text-sm text-[#6B7280]">
<li class="inline-flex items-center gap-2"><span class="text-[#625DF5]">@include('dashboard.partials.icon', ['icon' => 'home'])</span><span>Home</span></li>
<li>/</li>
<li class="font-medium text-[#15171A]">{{ $pageTitle }}</li>
</ol>
<h1 class="mt-4 text-[26px] font-bold leading-[34px] text-[#15171A]">{{ $pageTitle }}</h1>
<p class="mt-2 max-w-[880px] text-sm leading-7 text-[#4B5563]">{{ $pageDescription }}</p>
</div>
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $user['username'] }} · {{ $user['nmprodi'] }}</div>
</div>
</section>
@if ($errors->any())
<div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">
<p class="font-semibold">Form belum bisa disimpan.</p>
<ul class="mt-2 list-disc space-y-1 pl-5">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ $action }}" class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
@csrf
@if (($method ?? 'POST') !== 'POST')
@method($method)
@endif
<div class="grid gap-5 lg:grid-cols-2">
@foreach ($fields as $field)
<div class="{{ ($field['type'] ?? 'text') === 'textarea' ? 'lg:col-span-2' : '' }}">
<label class="text-sm font-semibold text-[#15171A]">{{ $field['label'] }}</label>
@if (($field['type'] ?? 'text') === 'select')
<select name="{{ $field['name'] }}" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm">
@foreach ($field['options'] as $value => $label)
<option value="{{ $value }}" @selected((string) old($field['name'], $field['value'] ?? '') === (string) $value)>{{ $label }}</option>
@endforeach
</select>
@elseif (($field['type'] ?? 'text') === 'textarea')
<textarea name="{{ $field['name'] }}" rows="7" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm focus:border-[#625DF5] focus:outline-none">{{ old($field['name'], $field['value'] ?? '') }}</textarea>
@else
<input type="{{ $field['type'] ?? 'text' }}" name="{{ $field['name'] }}" value="{{ old($field['name'], $field['value'] ?? '') }}" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm focus:border-[#625DF5] focus:outline-none" @if(!empty($field['required'])) required @endif>
@endif
@if (!empty($field['help']))
<p class="mt-2 text-xs leading-5 text-[#6B7280]">{{ $field['help'] }}</p>
@endif
</div>
@endforeach
</div>
<div class="mt-6 flex flex-wrap gap-3 border-t border-[#E5E7EB] pt-5">
<button class="rounded-md bg-[#15171A] px-5 py-2.5 text-sm font-medium text-white hover:opacity-90">Simpan</button>
<a href="{{ $cancel }}" class="rounded-md border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#15171A] hover:bg-[#F9FAFB]">Batal</a>
</div>
</form>
</x-admin.partials.page-shell>

View File

@@ -0,0 +1,101 @@
<x-admin.partials.page-shell :title="$title" :sidebar="$sidebar">
<section class="rounded-xl bg-white p-4 shadow-[0_8px_12px_rgba(13,10,44,0.04)] lg:p-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<ol class="flex flex-wrap items-center gap-2 text-sm text-[#6B7280]">
<li class="inline-flex items-center gap-2">
<span class="text-[#625DF5]">@include('dashboard.partials.icon', ['icon' => 'home'])</span>
<span>Home</span>
</li>
<li>/</li>
<li class="font-medium text-[#15171A]">{{ $pageTitle }}</li>
</ol>
<h1 class="mt-4 text-[26px] font-bold leading-[34px] text-[#15171A]">{{ $pageTitle }}</h1>
<p class="mt-2 max-w-[880px] text-sm leading-7 text-[#4B5563]">{{ $pageDescription }}</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $pageDate }}</div>
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $user['username'] }} · {{ $user['nmprodi'] }}</div>
<form method="POST" action="{{ route('legacy.logout') }}">
@csrf
<button type="submit" class="rounded-md bg-[#15171A] px-5.5 py-2.5 text-sm font-medium text-white hover:opacity-90">Logout</button>
</form>
</div>
</div>
@if (!empty($actions ?? []))
<div class="mt-5 flex flex-wrap gap-3 border-t border-[#E5E7EB] pt-5">
@foreach ($actions as $action)
<a href="{{ $action['href'] }}" class="{{ ($action['variant'] ?? 'light') === 'dark' ? 'rounded-md bg-[#15171A] px-4 py-2.5 text-sm font-medium text-white hover:opacity-90' : 'rounded-md border border-[#D1D5DB] bg-white px-4 py-2.5 text-sm font-medium text-[#15171A] hover:bg-[#F9FAFB]' }}">{{ $action['label'] }}</a>
@endforeach
</div>
@endif
</section>
@if (session('success'))
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
@endif
@if (!empty($search ?? false))
<form method="GET" class="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto]">
<input name="q" value="{{ $keyword ?? '' }}" placeholder="Cari judul, nama mahasiswa, atau NIM" class="rounded-md border border-[#D1D5DB] px-4 py-3 text-sm focus:border-[#625DF5] focus:outline-none">
<button type="submit" class="rounded-md bg-[#15171A] px-5 py-3 text-sm font-medium text-white hover:opacity-90">Cari</button>
</div>
</form>
@endif
<section class="overflow-hidden rounded-xl border border-[#E5E7EB] bg-white shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="overflow-x-auto">
<table class="min-w-[900px] w-full text-left text-sm">
<thead class="bg-[#F9FAFB] text-xs uppercase tracking-[0.12em] text-[#6B7280]">
<tr>
<th class="px-4 py-3">No.</th>
@foreach ($columns as $column)
<th class="px-4 py-3">{{ $column }}</th>
@endforeach
@if (!empty($rowActions ?? null))
<th class="px-4 py-3 text-right">Aksi</th>
@endif
</tr>
</thead>
<tbody class="divide-y divide-[#E5E7EB]">
@forelse ($rows as $row)
<tr>
<td class="px-4 py-4 text-[#6B7280]">{{ method_exists($rows, 'currentPage') ? $loop->iteration + ($rows->currentPage() - 1) * $rows->perPage() : $loop->iteration }}</td>
@foreach ($map($row) as $value)
<td class="px-4 py-4 text-[#374151]">{!! is_string($value) ? e($value) : $value !!}</td>
@endforeach
@if (!empty($rowActions ?? null))
<td class="px-4 py-4">
<div class="flex justify-end gap-2 whitespace-nowrap">
@foreach ($rowActions($row) as $action)
@if (($action['method'] ?? 'GET') === 'GET')
<a href="{{ $action['href'] }}" class="rounded-md border border-[#D1D5DB] px-3 py-2 text-xs font-medium text-[#15171A] hover:bg-[#F9FAFB]">{{ $action['label'] }}</a>
@else
<form method="POST" action="{{ $action['href'] }}" onsubmit="return confirm('{{ $action['confirm'] ?? 'Lanjutkan aksi ini?' }}')">
@csrf
@method($action['method'])
<button class="rounded-md bg-rose-600 px-3 py-2 text-xs font-medium text-white hover:opacity-90">{{ $action['label'] }}</button>
</form>
@endif
@endforeach
</div>
</td>
@endif
</tr>
@empty
<tr>
<td colspan="{{ count($columns) + 1 + (!empty($rowActions ?? null) ? 1 : 0) }}" class="px-4 py-8 text-center text-[#6B7280]">Belum ada data.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if (method_exists($rows, 'links'))
<div class="border-t border-[#E5E7EB] px-4 py-3">
{{ $rows->links() }}
</div>
@endif
</section>
</x-admin.partials.page-shell>

View File

@@ -0,0 +1,74 @@
<x-layouts.app :title="$meta['title']">
<section class="bg-[#F9FAFB] pb-15 pt-34 lg:pb-20 lg:pt-39">
<div class="mx-auto max-w-[1170px] px-4 sm:px-8 xl:px-0">
<div class="grid gap-6 lg:grid-cols-[minmax(0,1.05fr)_minmax(360px,440px)] lg:items-start">
<article class="rounded-xl bg-white p-7 shadow-[0_8px_12px_rgba(13,10,44,0.04)] sm:p-9">
<span class="inline-flex rounded-full bg-[#625DF5]/[0.08] px-3 py-1 text-sm font-medium text-[#625DF5]">{{ $meta['eyebrow'] }}</span>
<h1 class="mt-5 max-w-[760px] text-[28px] font-bold leading-[38px] text-[#15171A] sm:text-[38px] sm:leading-[48px]">{{ $meta['heading'] }}</h1>
<p class="mt-5 max-w-[760px] text-sm leading-8 text-[#4B5563] sm:text-base">{{ $meta['description'] }}</p>
<div class="mt-8 rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-5">
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-[#979797]">{{ $meta['summary_title'] }}</p>
<div class="mt-4 space-y-3 text-sm leading-7 text-[#374151]">
@foreach ($meta['summary_points'] as $point)
<div class="rounded-lg bg-white px-4 py-3">{{ $point }}</div>
@endforeach
</div>
</div>
<div class="mt-6 rounded-xl border border-[#E5E7EB] bg-white p-5 text-sm leading-7 text-[#4B5563]">
<p class="font-semibold text-[#15171A]">Informasi Login</p>
<p class="mt-2">{{ $meta['status_note'] }}</p>
<p class="mt-2">{{ $meta['help_note'] }}</p>
</div>
</article>
<div class="rounded-xl bg-white p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)] sm:p-7.5 xl:p-9">
<div class="mb-6 grid grid-cols-2 rounded-[14px] bg-[#F9FAFB] p-1.5 text-sm font-medium text-[#15171A]">
<a href="{{ route('legacy.login', 'mahasiswa') }}" class="rounded-[10px] px-4 py-3 text-center {{ $role === 'mahasiswa' ? 'bg-white shadow-[0_4px_10px_rgba(13,10,44,0.05)]' : 'text-[#5C6A78]' }}">Mahasiswa</a>
<a href="{{ route('legacy.login', 'dosen') }}" class="rounded-[10px] px-4 py-3 text-center {{ $role === 'dosen' ? 'bg-white shadow-[0_4px_10px_rgba(13,10,44,0.05)]' : 'text-[#5C6A78]' }}">Dosen</a>
</div>
<div class="mb-6 inline-flex rounded-full border border-[#E5E7EB] bg-[#F9FAFB] px-3 py-1.5 text-sm font-medium text-[#15171A]">
{{ $meta['status'] }}
</div>
@if ($errors->any())
<div class="mb-6 rounded-md border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
<ul class="space-y-1">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('legacy.authenticate', $role) }}">
@csrf
<div class="mb-4">
<label for="identifier" class="mb-3 block text-[14px] font-medium leading-[22px] text-[#15171A]">{{ $meta['input'] }}</label>
<input id="identifier" name="identifier" type="text" value="{{ old('identifier') }}" placeholder="{{ $meta['placeholder'] }}" class="template-input">
</div>
@if ($role === 'dosen')
<div class="mb-4">
<label for="prodi" class="mb-3 block text-[14px] font-medium leading-[22px] text-[#15171A]">Program Studi</label>
<input id="prodi" type="text" value="Informatika" disabled class="template-input bg-[#F9FAFB] text-[#5C6A78]">
</div>
@endif
<div class="mb-5">
<label for="password" class="mb-3 block text-[14px] font-medium leading-[22px] text-[#15171A]">Password</label>
<input id="password" name="password" type="password" placeholder="Masukkan password" class="template-input">
</div>
<div class="mb-7 flex items-center justify-between text-sm text-[#374151]">
<span>{{ $role === 'mahasiswa' ? 'Gunakan akun mahasiswa aktif' : ($role === 'dosen' ? 'Gunakan akun dosen aktif' : 'Akses admin belum tersedia') }}</span>
<a href="{{ route('role-login') }}" class="text-[#15171A]">Kembali</a>
</div>
<button type="submit" class="flex w-full justify-center rounded-md bg-[#15171A] px-5 py-3.5 font-medium text-white hover:opacity-90 {{ $role === 'admin' ? 'opacity-75' : '' }}">{{ $role === 'mahasiswa' ? 'Masuk ke Portal Mahasiswa' : ($role === 'dosen' ? 'Masuk ke Dashboard Dosen' : 'Coba Masuk') }}</button>
</form>
<p class="mt-5 text-sm leading-7 text-[#5C6A78]">{{ $role === 'mahasiswa' ? 'Login mahasiswa tetap memakai data autentikasi SPOTA yang aktif saat ini.' : ($role === 'dosen' ? 'Login dosen membaca akun legacy aktif dengan struktur session yang mengikuti sistem lama.' : 'Halaman admin akan dibuka setelah modul utama selesai dipindahkan.') }}</p>
</div>
</div>
</div>
</section>
</x-layouts.app>

View File

@@ -0,0 +1,43 @@
<x-layouts.app :title="$title">
<main>
<section class="relative z-10 overflow-hidden rounded-b-[50px] pb-15 pt-34">
<div class="absolute bottom-0 left-0 h-full w-full rounded-b-[50px] bg-[#F9FAFB]"></div>
<div class="mx-auto relative z-10 max-w-[1170px] px-4 sm:px-8 xl:px-0">
<div class="grid gap-6 lg:grid-cols-[minmax(0,1.3fr)_minmax(320px,0.7fr)] lg:items-start">
<article class="rounded-xl bg-white p-7 shadow-[0_8px_12px_rgba(13,10,44,0.04)] sm:p-9">
<span class="inline-flex rounded-full bg-[#625DF5]/[0.08] px-3 py-1 text-sm font-medium text-[#625DF5]">{{ $intro['eyebrow'] }}</span>
<h1 class="mt-5 max-w-[820px] text-[28px] font-bold leading-[38px] text-[#15171A] sm:text-[38px] sm:leading-[48px]">{{ $intro['heading'] }}</h1>
<p class="mt-5 max-w-[820px] text-sm leading-8 text-[#4B5563] sm:text-base">{{ $intro['description'] }}</p>
<div class="mt-8 grid gap-4">
@foreach ($intro['details'] as $detail)
<div class="rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] px-5 py-4 text-sm leading-7 text-[#374151]">
{{ $detail }}
</div>
@endforeach
</div>
</article>
<aside class="rounded-xl bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)] sm:p-7">
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-[#979797]">Akses Akun</p>
<h2 class="mt-3 text-[24px] font-semibold leading-[32px] text-[#15171A]">Pilih peran pengguna</h2>
<p class="mt-3 text-sm leading-7 text-[#5C6A78]">Gunakan akun yang sudah aktif pada SPOTA. Saat ini rebuild berfokus pada alur mahasiswa dan dosen.</p>
<div class="mt-6 space-y-4">
@foreach ($roles as $role)
<a href="{{ route('legacy.login', $role['slug']) }}" class="block rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] px-5 py-4 transition hover:border-[#15171A] hover:bg-white">
<p class="text-base font-semibold text-[#15171A]">{{ $role['name'] }}</p>
<p class="mt-2 text-sm leading-7 text-[#5C6A78]">{{ $role['summary'] }}</p>
</a>
@endforeach
</div>
<div class="mt-6 rounded-xl border border-[#E5E7EB] bg-white px-5 py-4 text-sm leading-7 text-[#4B5563]">
Jika belum yakin masuk sebagai siapa, mulai dari peran yang memang digunakan sehari-hari pada proses tugas akhir di SPOTA.
</div>
</aside>
</div>
</div>
</section>
</main>
</x-layouts.app>

View File

@@ -0,0 +1,41 @@
<x-layouts.app :title="$title" :dashboard-layout="true">
<main class="min-h-screen bg-[#F9FAFB]">
<div class="dashboard-shell">
<aside class="dashboard-sidebar">
<div class="dashboard-sidebar-card">
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-[#979797]">Menu Admin</p>
<div class="mt-4 space-y-1">
@foreach ($sidebar['main'] as $menu)
<a href="{{ $menu['href'] }}" class="dashboard-sidebar-link {{ !empty($menu['active']) ? 'dashboard-sidebar-link-active' : '' }}">
<span class="dashboard-sidebar-icon">@include('dashboard.partials.icon', ['icon' => $menu['icon']])</span>
<span>{{ $menu['title'] }}</span>
</a>
@endforeach
</div>
<div class="mt-6 space-y-5 border-t border-[#E5E7EB] pt-6">
@foreach ($sidebar['sections'] as $group)
<section class="dashboard-sidebar-group">
<div class="dashboard-sidebar-group-label">
<span class="dashboard-sidebar-icon">@include('dashboard.partials.icon', ['icon' => $group['icon']])</span>
<span>{{ $group['title'] }}</span>
</div>
<div class="dashboard-sidebar-submenu">
@foreach ($group['items'] as $item)
<a href="{{ $item['href'] }}" @if(!empty($item['external'])) target="_blank" rel="noreferrer" @endif class="dashboard-sidebar-link">
<span class="dashboard-sidebar-icon">@include('dashboard.partials.icon', ['icon' => $item['icon']])</span>
<span>{{ $item['title'] }}</span>
</a>
@endforeach
</div>
</section>
@endforeach
</div>
</div>
</aside>
<div class="dashboard-content space-y-5">
{{ $slot }}
</div>
</div>
</main>
</x-layouts.app>

View File

@@ -0,0 +1,86 @@
<x-layouts.app :title="$title" :dashboard-layout="true">
<main class="min-h-screen bg-[#F9FAFB]">
<div class="dashboard-shell">
<aside class="dashboard-sidebar">
<div class="dashboard-sidebar-card">
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-[#979797]">Menu Dosen</p>
<div class="mt-4 space-y-1">
@foreach ($sidebar['main'] as $menu)
<a href="{{ $menu['href'] }}" class="dashboard-sidebar-link {{ !empty($menu['active']) ? 'dashboard-sidebar-link-active' : '' }}">
<span class="dashboard-sidebar-icon">@include('dashboard.partials.icon', ['icon' => $menu['icon']])</span>
<span>{{ $menu['title'] }}</span>
</a>
@endforeach
</div>
<div class="mt-6 space-y-5 border-t border-[#E5E7EB] pt-6">
@foreach ($sidebar['sections'] as $group)
<section class="dashboard-sidebar-group">
<div class="dashboard-sidebar-group-label">
<span class="dashboard-sidebar-icon">@include('dashboard.partials.icon', ['icon' => $group['icon']])</span>
<span>{{ $group['title'] }}</span>
</div>
<div class="dashboard-sidebar-submenu">
@foreach ($group['items'] as $item)
<a href="{{ $item['href'] }}" @if(!empty($item['external'])) target="_blank" rel="noreferrer" @endif class="dashboard-sidebar-link {{ !empty($item['active']) ? 'dashboard-sidebar-link-active' : '' }}">
<span class="dashboard-sidebar-icon">@include('dashboard.partials.icon', ['icon' => $item['icon']])</span>
<span>{{ $item['title'] }}</span>
</a>
@endforeach
</div>
</section>
@endforeach
</div>
</div>
</aside>
<div class="dashboard-content space-y-5">
<div class="rounded-xl bg-white p-4 shadow-[0_8px_12px_rgba(13,10,44,0.04)] lg:p-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<ol class="flex flex-wrap items-center gap-2 text-sm text-[#6B7280]">
<li class="inline-flex items-center gap-2">
<span class="text-[#625DF5]">@include('dashboard.partials.icon', ['icon' => 'home'])</span>
<span>Home</span>
</li>
<li>/</li>
<li class="font-medium text-[#15171A]">{{ $pageTitle }}</li>
</ol>
<h1 class="mt-4 text-[26px] font-bold leading-[34px] text-[#15171A]">{{ $pageTitle }}</h1>
<p class="mt-2 max-w-[880px] text-sm leading-7 text-[#4B5563]">{{ $pageDescription }}</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $pageDate }}</div>
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $user['nip'] }} · {{ $user['nmprodi'] }}</div>
<form method="POST" action="{{ route('legacy.logout') }}">
@csrf
<button type="submit" class="rounded-md bg-[#15171A] px-5.5 py-2.5 text-sm font-medium text-white hover:opacity-90">Logout</button>
</form>
</div>
</div>
@if (!empty($pageActions ?? []))
<div class="mt-5 flex flex-wrap gap-3 border-t border-[#E5E7EB] pt-5">
@foreach ($pageActions as $action)
<a href="{{ $action['href'] }}" @if(!empty($action['external'])) target="_blank" rel="noreferrer" @endif class="{{ ($action['variant'] ?? 'light') === 'dark' ? 'rounded-md bg-[#15171A] px-4 py-2.5 text-sm font-medium text-white hover:opacity-90' : 'rounded-md border border-[#D1D5DB] bg-white px-4 py-2.5 text-sm font-medium text-[#15171A] hover:bg-[#F9FAFB]' }}">{{ $action['label'] }}</a>
@endforeach
</div>
@endif
</div>
@if (session('success'))
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">
{{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">
{{ session('error') }}
</div>
@endif
{{ $slot }}
</div>
</div>
</main>
</x-layouts.app>

View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $title ?? 'SPOTA Rebuild' }}</title>
<meta name="description" content="Rebuild modern SPOTA Universitas Tanjungpura dengan Laravel Blade dan Tailwind.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="min-h-screen bg-white font-[Inter] text-[#374151] antialiased">
@php
$legacyAuth = session('legacy_auth');
$legacyRole = $legacyAuth['role'] ?? null;
$spotaLogoUrl = route('assets.spota-logo');
$dashboardRoute = match ($legacyRole) {
'mahasiswa' => route('dashboard.mahasiswa'),
'dosen' => route('dashboard.dosen'),
default => null,
};
$isDashboardShell = isset($dashboardLayout) && $dashboardLayout === true;
@endphp
<div class="min-h-screen">
<header class="fixed left-0 top-0 z-40 w-full border-b border-[#E5E7EB] bg-white">
<div class="mx-auto flex max-w-[1400px] items-center justify-between px-4 py-4 sm:px-6 xl:px-8 lg:relative">
<div class="flex w-full items-center justify-between lg:w-3/12">
<a href="{{ route('home') }}" class="inline-flex items-center">
<img src="{{ $spotaLogoUrl }}" alt="SPOTA" class="h-11 w-auto max-w-[180px] object-contain">
</a>
</div>
<div class="hidden w-full items-center justify-between lg:flex lg:w-9/12">
@if (! $isDashboardShell)
<nav>
<ul class="flex items-center gap-10 text-sm text-[#374151]">
<li><a href="{{ route('home') }}" class="hover:text-[#15171A]">Beranda</a></li>
<li><a href="{{ route('role-login') }}" class="hover:text-[#15171A]">Pilih Akun</a></li>
<li><a href="{{ route('legacy.login', 'mahasiswa') }}" class="hover:text-[#15171A]">Mahasiswa</a></li>
<li><a href="{{ route('legacy.login', 'dosen') }}" class="hover:text-[#15171A]">Dosen</a></li>
</ul>
</nav>
@else
<div class="text-sm text-[#5C6A78]">
{{ $legacyAuth['user']['nama_lengkap'] ?? 'Pengguna' }}
</div>
@endif
@if ($dashboardRoute && $isDashboardShell)
<div class="flex items-center gap-3">
<a href="{{ route('role-login') }}" class="rounded-md border border-[#E5E7EB] bg-white px-5.5 py-2.5 text-sm font-medium text-[#15171A] hover:bg-[#F3F4F6]">Portal</a>
<form method="POST" action="{{ route('legacy.logout') }}">
@csrf
<button type="submit" class="rounded-md bg-[#15171A] px-5.5 py-2.5 text-sm font-medium text-white hover:opacity-90">Logout</button>
</form>
</div>
@else
<a href="{{ route('legacy.login', 'mahasiswa') }}" class="rounded-md bg-[#15171A] px-5.5 py-2.5 text-sm font-medium text-white hover:opacity-90">Masuk</a>
@endif
</div>
</div>
</header>
{{ $slot }}
</div>
</body>
</html>

View File

@@ -0,0 +1,97 @@
<x-layouts.app :title="$title" :dashboard-layout="true">
<main class="min-h-screen bg-[#F9FAFB]">
<div class="dashboard-shell">
<aside class="dashboard-sidebar">
<div class="dashboard-sidebar-card">
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-[#979797]">Menu Mahasiswa</p>
<div class="mt-4 space-y-1">
@foreach ($sidebar['main'] as $menu)
<a href="{{ $menu['href'] }}" class="dashboard-sidebar-link {{ !empty($menu['active']) ? 'dashboard-sidebar-link-active' : '' }}">
<span class="dashboard-sidebar-icon">@include('dashboard.partials.icon', ['icon' => $menu['icon']])</span>
<span>{{ $menu['title'] }}</span>
</a>
@endforeach
</div>
<div class="mt-6 space-y-5 border-t border-[#E5E7EB] pt-6">
@foreach ($sidebar['sections'] as $group)
<section class="dashboard-sidebar-group">
<div class="dashboard-sidebar-group-label">
<span class="dashboard-sidebar-icon">@include('dashboard.partials.icon', ['icon' => $group['icon']])</span>
<span>{{ $group['title'] }}</span>
</div>
<div class="dashboard-sidebar-submenu">
@foreach ($group['items'] as $item)
<a href="{{ $item['href'] }}" class="dashboard-sidebar-link {{ !empty($item['active']) ? 'dashboard-sidebar-link-active' : '' }}">
<span class="dashboard-sidebar-icon">@include('dashboard.partials.icon', ['icon' => $item['icon']])</span>
<span>{{ $item['title'] }}</span>
</a>
@endforeach
</div>
</section>
@endforeach
</div>
</div>
</aside>
<div class="dashboard-content space-y-5">
<div class="rounded-xl bg-white p-4 shadow-[0_8px_12px_rgba(13,10,44,0.04)] lg:p-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<ol class="flex flex-wrap items-center gap-2 text-sm text-[#6B7280]">
<li class="inline-flex items-center gap-2">
<span class="text-[#625DF5]">@include('dashboard.partials.icon', ['icon' => 'home'])</span>
<span>Home</span>
</li>
<li>/</li>
<li class="font-medium text-[#15171A]">{{ $pageTitle }}</li>
</ol>
<h1 class="mt-4 text-[26px] font-bold leading-[34px] text-[#15171A]">{{ $pageTitle }}</h1>
<p class="mt-2 max-w-[880px] text-sm leading-7 text-[#4B5563]">{{ $pageDescription }}</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $pageDate }}</div>
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $user['nim'] }} · {{ $user['nmprodi'] }}</div>
<form method="POST" action="{{ route('legacy.logout') }}">
@csrf
<button type="submit" class="rounded-md bg-[#15171A] px-5.5 py-2.5 text-sm font-medium text-white hover:opacity-90">Logout</button>
</form>
</div>
</div>
@if (!empty($pageActions ?? []))
<div class="mt-5 flex flex-wrap gap-3 border-t border-[#E5E7EB] pt-5">
@foreach ($pageActions as $action)
<a href="{{ $action['href'] }}" class="{{ ($action['variant'] ?? 'light') === 'dark' ? 'rounded-md bg-[#15171A] px-4 py-2.5 text-sm font-medium text-white hover:opacity-90' : 'rounded-md border border-[#D1D5DB] bg-white px-4 py-2.5 text-sm font-medium text-[#15171A] hover:bg-[#F9FAFB]' }}">{{ $action['label'] }}</a>
@endforeach
</div>
@endif
</div>
@if (session('success'))
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">
{{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">
{{ session('error') }}
</div>
@endif
@if ($errors->any())
<div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">
<p class="font-semibold">Form belum bisa disimpan.</p>
<ul class="mt-2 list-disc space-y-1 pl-5">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{{ $slot }}
</div>
</div>
</main>
</x-layouts.app>

View File

@@ -0,0 +1,87 @@
<x-admin.partials.page-shell :title="$title" :sidebar="$sidebar">
<section class="rounded-xl bg-white p-4 shadow-[0_8px_12px_rgba(13,10,44,0.04)] lg:p-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<ol class="flex flex-wrap items-center gap-2 text-sm text-[#6B7280]">
<li class="inline-flex items-center gap-2">
<span class="text-[#625DF5]">@include('dashboard.partials.icon', ['icon' => 'home'])</span>
<span>Home</span>
</li>
<li>/</li>
<li class="font-medium text-[#15171A]">Dashboard</li>
</ol>
<h1 class="mt-4 text-[26px] font-bold leading-[34px] text-[#15171A]">Dashboard Admin</h1>
<p class="mt-2 max-w-[880px] text-sm leading-7 text-[#4B5563]">Selamat datang di halaman administrator Sistem Pendukung Outline Tugas Akhir (SPOTA) Universitas Tanjungpura.</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $pageDate }}</div>
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $user['username'] }} · {{ $user['nmprodi'] }}</div>
<form method="POST" action="{{ route('legacy.logout') }}">
@csrf
<button type="submit" class="rounded-md bg-[#15171A] px-5.5 py-2.5 text-sm font-medium text-white hover:opacity-90">Logout</button>
</form>
</div>
</div>
</section>
<section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
@foreach ($stats as $stat)
<article class="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<p class="text-sm font-semibold text-[#5C6A78]">{{ $stat['label'] }}</p>
<p class="mt-3 text-3xl font-bold text-[#15171A]">{{ $stat['value'] }}</p>
<p class="mt-3 text-sm leading-7 text-[#6B7280]">{{ $stat['note'] }}</p>
</article>
@endforeach
</section>
<div class="grid gap-5 lg:grid-cols-[minmax(0,1fr)_380px]">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h2 class="text-xl font-semibold text-[#15171A]">Menu Kerja Administrator</h2>
<p class="mt-2 text-sm leading-7 text-[#6B7280]">Link di bawah mengikuti struktur fungsi administrator lama. Rebuild ini menjaga akses menuju halaman yang sudah aktif tanpa mengubah proses data yang berjalan.</p>
<div class="mt-5 grid gap-4 md:grid-cols-2">
@foreach ($sidebar['sections'] as $group)
<article class="rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-4">
<div class="flex items-center gap-2 font-semibold text-[#15171A]">
<span class="text-[#625DF5]">@include('dashboard.partials.icon', ['icon' => $group['icon']])</span>
<span>{{ $group['title'] }}</span>
</div>
<div class="mt-4 space-y-2">
@foreach ($group['items'] as $item)
<a href="{{ $item['href'] }}" @if(!empty($item['external'])) target="_blank" rel="noreferrer" @endif class="flex items-center justify-between rounded-md bg-white px-3 py-2 text-sm text-[#374151] hover:text-[#625DF5]">
<span>{{ $item['title'] }}</span>
<span>@include('dashboard.partials.icon', ['icon' => 'search'])</span>
</a>
@endforeach
</div>
</article>
@endforeach
</div>
</section>
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-[#979797]">Jadwal</p>
<h2 class="mt-2 text-xl font-semibold text-[#15171A]">Seminar/Sidang Terbaru</h2>
</div>
<a href="{{ route('admin.jadwal.kalender', [], false) }}" class="rounded-md border border-[#D1D5DB] px-3 py-2 text-xs font-medium text-[#15171A] hover:bg-[#F9FAFB]">Kalender</a>
</div>
<div class="mt-5 space-y-3">
@forelse ($schedules as $schedule)
<article class="rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-4">
<div class="flex items-start justify-between gap-3">
<div>
<p class="font-semibold text-[#15171A]">{{ $schedule['jenis'] }}</p>
<p class="mt-1 text-sm text-[#6B7280]">{{ $schedule['mahasiswa'] }}</p>
</div>
<span class="rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-[#4B5563]">{{ $schedule['ruangan'] }}</span>
</div>
<p class="mt-3 text-sm text-[#4B5563]">{{ $schedule['tanggal'] }}</p>
</article>
@empty
<div class="rounded-xl border border-dashed border-[#D1D5DB] bg-[#F9FAFB] p-5 text-center text-sm text-[#6B7280]">Belum ada jadwal seminar/sidang.</div>
@endforelse
</div>
</section>
</div>
</x-admin.partials.page-shell>

View File

@@ -0,0 +1,128 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar ?? $dashboard['sidebar']" :page-title="$dashboard['pageTitle']" :page-description="$dashboard['welcomeText']" :page-date="$dashboard['dateLabel']" :user="$user">
<section>
<article class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)] lg:p-7">
<h2 class="text-[22px] font-semibold leading-[30px] text-[#15171A]">{{ $dashboard['welcomeTitle'] }}</h2>
<p class="mt-3 max-w-[920px] text-[15px] leading-7 text-[#4B5563]">{{ $dashboard['welcomeText'] }}</p>
@if ($dashboard['androidLink'])
<div class="mt-5">
<a href="{{ $dashboard['androidLink'] }}" target="_blank" rel="noreferrer" class="inline-flex items-center rounded-md bg-[#2D68F8] px-4 py-2.5 text-sm font-medium text-white hover:opacity-90">Unduh Aplikasi SPOTA Android</a>
</div>
@endif
</article>
</section>
<section class="grid gap-5 lg:grid-cols-3">
<article class="rounded-xl border border-[#F2D38B] bg-[#FFF9E9] p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="flex items-start gap-3">
<div class="mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#FDE7AE] text-[#A16207]">
@include('dashboard.partials.icon', ['icon' => 'warning'])
</div>
<div class="min-w-0 flex-1">
<h3 class="text-lg font-semibold text-[#15171A]">{{ $dashboard['announcementNotice']['title'] }}</h3>
<p class="mt-2 text-sm leading-7 text-[#5B4A1C]">{{ $dashboard['announcementNotice']['message'] }}</p>
<div class="mt-4">
<a href="{{ $dashboard['announcementNotice']['primaryHref'] }}" class="inline-flex items-center rounded-md bg-[#EAB308] px-4 py-2.5 text-sm font-medium text-[#15171A] hover:opacity-90">{{ $dashboard['announcementNotice']['primaryLabel'] }}</a>
</div>
</div>
</div>
</article>
<article class="rounded-xl border border-[#F2D38B] bg-[#FFF9E9] p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="flex items-start gap-3">
<div class="mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#FDE7AE] text-[#A16207]">
@include('dashboard.partials.icon', ['icon' => 'warning'])
</div>
<div class="min-w-0 flex-1">
<h3 class="text-lg font-semibold text-[#15171A]">{{ $dashboard['proposalNotice']['title'] }}</h3>
<p class="mt-2 text-sm leading-7 text-[#5B4A1C]">{{ $dashboard['proposalNotice']['message'] }}</p>
<div class="mt-4 flex flex-wrap gap-3">
<a href="{{ $dashboard['proposalNotice']['primaryHref'] }}" class="inline-flex items-center rounded-md bg-[#EAB308] px-4 py-2.5 text-sm font-medium text-[#15171A] hover:opacity-90">{{ $dashboard['proposalNotice']['primaryLabel'] }}</a>
@if ($dashboard['proposalNotice']['secondaryLabel'])
<a href="{{ $dashboard['proposalNotice']['secondaryHref'] }}" class="inline-flex items-center rounded-md border border-[#E5C35C] bg-white px-4 py-2.5 text-sm font-medium text-[#15171A] hover:bg-[#FFFDF5]">{{ $dashboard['proposalNotice']['secondaryLabel'] }}</a>
@endif
</div>
</div>
</div>
</article>
<article class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-[#979797]">Agenda Terdekat</p>
<h3 class="mt-2 text-xl font-semibold text-[#15171A]">Jadwal Seminar</h3>
</div>
<span class="rounded-full border border-[#E5E7EB] bg-[#F9FAFB] px-3 py-1 text-xs font-semibold text-[#4B5563]">{{ count($dashboard['upcomingSchedules']) }} data</span>
</div>
@if ($dashboard['upcomingSchedules'] === [])
<div class="mt-5 rounded-lg border border-dashed border-[#D1D5DB] bg-[#F9FAFB] p-5 text-sm leading-7 text-[#6B7280]">
Belum ada jadwal seminar terdekat yang terpublikasi untuk program studi ini.
</div>
@else
<div class="mt-5 space-y-3">
@foreach ($dashboard['upcomingSchedules'] as $schedule)
<div class="rounded-lg border border-[#E5E7EB] bg-[#F9FAFB] p-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="flex flex-wrap items-center gap-2">
<span class="rounded-full px-2.5 py-1 text-[11px] font-semibold {{ $schedule['jenisClass'] }}">{{ $schedule['jenis'] }}</span>
<p class="text-sm font-semibold text-[#15171A]">{{ $schedule['nama'] }}</p>
<span class="text-sm text-[#6B7280]">{{ $schedule['nim'] }}</span>
</div>
<p class="mt-2 text-sm leading-6 text-[#4B5563]">{{ $schedule['judul'] }}</p>
</div>
<div class="text-right text-sm text-[#6B7280]">
<p>{{ $schedule['tanggal'] }}</p>
<p class="mt-1">{{ $schedule['ruangan'] }}</p>
</div>
</div>
</div>
@endforeach
</div>
@endif
</article>
</section>
<section>
<article class="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)] lg:p-6 xl:p-7">
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-[#979797]">Kalender</p>
<h3 class="mt-2 text-xl font-semibold text-[#15171A]">{{ $dashboard['calendar']['monthLabel'] }}</h3>
</div>
<span class="rounded-full border border-[#E5E7EB] bg-[#F9FAFB] px-3 py-1 text-xs font-semibold text-[#4B5563]">Publikasi jadwal aktif</span>
</div>
<div class="mt-6 grid grid-cols-7 gap-2.5 text-center text-xs font-semibold uppercase tracking-[0.12em] text-[#9CA3AF]">
@foreach ($dashboard['calendar']['weekdays'] as $weekday)
<div class="rounded-md bg-[#F9FAFB] px-2 py-2.5">{{ $weekday }}</div>
@endforeach
</div>
<div class="mt-3 space-y-2.5">
@foreach ($dashboard['calendar']['weeks'] as $week)
<div class="grid grid-cols-7 gap-2.5">
@foreach ($week as $day)
<div class="aspect-[0.82/1] min-h-[72px] rounded-lg border px-1.5 py-1.5 {{ $day['isCurrentMonth'] ? 'border-[#E5E7EB] bg-white' : 'border-[#EEF2F7] bg-[#F9FAFB]/80' }} {{ $day['isToday'] ? 'ring-2 ring-[#625DF5]/20' : '' }}">
<div class="flex items-center justify-between">
<span class="text-[13px] font-semibold {{ $day['isCurrentMonth'] ? 'text-[#15171A]' : 'text-[#9CA3AF]' }}">{{ $day['day'] }}</span>
@if ($day['isToday'])
<span class="rounded-full bg-[#625DF5] px-1 py-0.5 text-[8px] font-semibold leading-none text-white">Hari ini</span>
@endif
</div>
<div class="mt-1.5 space-y-1">
@foreach (array_slice($day['events'], 0, 3) as $event)
<div class="rounded-md border px-1.5 py-0.5 text-[8px] leading-3 {{ $event['className'] }}">
<p class="font-semibold">{{ $event['jenis'] }}</p>
<p class="truncate">{{ $event['title'] }}</p>
</div>
@endforeach
</div>
</div>
@endforeach
</div>
@endforeach
</div>
</article>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,118 @@
<x-layouts.app :title="$title" :dashboard-layout="true">
<main class="bg-[#F9FAFB] min-h-screen">
<div class="dashboard-shell">
<aside class="dashboard-sidebar">
<div class="dashboard-sidebar-card">
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-[#979797]">Menu Mahasiswa</p>
<div class="mt-4 space-y-1">
@foreach ($dashboard['menus'] as $menu)
@if (isset($menu['children']))
<div class="dashboard-sidebar-group">
<div class="dashboard-sidebar-group-label">
<span class="dashboard-sidebar-icon">@include('dashboard.partials.icon', ['icon' => $menu['icon']])</span>
<span>{{ $menu['title'] }}</span>
</div>
<div class="dashboard-sidebar-submenu">
@foreach ($menu['children'] as $child)
<a href="{{ $child['href'] }}" class="dashboard-sidebar-link">
<span class="dashboard-sidebar-icon">@include('dashboard.partials.icon', ['icon' => $child['icon']])</span>
<span>{{ $child['title'] }}</span>
</a>
@endforeach
</div>
</div>
@else
<a href="{{ $menu['href'] }}" class="dashboard-sidebar-link {{ !empty($menu['active']) ? 'dashboard-sidebar-link-active' : '' }}">
<span class="dashboard-sidebar-icon">@include('dashboard.partials.icon', ['icon' => $menu['icon']])</span>
<span>{{ $menu['title'] }}</span>
</a>
@endif
@endforeach
</div>
</div>
</aside>
<div class="dashboard-content">
<div class="flex flex-col gap-7.5 rounded-xl bg-white p-4 shadow-[0_8px_12px_rgba(13,10,44,0.04)] lg:flex-row lg:items-center lg:justify-between lg:p-6">
<div class="lg:max-w-[700px]">
<span class="mb-4 inline-flex rounded-full bg-[#625DF5]/[0.08] px-3 py-1 text-sm font-medium text-[#625DF5]">{{ $dashboard['eyebrow'] }}</span>
<h1 class="mb-4 font-bold text-[22px] leading-[30px] text-[#15171A] xl:text-[30px] xl:leading-[38px]">{{ $dashboard['title'] }}</h1>
<p>{{ $dashboard['description'] }}</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<div class="rounded-md border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 text-sm text-[#15171A]">{{ $user['nim'] }} · {{ $user['nmprodi'] }}</div>
<form method="POST" action="{{ route('legacy.logout') }}">
@csrf
<button type="submit" class="rounded-md bg-[#15171A] px-5.5 py-2.5 text-sm font-medium text-white hover:opacity-90">Logout</button>
</form>
</div>
</div>
<section id="ringkasan" class="mt-5 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
@foreach ($dashboard['stats'] as $stat)
<article class="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<p class="text-sm font-semibold text-[#5C6A78]">{{ $stat['label'] }}</p>
<div class="mt-3 flex items-start justify-between gap-4">
<p class="text-3xl font-bold text-[#15171A]">{{ $stat['value'] }}</p>
<span class="rounded-md px-3 py-1 text-xs font-semibold {{ $stat['deltaClass'] }}">{{ $stat['delta'] }}</span>
</div>
<p class="mt-3 text-sm leading-7">{{ $stat['note'] }}</p>
</article>
@endforeach
</section>
<div class="mt-6 grid gap-6 lg:grid-cols-[minmax(0,1fr)_380px]">
<section class="space-y-6">
<article id="status-usulan" class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h2 class="text-xl font-semibold text-[#15171A]">Status Usulan</h2>
@if ($dashboard['latestTitle'])
<p class="mt-3 text-sm leading-7 text-[#4B5563]">Judul terakhir: {{ $dashboard['latestTitle'] }}</p>
@endif
<div class="mt-5 rounded-xl border p-5 {{ $dashboard['statusAlert']['class'] }}">
<h3 class="text-lg font-semibold">{{ $dashboard['statusAlert']['title'] }}</h3>
<p class="mt-3 text-sm leading-7">{{ $dashboard['statusAlert']['description'] }}</p>
@if ($dashboard['statusAlert']['button'])
<div class="mt-4">
<a href="{{ $dashboard['statusAlert']['buttonHref'] }}" class="inline-flex rounded-md px-4 py-2.5 text-sm font-medium {{ $dashboard['statusAlert']['buttonClass'] }}">{{ $dashboard['statusAlert']['button'] }}</a>
</div>
@endif
</div>
</article>
<article id="pengumuman" class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h2 class="text-xl font-semibold text-[#15171A]">Pengumuman</h2>
<div class="mt-5 rounded-xl border p-5 {{ $dashboard['announcementAlert']['class'] }}">
<h3 class="text-lg font-semibold">{{ $dashboard['announcementAlert']['title'] }}</h3>
<p class="mt-3 text-sm leading-7">{{ $dashboard['announcementAlert']['description'] }}</p>
<div class="mt-4">
<a href="{{ $dashboard['announcementAlert']['buttonHref'] }}" class="inline-flex rounded-md px-4 py-2.5 text-sm font-medium {{ $dashboard['announcementAlert']['buttonClass'] }}">{{ $dashboard['announcementAlert']['button'] }}</a>
</div>
</div>
</article>
</section>
<section id="jadwal" class="space-y-6">
<article class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-[#979797]">Jadwal</p>
<h2 class="mt-2 text-xl font-semibold text-[#15171A]">Jadwal Terdekat</h2>
</div>
<span class="rounded-full border border-[#E5E7EB] bg-[#F9FAFB] px-3 py-1 text-xs font-semibold text-[#4B5563]">{{ $dashboard['publishedSchedules'] }} publikasi</span>
</div>
@if ($dashboard['nextSchedule'])
<div class="mt-5 rounded-xl border border-sky-200 bg-sky-50 p-5 text-sky-900">
<p class="text-sm font-semibold uppercase tracking-[0.16em]">{{ $dashboard['nextSchedule']['jenis'] }}</p>
<p class="mt-3 text-lg font-semibold">{{ $dashboard['nextSchedule']['tanggal'] }}</p>
<p class="mt-2 text-sm leading-7">Ruangan: {{ $dashboard['nextSchedule']['ruangan'] }}</p>
</div>
@else
<div class="mt-5 rounded-xl border border-dashed border-[#D1D5DB] bg-[#F9FAFB] p-5 text-sm leading-7 text-[#6B7280]">
Belum ada jadwal seminar yang dipublikasikan untuk mahasiswa ini.
</div>
@endif
</article>
</section>
</div>
</div>
</div>
</main>
</x-layouts.app>

View File

@@ -0,0 +1,46 @@
@switch($icon)
@case('home')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/></svg>
@break
@case('folder')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5A2.5 2.5 0 0 1 5.5 5H10l2 2h6.5A2.5 2.5 0 0 1 21 9.5v8A2.5 2.5 0 0 1 18.5 20h-13A2.5 2.5 0 0 1 3 17.5z"/></svg>
@break
@case('briefcase')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6V4.5h6V6"/><path d="M4 8.5h16v9A2.5 2.5 0 0 1 17.5 20h-11A2.5 2.5 0 0 1 4 17.5z"/><path d="M4 11h16"/></svg>
@break
@case('chat')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 6.5A2.5 2.5 0 0 1 7.5 4h9A2.5 2.5 0 0 1 19 6.5v6A2.5 2.5 0 0 1 16.5 15H10l-4 4v-4H7.5A2.5 2.5 0 0 1 5 12.5z"/></svg>
@break
@case('search')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="5.5"/><path d="m16 16 4 4"/></svg>
@break
@case('users')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16.5 19a4.5 4.5 0 0 0-9 0"/><circle cx="12" cy="9" r="3"/><path d="M18.5 18a3.5 3.5 0 0 0-2.5-3.36"/><path d="M8 14.64A3.5 3.5 0 0 0 5.5 18"/></svg>
@break
@case('bell')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4.5a4 4 0 0 0-4 4V11c0 1.4-.5 2.75-1.4 3.82L5.5 16h13l-1.1-1.18A5.74 5.74 0 0 1 16 11V8.5a4 4 0 0 0-4-4Z"/><path d="M10 18a2 2 0 0 0 4 0"/></svg>
@break
@case('megaphone')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11v2a2 2 0 0 0 2 2h2l3 3V6L8 9H6a2 2 0 0 0-2 2Z"/><path d="M14 8.5a6 6 0 0 1 0 7"/><path d="M16.5 6a9 9 0 0 1 0 12"/></svg>
@break
@case('user')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="3.5"/><path d="M5.5 19a6.5 6.5 0 0 1 13 0"/></svg>
@break
@case('document')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3.5h6l4 4V20a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 7 20V5A1.5 1.5 0 0 1 8.5 3.5Z"/><path d="M14 3.5V8h4"/></svg>
@break
@case('warning')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4 21 20H3z"/><path d="M12 9v4.5"/><circle cx="12" cy="16.5" r=".8" fill="currentColor" stroke="none"/></svg>
@break
@case('clipboard')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4.5h6"/><path d="M9.5 3h5A1.5 1.5 0 0 1 16 4.5V6h2A1.5 1.5 0 0 1 19.5 7.5v12A1.5 1.5 0 0 1 18 21h-12a1.5 1.5 0 0 1-1.5-1.5v-12A1.5 1.5 0 0 1 6 6h2V4.5A1.5 1.5 0 0 1 9.5 3Z"/></svg>
@break
@case('clock')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8"/><path d="M12 7.5V12l3 2"/></svg>
@break
@case('chart')
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19.5h14"/><path d="M7.5 17V10"/><path d="M12 17V6.5"/><path d="M16.5 17v-4"/></svg>
@break
@default
<svg viewBox="0 0 24 24" class="h-[18px] w-[18px]" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="7"/></svg>
@endswitch

View File

@@ -0,0 +1,25 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="space-y-4">
@forelse ($bimbingan as $item)
<article class="rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-5">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="min-w-0 flex-1">
<h3 class="break-words text-lg font-semibold leading-7 text-[#15171A]">{{ $item['judul'] }}</h3>
<div class="mt-3 grid gap-2 text-sm text-[#6B7280] sm:grid-cols-2 xl:grid-cols-3">
<span class="break-words">{{ $item['mahasiswa'] }}</span>
<span>{{ $item['periode'] }}</span>
<span>{{ $item['tanggal'] }}</span>
</div>
</div>
<div class="shrink-0">
<a href="{{ $item['reviewHref'] }}" class="inline-flex rounded-md border border-[#D1D5DB] bg-white px-3 py-2 text-xs font-medium text-[#15171A] hover:bg-[#F9FAFB]">Lihat Data</a>
</div>
</div>
</article>
@empty
<div class="rounded-xl border border-dashed border-[#D1D5DB] bg-[#F9FAFB] p-6 text-center text-[#6B7280]">Belum ada data bimbingan.</div>
@endforelse
</div>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,34 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<form method="GET" class="grid gap-4 lg:grid-cols-[220px_minmax(0,1fr)_120px]">
<select name="by" class="template-input">
<option value="nim" @selected($searchBy === 'nim')>NIM</option>
<option value="judul" @selected($searchBy === 'judul')>Judul Praoutline</option>
<option value="dosen" @selected($searchBy === 'dosen')>Pembimbing/Penguji</option>
</select>
<input type="text" name="q" value="{{ $keyword }}" class="template-input" placeholder="Cari...">
<button type="submit" class="template-button-dark">Cari</button>
</form>
<div class="mt-6 space-y-4">
@if ($keyword !== '' && $results === [])
<div class="rounded-xl border border-dashed border-[#D1D5DB] bg-[#F9FAFB] p-6 text-center text-[#6B7280]">Tidak ada hasil untuk `{{ $keyword }}`.</div>
@endif
@foreach ($results as $item)
<article class="rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-5">
<h3 class="text-lg font-semibold text-[#15171A]">{{ $item['judul'] }}</h3>
<p class="mt-2 text-sm text-[#4B5563]">{{ $item['deskripsi'] }}</p>
<div class="mt-3 flex flex-wrap gap-3 text-sm text-[#6B7280]">
<span>{{ $item['mahasiswa'] }}</span>
<span>{{ $item['tanggal'] }}</span>
<span>{{ $item['status'] }}</span>
</div>
<div class="mt-4">
<a href="{{ $item['reviewHref'] }}" class="rounded-md bg-[#15171A] px-3 py-2 text-xs font-medium text-white hover:opacity-90">Buka Review</a>
</div>
</article>
@endforeach
</div>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,26 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="space-y-4">
@forelse ($usulan as $item)
<article class="rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-5">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h3 class="text-lg font-semibold text-[#15171A]">{{ $item['judul'] }}</h3>
<p class="mt-2 text-sm text-[#4B5563]">{{ $item['mahasiswa'] }}</p>
<p class="mt-2 text-sm text-[#6B7280]">{{ $item['periode'] }} · {{ $item['tanggal'] }}</p>
<div class="mt-4 flex flex-wrap gap-2">
<a href="{{ $item['reviewHref'] }}" class="rounded-md bg-[#15171A] px-3 py-2 text-xs font-medium text-white hover:opacity-90">Review</a>
</div>
</div>
<div class="text-right">
<div class="rounded-full bg-white px-3 py-1 text-xs font-semibold text-[#15171A]">{{ $item['status'] }}</div>
<p class="mt-3 text-sm text-[#6B7280]">KK: {{ $item['kk'] }}</p>
</div>
</div>
</article>
@empty
<div class="rounded-xl border border-dashed border-[#D1D5DB] bg-[#F9FAFB] p-6 text-center text-[#6B7280]">Tidak ada usulan aktif.</div>
@endforelse
</div>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,70 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="grid gap-5 lg:grid-cols-6">
<article class="rounded-xl border border-[#3B0A0A] bg-[#FFF1F1] p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-[#7F1D1D]">Ancaman DO</p>
<p class="mt-3 text-3xl font-bold text-[#3B0A0A]">{{ $summary['dropoutCount'] }}</p>
<p class="mt-2 text-sm leading-6 text-[#7F1D1D]">Mahasiswa angkatan lama di ambang akhir masa studi.</p>
</article>
<article class="rounded-xl border border-rose-300 bg-rose-50 p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-rose-700">Kritis</p>
<p class="mt-3 text-3xl font-bold text-rose-800">{{ $summary['criticalCount'] }}</p>
<p class="mt-2 text-sm leading-6 text-rose-700">Tahun akhir studi dan berisiko tinggi.</p>
</article>
<article class="rounded-xl border border-rose-200 bg-rose-50 p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-rose-700">Warning</p>
<p class="mt-3 text-3xl font-bold text-rose-800">{{ $summary['warningCount'] }}</p>
<p class="mt-2 text-sm leading-6 text-rose-700">Sudah masuk fase studi lanjut dan perlu follow-up.</p>
</article>
<article class="rounded-xl border border-amber-200 bg-amber-50 p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-amber-700">Perlu Pantau</p>
<p class="mt-3 text-3xl font-bold text-amber-800">{{ $summary['watchCount'] }}</p>
<p class="mt-2 text-sm leading-6 text-amber-700">Mulai masuk tahun studi yang perlu perhatian.</p>
</article>
<article class="rounded-xl border border-emerald-200 bg-emerald-50 p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-emerald-700">Aman</p>
<p class="mt-3 text-3xl font-bold text-emerald-800">{{ $summary['safeCount'] }}</p>
<p class="mt-2 text-sm leading-6 text-emerald-700">Masih dalam rentang monitoring rutin.</p>
</article>
<article class="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-[#979797]">Total Data</p>
<p class="mt-3 text-3xl font-bold text-[#15171A]">{{ $summary['totalCount'] }}</p>
<p class="mt-2 text-sm leading-6 text-[#4B5563]">Seluruh mahasiswa bimbingan yang termonitor.</p>
</article>
</section>
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="mb-5 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm leading-7 text-rose-800">
Prioritaskan mahasiswa dengan status <strong>Ancaman DO</strong>, <strong>Kritis</strong>, dan <strong>Warning</strong>. Penilaian risiko memakai angkatan mahasiswa dan umur studi, dengan fallback pembacaan angkatan dari pola NIM.
</div>
<div class="space-y-4">
@forelse ($records as $item)
<article class="rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-5">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<h3 class="text-lg font-semibold text-[#15171A]">{{ $item['mahasiswa'] }}</h3>
<span class="rounded-full px-2.5 py-1 text-[11px] font-semibold {{ $item['statusClass'] }}">{{ $item['status'] }}</span>
</div>
<p class="mt-3 break-words text-sm leading-7 text-[#4B5563]">{{ $item['judul'] }}</p>
<div class="mt-3 flex flex-wrap gap-3 text-sm text-[#6B7280]">
<span>Angkatan {{ $item['angkatan'] ?? '-' }}</span>
<span>{{ !is_null($item['tahunStudi']) ? $item['tahunStudi'].' tahun studi' : 'Tahun studi tidak diketahui' }}</span>
<span>{{ $item['tanggal'] }}</span>
@if (!is_null($item['days']))
<span>{{ $item['days'] }} hari sejak keputusan</span>
@endif
</div>
<p class="mt-3 text-sm leading-6 {{ in_array($item['severity'], ['dropout', 'critical', 'warning'], true) ? 'text-rose-700' : ($item['severity'] === 'watch' ? 'text-amber-700' : 'text-emerald-700') }}">{{ $item['warningText'] }}</p>
</div>
<div class="shrink-0">
<a href="{{ $item['detailHref'] }}" target="_blank" rel="noreferrer" class="inline-flex rounded-md border border-[#D1D5DB] bg-white px-3 py-2 text-xs font-medium text-[#15171A] hover:bg-[#F9FAFB]">Buka Monitoring Lengkap</a>
</div>
</div>
</article>
@empty
<div class="rounded-xl border border-dashed border-[#D1D5DB] bg-[#F9FAFB] p-6 text-center text-[#6B7280]">Belum ada data early warning.</div>
@endforelse
</div>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,19 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="space-y-3">
@forelse ($pemberitahuan as $item)
<article class="rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-4">
<p class="text-sm text-[#6B7280]">{{ \Carbon\Carbon::parse($item['tgl'])->locale('id')->translatedFormat('j F Y, H:i') }}</p>
<p class="mt-2 text-[#15171A]">{{ $item['msg'] }}</p>
@if ($item['reviewHref'])
<div class="mt-4">
<a href="{{ $item['reviewHref'] }}" class="rounded-md border border-[#D1D5DB] bg-white px-3 py-2 text-xs font-medium text-[#15171A] hover:bg-[#F9FAFB]">Buka Review</a>
</div>
@endif
</article>
@empty
<div class="rounded-xl border border-dashed border-[#D1D5DB] bg-[#F9FAFB] p-6 text-center text-[#6B7280]">Tidak Ada Pemberitahuan Terbaru</div>
@endforelse
</div>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,31 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<form method="POST" action="{{ $formAction }}" class="space-y-6">
@csrf
@if ($formMethod !== 'POST')
@method($formMethod)
@endif
<div>
<label for="judul_penawaran" class="mb-2 block text-sm font-medium text-[#15171A]">Judul</label>
<input id="judul_penawaran" type="text" name="judul_penawaran" value="{{ $penawaranItem['judul'] }}" class="template-input" required>
@error('judul_penawaran')
<p class="mt-2 text-sm text-rose-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="keterangan_penawaran" class="mb-2 block text-sm font-medium text-[#15171A]">Keterangan</label>
<textarea id="keterangan_penawaran" name="keterangan_penawaran" rows="10" class="template-input">{{ $penawaranItem['deskripsi'] }}</textarea>
@error('keterangan_penawaran')
<p class="mt-2 text-sm text-rose-600">{{ $message }}</p>
@enderror
</div>
<div class="flex flex-wrap gap-3">
<button type="submit" class="template-button-dark">{{ $formMode === 'create' ? 'Simpan Data' : 'Simpan Perubahan' }}</button>
<a href="{{ route('dosen.penawaran.index') }}" class="rounded-md border border-[#D1D5DB] bg-white px-4 py-2.5 text-sm font-medium text-[#15171A] hover:bg-[#F9FAFB]">Kembali</a>
</div>
</form>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,94 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user" :page-actions="$pageActions ?? []">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<form method="GET" class="grid gap-4 border-b border-[#E5E7EB] pb-6 lg:grid-cols-4">
<label class="block text-sm text-[#374151]">
<span class="mb-2 block font-medium text-[#15171A]">Tampilkan :</span>
<select name="sumber" class="template-input">
<option value="0" @selected($source === '0')>Judul Saya</option>
<option value="1" @selected($source === '1')>Semua Dosen</option>
</select>
</label>
<label class="block text-sm text-[#374151]">
<span class="mb-2 block font-medium text-[#15171A]">Status :</span>
<select name="status" class="template-input">
<option value="Semua" @selected($statusFilter === 'Semua')>Semua</option>
<option value="Belum Diambil" @selected($statusFilter === 'Belum Diambil')>Belum Diambil</option>
<option value="Belum Diproses" @selected($statusFilter === 'Belum Diproses')>Belum Diproses</option>
<option value="Diterima" @selected($statusFilter === 'Diterima')>Diterima</option>
</select>
</label>
<label class="block text-sm text-[#374151]">
<span class="mb-2 block font-medium text-[#15171A]">Status :</span>
<select name="kk" class="template-input">
<option value="all" @selected($kkFilter === 'all')>Semua KK</option>
@foreach ($kkOptions as $option)
<option value="{{ $option['value'] }}" @selected($kkFilter === $option['value'])>{{ $option['label'] }}</option>
@endforeach
</select>
</label>
<div class="flex items-end gap-3">
<button type="submit" class="template-button-dark w-full lg:w-auto">Filter</button>
</div>
</form>
<div class="mt-6 overflow-x-auto">
<table class="min-w-full divide-y divide-[#E5E7EB] text-sm">
<thead>
<tr class="text-left text-[#6B7280]">
<th class="px-4 py-3 font-semibold">No.</th>
<th class="px-4 py-3 font-semibold">Judul</th>
<th class="px-4 py-3 font-semibold">Deskripsi</th>
<th class="px-4 py-3 font-semibold">Ditawarkan Oleh</th>
<th class="px-4 py-3 font-semibold">Status</th>
<th class="px-4 py-3 font-semibold">Diambil Oleh</th>
<th class="px-4 py-3 font-semibold">Aksi</th>
</tr>
</thead>
<tbody class="divide-y divide-[#F1F5F9]">
@forelse ($penawaran as $index => $item)
<tr>
<td class="px-4 py-4 text-[#374151]">{{ $index + 1 }}</td>
<td class="px-4 py-4 align-top">
<p class="font-semibold text-[#15171A]">{{ $item['judul'] }}</p>
<p class="mt-1 text-xs text-[#6B7280]">{{ $item['waktu'] }}</p>
</td>
<td class="px-4 py-4 text-[#374151]">{{ $item['deskripsi'] }}</td>
<td class="px-4 py-4 text-[#374151]">{{ $item['ditawarkanOleh'] }}</td>
<td class="px-4 py-4 text-[#374151]">{{ $item['status'] }}</td>
<td class="px-4 py-4 text-[#374151]">{{ $item['mahasiswa'] }}</td>
<td class="px-4 py-4">
<div class="flex flex-wrap gap-2">
@if ($item['canApprove'])
<form method="POST" action="{{ $item['approveHref'] }}">
@csrf
<button type="submit" class="rounded-md border border-emerald-200 bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 hover:bg-emerald-100">Setujui</button>
</form>
<form method="POST" action="{{ $item['rejectHref'] }}">
@csrf
<button type="submit" class="rounded-md border border-amber-200 bg-amber-50 px-3 py-1.5 text-xs font-medium text-amber-700 hover:bg-amber-100">Tolak</button>
</form>
@endif
@if ($item['isMine'])
<a href="{{ $item['editHref'] }}" class="rounded-md bg-[#15171A] px-3 py-1.5 text-xs font-medium text-white hover:opacity-90">Edit</a>
<form method="POST" action="{{ $item['destroyHref'] }}" onsubmit="return confirm('Hapus data penawaran judul ini?');">
@csrf
@method('DELETE')
<button type="submit" class="rounded-md border border-rose-200 bg-rose-50 px-3 py-1.5 text-xs font-medium text-rose-700 hover:bg-rose-100">Hapus</button>
</form>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-4 py-8 text-center text-[#6B7280]">Mengambil Data . . .</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,10 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h2 class="text-2xl font-semibold text-[#15171A]">{{ $pengumumanItem->judul }}</h2>
<p class="mt-2 text-sm text-[#6B7280]">Diposting {{ \Carbon\Carbon::parse($pengumumanItem->tgl)->locale('id')->translatedFormat('j F Y, H:i') }}</p>
<div class="prose mt-6 max-w-none text-[#374151]">{!! $pengumumanItem->isi !!}</div>
<div class="mt-6">
<a href="{{ route('dosen.pengumuman.index') }}" class="inline-flex items-center rounded-md border border-[#D1D5DB] bg-white px-4 py-2.5 text-sm font-medium text-[#15171A] hover:bg-[#F9FAFB]">Kembali</a>
</div>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,17 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="space-y-4">
@forelse ($pengumuman as $item)
<article class="rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-5">
<a href="{{ $item['detailHref'] }}" class="text-lg font-semibold text-[#15171A] hover:text-[#625DF5]">{{ $item['judul'] }}</a>
<p class="mt-2 text-sm text-[#6B7280]">{{ \Carbon\Carbon::parse($item['tgl'])->locale('id')->translatedFormat('j F Y, H:i') }}</p>
<div class="mt-4 flex flex-wrap gap-2">
<a href="{{ $item['detailHref'] }}" class="rounded-md bg-[#15171A] px-3 py-2 text-xs font-medium text-white hover:opacity-90">Lihat Detail</a>
</div>
</article>
@empty
<div class="rounded-xl border border-dashed border-[#D1D5DB] bg-[#F9FAFB] p-6 text-center text-[#6B7280]">Belum ada pengumuman.</div>
@endforelse
</div>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,12 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h3 class="text-xl font-semibold text-[#15171A]">Integrasi Pra LIRS</h3>
<p class="mt-4 max-w-[820px] text-sm leading-7 text-[#4B5563]">Modul lama mengambil data Pra LIRS dari layanan eksternal Informatika. Untuk tahap overhaul ini, jalur modul dikembalikan sebagai halaman rebuild dan sumber integrasinya dipertahankan sama agar perilaku existing tidak berubah mendadak.</p>
<div class="mt-6 rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-5 text-sm text-[#374151]">
Endpoint sumber: <span class="font-mono text-[13px]">{{ $externalUrl }}</span>
</div>
<div class="mt-6">
<a href="https://informatika.untan.ac.id/konsultasi/" target="_blank" rel="noreferrer" class="inline-flex items-center rounded-md bg-[#15171A] px-4 py-2.5 text-sm font-medium text-white hover:opacity-90">Buka Layanan Terkait</a>
</div>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,65 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="grid gap-5 lg:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]">
<article class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h3 class="text-xl font-semibold text-[#15171A]">Edit Profil</h3>
<form method="POST" action="{{ route('dosen.profile.update', [], false) }}" class="mt-5 space-y-5">
@csrf
@method('PUT')
<div class="grid gap-5 md:grid-cols-2">
<label class="block text-sm text-[#374151]">
<span class="mb-2 block font-medium text-[#15171A]">NIP</span>
<input type="text" value="{{ $profile->nip ?? '-' }}" class="template-input bg-[#F9FAFB]" readonly>
</label>
<label class="block text-sm text-[#374151]">
<span class="mb-2 block font-medium text-[#15171A]">Nama Lengkap</span>
<input type="text" name="nmLengkap" value="{{ old('nmLengkap', $profile->nmLengkap ?? '') }}" class="template-input @error('nmLengkap') border-rose-300 ring-rose-100 @enderror" required>
@error('nmLengkap')<p class="mt-2 text-xs text-rose-600">{{ $message }}</p>@enderror
</label>
<label class="block text-sm text-[#374151]">
<span class="mb-2 block font-medium text-[#15171A]">Email</span>
<input type="email" name="email" value="{{ old('email', $profile->email ?? '') }}" class="template-input @error('email') border-rose-300 ring-rose-100 @enderror">
@error('email')<p class="mt-2 text-xs text-rose-600">{{ $message }}</p>@enderror
</label>
<label class="block text-sm text-[#374151]">
<span class="mb-2 block font-medium text-[#15171A]">No Telp/HP</span>
<input type="text" name="nohp" value="{{ old('nohp', $profile->nohp ?? '') }}" class="template-input @error('nohp') border-rose-300 ring-rose-100 @enderror">
@error('nohp')<p class="mt-2 text-xs text-rose-600">{{ $message }}</p>@enderror
</label>
<label class="block text-sm text-[#374151]">
<span class="mb-2 block font-medium text-[#15171A]">Password Baru</span>
<input type="password" name="password" class="template-input @error('password') border-rose-300 ring-rose-100 @enderror" placeholder="Kosongkan jika tidak diubah">
@error('password')<p class="mt-2 text-xs text-rose-600">{{ $message }}</p>@enderror
</label>
<label class="block text-sm text-[#374151]">
<span class="mb-2 block font-medium text-[#15171A]">Konfirmasi Password</span>
<input type="password" name="password_again" class="template-input @error('password_again') border-rose-300 ring-rose-100 @enderror" placeholder="Ulangi password baru">
@error('password_again')<p class="mt-2 text-xs text-rose-600">{{ $message }}</p>@enderror
</label>
</div>
<div class="flex flex-wrap gap-3 border-t border-[#E5E7EB] pt-5">
<button type="submit" class="rounded-md bg-[#15171A] px-4 py-2.5 text-sm font-medium text-white hover:opacity-90">Simpan Perubahan</button>
</div>
</form>
</article>
<article class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h3 class="text-xl font-semibold text-[#15171A]">Ringkasan Akun</h3>
<div class="mt-5 space-y-4 text-sm text-[#374151]">
<div><span class="font-semibold text-[#15171A]">NIP:</span> {{ $profile->nip ?? '-' }}</div>
<div><span class="font-semibold text-[#15171A]">Jenis Dosen:</span> {{ ($profile->jenis ?? 'D') === 'K' ? 'Ketua Program Studi' : 'Dosen' }}</div>
<div><span class="font-semibold text-[#15171A]">Status Akun:</span> {{ ($profile->status ?? 'N') === 'A' ? 'Aktif' : 'Nonaktif' }}</div>
<div><span class="font-semibold text-[#15171A]">Jabatan:</span> {{ $profile->jabatan ?: '-' }}</div>
</div>
<div class="mt-6 rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-4 text-sm leading-7 text-[#4B5563]">
Perubahan foto profil belum dipindahkan ke rebuild. Saat ini yang sudah aktif native adalah pembaruan nama lengkap, email, nomor HP, dan password.
</div>
</article>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,59 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user" :page-actions="$pageActions ?? []">
<section class="space-y-5">
<article class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="min-w-0 flex-1">
<h2 class="break-words text-[22px] font-semibold leading-8 text-[#15171A]">{{ $outline['judul'] }}</h2>
<p class="mt-3 text-sm text-[#4B5563]">{{ $outline['mahasiswa'] }}</p>
<div class="mt-3 flex flex-wrap gap-3 text-sm text-[#6B7280]">
<span>{{ $outline['periode'] }}</span>
<span>{{ $outline['tanggal'] }}</span>
<span>KK: {{ $outline['kk'] }}</span>
</div>
</div>
<div class="shrink-0 rounded-full bg-[#F9FAFB] px-3 py-1 text-xs font-semibold text-[#15171A]">
{{ $outline['status'] }}
</div>
</div>
<div class="mt-5 rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-4 text-sm leading-7 text-[#374151]">
{!! $outline['deskripsi'] ?: '<span class="text-[#6B7280]">Tidak ada deskripsi.</span>' !!}
</div>
</article>
<section id="post_review" class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-[#979797]">Diskusi</p>
<h3 class="mt-2 text-xl font-semibold text-[#15171A]">Riwayat Review</h3>
</div>
<span class="rounded-full border border-[#E5E7EB] bg-[#F9FAFB] px-3 py-1 text-xs font-semibold text-[#4B5563]">{{ count($reviews) }} entri</span>
</div>
<div class="mt-5 space-y-4">
@forelse ($reviews as $review)
<article class="rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-5">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<div class="flex flex-wrap items-center gap-2">
<p class="font-semibold text-[#15171A]">{{ $review['author'] }}</p>
<span class="rounded-full bg-white px-2.5 py-1 text-[11px] font-semibold text-[#4B5563]">{{ $review['role'] }}</span>
<span class="rounded-full px-2.5 py-1 text-[11px] font-semibold {{ $review['type'] === 'Putusan' ? 'bg-amber-100 text-amber-800' : 'bg-slate-100 text-slate-700' }}">{{ $review['type'] }}</span>
@if ($review['decision'])
<span class="rounded-full px-2.5 py-1 text-[11px] font-semibold {{ $review['decision'] === 'Setuju' ? 'bg-emerald-100 text-emerald-700' : 'bg-rose-100 text-rose-700' }}">{{ $review['decision'] }}</span>
@endif
</div>
<p class="mt-2 text-xs text-[#6B7280]">{{ $review['timestamp'] }}</p>
</div>
</div>
<div class="prose prose-sm mt-4 max-w-none text-[#374151] prose-p:leading-7">
{!! $review['body'] !!}
</div>
</article>
@empty
<div class="rounded-xl border border-dashed border-[#D1D5DB] bg-[#F9FAFB] p-6 text-center text-[#6B7280]">Belum ada review untuk usulan ini.</div>
@endforelse
</div>
</section>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,36 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="overflow-x-auto">
<table class="min-w-[1100px] divide-y divide-[#E5E7EB] text-sm">
<thead>
<tr class="text-left text-[#6B7280]">
<th class="w-[220px] px-4 py-3 font-semibold">Mahasiswa</th>
<th class="w-[420px] px-4 py-3 font-semibold">Judul Usulan</th>
<th class="w-[160px] px-4 py-3 font-semibold">Periode</th>
<th class="w-[180px] px-4 py-3 font-semibold">Tanggal</th>
<th class="w-[140px] px-4 py-3 font-semibold">Status</th>
<th class="w-[140px] px-4 py-3 font-semibold">Aksi</th>
</tr>
</thead>
<tbody class="divide-y divide-[#F1F5F9]">
@forelse ($reviews as $item)
<tr class="align-top">
<td class="px-4 py-4 text-[#374151]">{{ $item['mahasiswa'] }}</td>
<td class="px-4 py-4 font-medium leading-6 text-[#15171A]">{{ $item['judul'] }}</td>
<td class="px-4 py-4 text-[#374151]">{{ $item['periode'] }}</td>
<td class="px-4 py-4 text-[#374151]">{{ $item['tanggal'] }}</td>
<td class="px-4 py-4 text-[#374151]">{{ $item['status'] }}</td>
<td class="whitespace-nowrap px-4 py-4">
<a href="{{ $item['reviewHref'] }}" class="inline-flex rounded-md border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-[#15171A] hover:bg-[#F9FAFB]">Lihat Review</a>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-4 py-8 text-center text-[#6B7280]">Belum ada data review.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,43 @@
<x-dosen.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="grid gap-5 xl:grid-cols-2">
<article class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h3 class="text-xl font-semibold text-[#15171A]">Statistik Draft Praoutline</h3>
<div class="mt-5 overflow-x-auto">
<table class="min-w-full divide-y divide-[#E5E7EB] text-sm">
<thead>
<tr class="text-left text-[#6B7280]">
<th class="px-4 py-3 font-semibold">Semester</th>
<th class="px-4 py-3 font-semibold">Proses</th>
<th class="px-4 py-3 font-semibold">Disetujui</th>
<th class="px-4 py-3 font-semibold">Ditolak</th>
<th class="px-4 py-3 font-semibold">Gugur</th>
<th class="px-4 py-3 font-semibold">Total</th>
</tr>
</thead>
<tbody class="divide-y divide-[#F1F5F9]">
@foreach ($draftStats as $item)
<tr>
<td class="px-4 py-4">{{ $item->semester }}</td>
<td class="px-4 py-4">{{ $item->proses }}</td>
<td class="px-4 py-4">{{ $item->terima }}</td>
<td class="px-4 py-4">{{ $item->tolak }}</td>
<td class="px-4 py-4">{{ $item->gugur }}</td>
<td class="px-4 py-4">{{ $item->totaldraft }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</article>
<article class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h3 class="text-xl font-semibold text-[#15171A]">Statistik Dosen</h3>
<div class="mt-5 grid gap-4 sm:grid-cols-2">
<div class="metric-card"><p class="text-sm text-[#6B7280]">Pembimbing 1</p><p class="mt-2 text-3xl font-bold text-[#15171A]">{{ $dosenStats->pemb1 ?? 0 }}</p></div>
<div class="metric-card"><p class="text-sm text-[#6B7280]">Pembimbing 2</p><p class="mt-2 text-3xl font-bold text-[#15171A]">{{ $dosenStats->pemb2 ?? 0 }}</p></div>
<div class="metric-card"><p class="text-sm text-[#6B7280]">Penguji 1</p><p class="mt-2 text-3xl font-bold text-[#15171A]">{{ $dosenStats->peng1 ?? 0 }}</p></div>
<div class="metric-card"><p class="text-sm text-[#6B7280]">Penguji 2</p><p class="mt-2 text-3xl font-bold text-[#15171A]">{{ $dosenStats->peng2 ?? 0 }}</p></div>
</div>
</article>
</section>
</x-dosen.partials.page-shell>

View File

@@ -0,0 +1,63 @@
<x-layouts.app :title="$title">
<main class="overflow-hidden bg-[#F5F7FB] pt-20">
<section class="relative border-b border-slate-200 bg-white">
<div class="absolute inset-x-0 top-0 h-56 bg-gradient-to-br from-[#0F5132] via-[#146C43] to-[#198754]"></div>
<div class="relative mx-auto max-w-[1180px] px-4 pb-12 pt-10 sm:px-6 lg:pb-16 lg:pt-14 xl:px-0">
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_380px] lg:items-stretch">
<div class="rounded-[28px] border border-white/30 bg-white p-6 shadow-[0_24px_70px_rgba(15,23,42,0.14)] sm:p-8 lg:p-10">
<div class="inline-flex items-center gap-2 rounded-full border border-emerald-100 bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-emerald-800">
Teknik Informatika UNTAN
</div>
<h1 class="mt-6 max-w-3xl text-[34px] font-black leading-[1.05] tracking-[-0.04em] text-slate-950 sm:text-[48px] lg:text-[64px]">
SPOTA untuk pengajuan, review, dan monitoring tugas akhir.
</h1>
<p class="mt-6 max-w-2xl text-base leading-8 text-slate-600 sm:text-lg">
SPOTA membantu mahasiswa mengajukan outline tugas akhir, memantau status persetujuan, melihat jadwal seminar atau sidang, serta menerima pengumuman akademik. Dosen dan admin dapat meninjau usulan, mengelola penawaran judul, dan memantau proses tugas akhir dalam satu sistem.
</p>
<div class="mt-8 flex flex-col gap-3 sm:flex-row">
<a href="{{ route('legacy.login', 'mahasiswa') }}" class="inline-flex items-center justify-center rounded-2xl bg-slate-950 px-6 py-3.5 text-sm font-bold text-white shadow-lg shadow-slate-950/15 transition hover:-translate-y-0.5 hover:bg-slate-800">
Masuk Mahasiswa
</a>
<a href="{{ route('legacy.login', 'dosen') }}" class="inline-flex items-center justify-center rounded-2xl border border-slate-200 bg-white px-6 py-3.5 text-sm font-bold text-slate-950 transition hover:-translate-y-0.5 hover:border-emerald-300 hover:text-emerald-800">
Masuk Dosen
</a>
<a href="{{ route('legacy.login', 'admin') }}" class="inline-flex items-center justify-center rounded-2xl border border-slate-200 bg-slate-50 px-6 py-3.5 text-sm font-bold text-slate-700 transition hover:-translate-y-0.5 hover:bg-white hover:text-slate-950">
Admin Prodi
</a>
</div>
</div>
<aside class="rounded-[28px] border border-white/30 bg-slate-950 p-6 text-white shadow-[0_24px_70px_rgba(15,23,42,0.22)] sm:p-8">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-emerald-300">Status Rebuild</p>
<div class="mt-6 space-y-4">
@foreach ($systemStatus as $status)
<div class="rounded-2xl border border-white/10 bg-white/[0.06] p-4">
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-white/45">{{ $status['label'] }}</p>
<p class="mt-2 text-lg font-bold text-white">{{ $status['value'] }}</p>
</div>
@endforeach
</div>
<div class="mt-6 rounded-2xl bg-emerald-400 p-5 text-slate-950">
<p class="text-sm font-black uppercase tracking-[0.16em]">Akses aktif</p>
<p class="mt-3 text-sm leading-7 font-medium">Login mahasiswa, dosen, dan admin sudah memakai akun dari database SPOTA.</p>
</div>
</aside>
</div>
</div>
</section>
<section class="mx-auto max-w-[1180px] px-4 py-10 sm:px-6 lg:py-14 xl:px-0">
<div class="grid gap-4 md:grid-cols-3">
@foreach ($highlights as $item)
<a href="{{ $item['href'] }}" class="group rounded-[24px] border border-slate-200 bg-white p-6 shadow-[0_14px_40px_rgba(15,23,42,0.06)] transition hover:-translate-y-1 hover:border-emerald-200 hover:shadow-[0_20px_50px_rgba(15,23,42,0.1)]">
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-50 text-lg font-black text-emerald-700">{{ $item['number'] }}</div>
<h2 class="mt-5 text-xl font-black tracking-[-0.02em] text-slate-950">{{ $item['label'] }}</h2>
<p class="mt-3 text-sm leading-7 text-slate-600">{{ $item['description'] }}</p>
<p class="mt-5 text-sm font-bold text-emerald-700 group-hover:text-emerald-900">Buka portal</p>
</a>
@endforeach
</div>
</section>
</main>
</x-layouts.app>

View File

@@ -0,0 +1,22 @@
<x-mahasiswa.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user" :page-actions="$pageActions ?? []">
<article class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="min-w-0 flex-1">
<h2 class="break-words text-[24px] font-semibold leading-8 text-[#15171A]">{{ $penawaran->judul }}</h2>
<div class="mt-3 flex flex-wrap gap-3 text-sm text-[#6B7280]">
<span>Ditawarkan oleh: {{ $penawaran->dosen ?: '-' }}</span>
<span>KK: {{ $penawaran->kk ?: '-' }}</span>
<span>{{ $penawaran->waktuInput ? \Carbon\Carbon::parse($penawaran->waktuInput)->locale('id')->translatedFormat('j F Y, H:i') : '-' }}</span>
</div>
</div>
<form method="POST" action="{{ route('mahasiswa.penawaran.book', ['id' => $penawaran->idPenawaran]) }}" class="shrink-0">
@csrf
<button type="submit" class="rounded-md bg-[#15171A] px-5 py-2.5 text-sm font-medium text-white hover:opacity-90">Booking Judul Ini</button>
</form>
</div>
<div class="prose prose-sm mt-6 max-w-none rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-5 text-[#374151] prose-p:leading-7">
{!! $penawaran->deskripsi ?: '<p>Tidak ada deskripsi.</p>' !!}
</div>
</article>
</x-mahasiswa.partials.page-shell>

View File

@@ -0,0 +1,82 @@
<x-mahasiswa.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="space-y-5">
<form method="GET" action="{{ route('mahasiswa.penawaran.index') }}" class="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="grid gap-4 lg:grid-cols-[220px_minmax(0,1fr)_auto] lg:items-end">
<div>
<label class="text-sm font-semibold text-[#15171A]">Status Judul</label>
<select name="status" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm">
<option value="0" @selected($filters['status'] === '0')>Belum Diambil</option>
<option value="1" @selected($filters['status'] === '1')>Sudah Diambil</option>
<option value="Semua" @selected($filters['status'] === 'Semua')>Semua Status</option>
</select>
</div>
<div>
<label class="text-sm font-semibold text-[#15171A]">Tampilkan</label>
<select name="kk" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm">
<option value="Semua" @selected($filters['kk'] === 'Semua')>Semua Kelompok Keahlian</option>
@foreach ($kelompokKeahlian as $kk)
<option value="{{ $kk->idKK }}" @selected((string) $filters['kk'] === (string) $kk->idKK)>{{ $kk->namaKK }}</option>
@endforeach
</select>
</div>
<button type="submit" class="rounded-md bg-[#15171A] px-5 py-3 text-sm font-medium text-white hover:opacity-90">Filter</button>
</div>
</form>
<section class="overflow-hidden rounded-xl border border-[#E5E7EB] bg-white shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="overflow-x-auto">
<table class="min-w-[980px] w-full text-left text-sm">
<thead class="bg-[#F9FAFB] text-xs uppercase tracking-[0.12em] text-[#6B7280]">
<tr>
<th class="px-4 py-3">No.</th>
<th class="px-4 py-3">Judul</th>
<th class="px-4 py-3">Ditawarkan Oleh</th>
<th class="px-4 py-3">KK</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Diambil Oleh</th>
<th class="px-4 py-3 text-right">Aksi</th>
</tr>
</thead>
<tbody class="divide-y divide-[#E5E7EB]">
@forelse ($penawaran as $item)
@php
$available = is_null($item->statusPengambilan) || $item->statusPengambilan === '2';
@endphp
<tr>
<td class="px-4 py-4 text-[#6B7280]">{{ $loop->iteration + ($penawaran->currentPage() - 1) * $penawaran->perPage() }}</td>
<td class="px-4 py-4">
<a href="{{ route('mahasiswa.penawaran.show', ['id' => $item->idPenawaran], false) }}" class="font-semibold text-[#15171A] hover:text-[#625DF5]">{{ $item->judul }}</a>
<p class="mt-1 line-clamp-2 text-xs leading-5 text-[#6B7280]">{{ str($item->deskripsi)->stripTags()->squish()->limit(120) }}</p>
</td>
<td class="px-4 py-4 text-[#374151]">{{ $item->dosen ?: '-' }}</td>
<td class="px-4 py-4 text-[#374151]">{{ $item->kk ?: '-' }}</td>
<td class="px-4 py-4">
<span class="rounded-full px-2.5 py-1 text-xs font-semibold {{ $available ? 'bg-emerald-100 text-emerald-800' : 'bg-amber-100 text-amber-800' }}">{{ $available ? 'Belum Diambil' : 'Sudah Diambil' }}</span>
</td>
<td class="px-4 py-4 text-[#374151]">{{ $item->diambil_oleh ?: '-' }}</td>
<td class="px-4 py-4">
<div class="flex justify-end gap-2 whitespace-nowrap">
<a href="{{ route('mahasiswa.penawaran.show', ['id' => $item->idPenawaran], false) }}" class="rounded-md border border-[#D1D5DB] px-3 py-2 text-xs font-medium text-[#15171A] hover:bg-[#F9FAFB]">Lihat</a>
@if ($available)
<form method="POST" action="{{ route('mahasiswa.penawaran.book', ['id' => $item->idPenawaran]) }}">
@csrf
<button type="submit" class="rounded-md bg-[#15171A] px-3 py-2 text-xs font-medium text-white hover:opacity-90">Booking</button>
</form>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-4 py-8 text-center text-[#6B7280]">Tidak ada penawaran judul sesuai filter.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="border-t border-[#E5E7EB] px-4 py-3">
{{ $penawaran->links() }}
</div>
</section>
</section>
</x-mahasiswa.partials.page-shell>

View File

@@ -0,0 +1,9 @@
<x-mahasiswa.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user" :page-actions="$pageActions ?? []">
<article class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h2 class="text-[24px] font-semibold leading-8 text-[#15171A]">{{ $pengumuman['judul'] }}</h2>
<p class="mt-3 text-sm text-[#6B7280]">{{ $pengumuman['tgl'] }}</p>
<div class="prose prose-sm mt-6 max-w-none text-[#374151] prose-p:leading-7">
{!! $pengumuman['isi'] !!}
</div>
</article>
</x-mahasiswa.partials.page-shell>

View File

@@ -0,0 +1,18 @@
<x-mahasiswa.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="space-y-4">
@forelse ($pengumuman as $item)
<article class="rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-5">
<a href="{{ $item['detailHref'] }}" class="text-lg font-semibold text-[#15171A] hover:text-[#625DF5]">{{ $item['judul'] }}</a>
<p class="mt-2 text-sm text-[#6B7280]">{{ \Carbon\Carbon::parse($item['tgl'])->locale('id')->translatedFormat('j F Y, H:i') }}</p>
<p class="mt-4 text-sm leading-7 text-[#4B5563]">{{ $item['preview'] }}</p>
<div class="mt-4 flex flex-wrap gap-2">
<a href="{{ $item['detailHref'] }}" class="rounded-md bg-[#15171A] px-3 py-2 text-xs font-medium text-white hover:opacity-90">Lihat Detail</a>
</div>
</article>
@empty
<div class="rounded-xl border border-dashed border-[#D1D5DB] bg-[#F9FAFB] p-6 text-center text-[#6B7280]">Belum ada pengumuman untuk mahasiswa.</div>
@endforelse
</div>
</section>
</x-mahasiswa.partials.page-shell>

View File

@@ -0,0 +1,71 @@
<x-mahasiswa.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user" :page-actions="$pageActions ?? []">
<section class="space-y-5">
@if ($outline)
<article class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="min-w-0 flex-1">
<h2 class="break-words text-[22px] font-semibold leading-8 text-[#15171A]">{{ $outline['judul'] }}</h2>
<div class="mt-3 flex flex-wrap gap-3 text-sm text-[#6B7280]">
<span>Draft #{{ $outline['id'] }}</span>
<span>{{ $outline['tanggal'] }}</span>
<span>KK: {{ $outline['kelompokKeahlian'] }}</span>
</div>
</div>
<div class="shrink-0 rounded-full px-3 py-1 text-xs font-semibold {{ $outline['statusClass'] }}">
{{ $outline['status'] }}
</div>
</div>
<div class="mt-5 rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-4 text-sm leading-7 text-[#374151]">
{!! $outline['deskripsi'] ?: '<span class="text-[#6B7280]">Tidak ada deskripsi usulan.</span>' !!}
</div>
@if ($outline['catatan'])
<div class="mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm leading-7 text-amber-900">
<p class="font-semibold">Catatan Putusan</p>
<p class="mt-2">{{ $outline['catatan'] }}</p>
</div>
@endif
</article>
<section class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-sm font-semibold uppercase tracking-[0.16em] text-[#979797]">Diskusi</p>
<h3 class="mt-2 text-xl font-semibold text-[#15171A]">Riwayat Review</h3>
</div>
<span class="rounded-full border border-[#E5E7EB] bg-[#F9FAFB] px-3 py-1 text-xs font-semibold text-[#4B5563]">{{ count($reviews) }} entri</span>
</div>
<div class="mt-5 space-y-4">
@forelse ($reviews as $review)
<article class="rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-5">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<div class="flex flex-wrap items-center gap-2">
<p class="font-semibold text-[#15171A]">{{ $review['author'] }}</p>
<span class="rounded-full bg-white px-2.5 py-1 text-[11px] font-semibold text-[#4B5563]">{{ $review['role'] }}</span>
<span class="rounded-full bg-white px-2.5 py-1 text-[11px] font-semibold text-[#4B5563]">{{ $review['type'] }}</span>
@if ($review['decision'])
<span class="rounded-full px-2.5 py-1 text-[11px] font-semibold {{ $review['decision'] === 'Setuju' ? 'bg-emerald-100 text-emerald-700' : 'bg-rose-100 text-rose-700' }}">{{ $review['decision'] }}</span>
@endif
</div>
<p class="mt-2 text-xs text-[#6B7280]">{{ $review['timestamp'] }}</p>
</div>
</div>
<div class="prose prose-sm mt-4 max-w-none text-[#374151] prose-p:leading-7">
{!! $review['body'] !!}
</div>
</article>
@empty
<div class="rounded-xl border border-dashed border-[#D1D5DB] bg-[#F9FAFB] p-6 text-center text-[#6B7280]">Belum ada review untuk usulan terbaru.</div>
@endforelse
</div>
</section>
@else
<div class="rounded-xl border border-dashed border-[#D1D5DB] bg-white p-8 text-center text-[#6B7280] shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
Belum ada draft praoutline yang tercatat untuk akun mahasiswa ini.
</div>
@endif
</section>
</x-mahasiswa.partials.page-shell>

View File

@@ -0,0 +1,89 @@
<x-mahasiswa.partials.page-shell :title="$title" :sidebar="$sidebar" :page-title="$pageTitle" :page-description="$pageDescription" :page-date="$pageDate" :user="$user">
@if ($hasActiveDraft)
<section class="rounded-xl border border-sky-200 bg-sky-50 p-6 text-sky-900 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
<h2 class="text-xl font-semibold">Draft Praoutline Anda Telah Diupload</h2>
<p class="mt-3 text-sm leading-7">Anda masih memiliki draft aktif. Silakan lihat status usulan dan riwayat review sebelum mengajukan draft baru.</p>
<div class="mt-5">
<a href="{{ route('mahasiswa.status-usulan', [], false) }}" class="inline-flex rounded-md bg-sky-600 px-4 py-2.5 text-sm font-medium text-white hover:opacity-90">Lihat Review</a>
</div>
</section>
@else
<section class="space-y-5">
<div class="rounded-xl border border-amber-200 bg-amber-50 p-5 text-sm leading-7 text-amber-900">
<p class="font-semibold">Perhatian</p>
<p class="mt-2">Pastikan file yang diupload berupa PDF dan draft sudah diperiksa. Jika terdapat kesalahan upload, hubungi administrator prodi.</p>
</div>
<form method="POST" action="{{ route('mahasiswa.praoutline.store') }}" enctype="multipart/form-data" class="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-[0_8px_12px_rgba(13,10,44,0.04)]">
@csrf
<div class="grid gap-5">
<div>
<label class="text-sm font-semibold text-[#15171A]">Judul Skripsi</label>
<input type="text" name="judul" value="{{ old('judul') }}" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm focus:border-[#625DF5] focus:outline-none" required>
</div>
<div>
<label class="text-sm font-semibold text-[#15171A]">Deskripsi</label>
<textarea name="deskripsi" rows="7" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm focus:border-[#625DF5] focus:outline-none">{{ old('deskripsi') }}</textarea>
</div>
<div class="grid gap-5 lg:grid-cols-2">
<div>
<label class="text-sm font-semibold text-[#15171A]">Berkas PDF</label>
<input type="file" name="berkas" accept="application/pdf" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm" required>
</div>
<div>
<label class="text-sm font-semibold text-[#15171A]">Kelompok Keahlian Tujuan</label>
<select name="kelompokKeahlian" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm focus:border-[#625DF5] focus:outline-none" required>
<option value="">Pilih Kelompok Keahlian</option>
@foreach ($kelompokKeahlian as $kk)
<option value="{{ $kk->idKK }}" @selected(old('kelompokKeahlian') == $kk->idKK)>{{ $kk->namaKK }}</option>
@endforeach
</select>
</div>
</div>
<div class="grid gap-5 lg:grid-cols-2">
<div>
<label class="text-sm font-semibold text-[#15171A]">Dosen Pembimbing Akademik (PA)</label>
<select name="dosenpa" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm focus:border-[#625DF5] focus:outline-none" required>
<option value="">Pilih Dosen</option>
@foreach ($dosen as $item)
<option value="{{ $item->nmLengkap }}" @selected(old('dosenpa') === $item->nmLengkap)>{{ $item->nmLengkap }}</option>
@endforeach
</select>
</div>
<div>
<label class="text-sm font-semibold text-[#15171A]">Dosen Yang Merekomendasikan Judul</label>
<select name="drekomjudul" class="mt-2 w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm focus:border-[#625DF5] focus:outline-none">
<option value="">Pilih Dosen</option>
@foreach ($dosen as $item)
<option value="{{ $item->nmLengkap }}" @selected(old('drekomjudul') === $item->nmLengkap)>{{ $item->nmLengkap }}</option>
@endforeach
</select>
</div>
</div>
<div>
<p class="text-sm font-semibold text-[#15171A]">Pilihan Dosen Pembimbing</p>
<div class="mt-2 grid gap-3 lg:grid-cols-2">
@for ($i = 1; $i <= 4; $i++)
<select name="pilpemb{{ $i }}" class="w-full rounded-md border border-[#D1D5DB] px-4 py-3 text-sm focus:border-[#625DF5] focus:outline-none">
<option value="">Pilihan {{ $i }}</option>
@foreach ($dosen as $item)
<option value="{{ $item->nmLengkap }}" @selected(old('pilpemb'.$i) === $item->nmLengkap)>{{ $item->nmLengkap }}</option>
@endforeach
</select>
@endfor
</div>
</div>
<div class="flex flex-wrap gap-3 border-t border-[#E5E7EB] pt-5">
<button type="submit" class="rounded-md bg-[#15171A] px-5 py-2.5 text-sm font-medium text-white hover:opacity-90">Upload Draft</button>
<a href="{{ route('mahasiswa.status-usulan', [], false) }}" class="rounded-md border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#15171A] hover:bg-[#F9FAFB]">Batal</a>
</div>
</div>
</form>
</section>
@endif
</x-mahasiswa.partials.page-shell>

View File

@@ -0,0 +1,8 @@
<?php
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');

119
rebuild/routes/web.php Normal file
View File

@@ -0,0 +1,119 @@
<?php
use App\Http\Controllers\Auth\LegacyAuthController;
use App\Http\Controllers\AdminLegacyController;
use App\Http\Controllers\AdminPageController;
use App\Http\Controllers\Dashboard\AdminDashboardController;
use App\Http\Controllers\Dashboard\DosenDashboardController;
use App\Http\Controllers\Dashboard\MahasiswaDashboardController;
use App\Http\Controllers\DosenPageController;
use App\Http\Controllers\MahasiswaPageController;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('home', [
'title' => 'SPOTA | Sistem Pendukung Outline Tugas Akhir',
'systemStatus' => [
['label' => 'Mahasiswa', 'value' => 'Dashboard, praoutline, pengumuman'],
['label' => 'Dosen', 'value' => 'Review, bimbingan, penawaran judul'],
['label' => 'Admin', 'value' => 'Data master dan pengaturan prodi'],
],
'highlights' => [
['number' => '01', 'label' => 'Mahasiswa', 'href' => route('legacy.login', 'mahasiswa'), 'description' => 'Pantau status usulan, baca pengumuman, upload praoutline, dan booking penawaran judul.'],
['number' => '02', 'label' => 'Dosen', 'href' => route('legacy.login', 'dosen'), 'description' => 'Kelola penawaran judul, review praoutline, lihat bimbingan, dan monitoring mahasiswa.'],
['number' => '03', 'label' => 'Admin', 'href' => route('legacy.login', 'admin'), 'description' => 'Kelola data mahasiswa, dosen, kelompok keahlian, pengumuman, jadwal, dan pengaturan prodi.'],
],
]);
})->name('home');
Route::get('/assets/spota-logo', function () {
$logoPath = base_path('../link6.jpg');
abort_unless(File::exists($logoPath), 404);
return response()->file($logoPath);
})->name('assets.spota-logo');
Route::get('/masuk', [LegacyAuthController::class, 'showRoleLogin'])->name('role-login');
Route::get('/login/{role}', [LegacyAuthController::class, 'showLegacyLogin'])->name('legacy.login');
Route::post('/login/{role}', [LegacyAuthController::class, 'authenticate'])->name('legacy.authenticate');
Route::post('/logout', [LegacyAuthController::class, 'logout'])->name('legacy.logout');
Route::middleware('legacy.role:mahasiswa')->group(function () {
Route::get('/dashboard/mahasiswa', MahasiswaDashboardController::class)->name('dashboard.mahasiswa');
Route::get('/mahasiswa/status-usulan', [MahasiswaPageController::class, 'statusUsulan'])->name('mahasiswa.status-usulan');
Route::get('/mahasiswa/praoutline/upload', [MahasiswaPageController::class, 'uploadPraoutline'])->name('mahasiswa.praoutline.upload');
Route::post('/mahasiswa/praoutline/upload', [MahasiswaPageController::class, 'storePraoutline'])->name('mahasiswa.praoutline.store');
Route::get('/mahasiswa/penawaran', [MahasiswaPageController::class, 'penawaran'])->name('mahasiswa.penawaran.index');
Route::get('/mahasiswa/penawaran/{id}', [MahasiswaPageController::class, 'showPenawaran'])->whereNumber('id')->name('mahasiswa.penawaran.show');
Route::post('/mahasiswa/penawaran/{id}/booking', [MahasiswaPageController::class, 'bookPenawaran'])->whereNumber('id')->name('mahasiswa.penawaran.book');
Route::get('/mahasiswa/pengumuman', [MahasiswaPageController::class, 'pengumuman'])->name('mahasiswa.pengumuman.index');
Route::get('/mahasiswa/pengumuman/{id}', [MahasiswaPageController::class, 'showPengumuman'])->whereNumber('id')->name('mahasiswa.pengumuman.show');
});
Route::middleware('legacy.role:admin')->group(function () {
Route::get('/dashboard/admin', AdminDashboardController::class)->name('dashboard.admin');
Route::get('/admin/legacy', AdminLegacyController::class)->name('admin.legacy');
Route::get('/admin/data/mahasiswa', [AdminPageController::class, 'mahasiswa'])->name('admin.data.mahasiswa');
Route::get('/admin/data/mahasiswa/create', [AdminPageController::class, 'createMahasiswa'])->name('admin.data.mahasiswa.create');
Route::post('/admin/data/mahasiswa', [AdminPageController::class, 'storeMahasiswa'])->name('admin.data.mahasiswa.store');
Route::get('/admin/data/mahasiswa/import', [AdminPageController::class, 'importMahasiswa'])->name('admin.data.mahasiswa.import');
Route::post('/admin/data/mahasiswa/import', [AdminPageController::class, 'storeImportMahasiswa'])->name('admin.data.mahasiswa.import.store');
Route::get('/admin/data/mahasiswa/{id}/edit', [AdminPageController::class, 'editMahasiswa'])->whereNumber('id')->name('admin.data.mahasiswa.edit');
Route::put('/admin/data/mahasiswa/{id}', [AdminPageController::class, 'updateMahasiswa'])->whereNumber('id')->name('admin.data.mahasiswa.update');
Route::delete('/admin/data/mahasiswa/{id}', [AdminPageController::class, 'destroyMahasiswa'])->whereNumber('id')->name('admin.data.mahasiswa.destroy');
Route::get('/admin/data/dosen', [AdminPageController::class, 'dosen'])->name('admin.data.dosen');
Route::get('/admin/data/dosen/create', [AdminPageController::class, 'createDosen'])->name('admin.data.dosen.create');
Route::post('/admin/data/dosen', [AdminPageController::class, 'storeDosen'])->name('admin.data.dosen.store');
Route::get('/admin/data/dosen/{id}/edit', [AdminPageController::class, 'editDosen'])->whereNumber('id')->name('admin.data.dosen.edit');
Route::put('/admin/data/dosen/{id}', [AdminPageController::class, 'updateDosen'])->whereNumber('id')->name('admin.data.dosen.update');
Route::delete('/admin/data/dosen/{id}', [AdminPageController::class, 'destroyDosen'])->whereNumber('id')->name('admin.data.dosen.destroy');
Route::get('/admin/data/kelompok-keahlian', [AdminPageController::class, 'kk'])->name('admin.data.kk');
Route::get('/admin/data/kelompok-keahlian/create', [AdminPageController::class, 'createKk'])->name('admin.data.kk.create');
Route::post('/admin/data/kelompok-keahlian', [AdminPageController::class, 'storeKk'])->name('admin.data.kk.store');
Route::get('/admin/data/kelompok-keahlian/{id}/edit', [AdminPageController::class, 'editKk'])->whereNumber('id')->name('admin.data.kk.edit');
Route::put('/admin/data/kelompok-keahlian/{id}', [AdminPageController::class, 'updateKk'])->whereNumber('id')->name('admin.data.kk.update');
Route::delete('/admin/data/kelompok-keahlian/{id}', [AdminPageController::class, 'destroyKk'])->whereNumber('id')->name('admin.data.kk.destroy');
Route::get('/admin/data/fakultas', [AdminPageController::class, 'fakultas'])->name('admin.data.fakultas');
Route::get('/admin/data/jurusan', [AdminPageController::class, 'jurusan'])->name('admin.data.jurusan');
Route::get('/admin/data/prodi', [AdminPageController::class, 'prodi'])->name('admin.data.prodi');
Route::get('/admin/praoutline', [AdminPageController::class, 'praoutline'])->name('admin.praoutline.index');
Route::get('/admin/praoutline/cari', [AdminPageController::class, 'praoutlineSearch'])->name('admin.praoutline.search');
Route::get('/admin/praoutline/keputusan', [AdminPageController::class, 'keputusan'])->name('admin.praoutline.keputusan');
Route::get('/admin/praoutline/kep-draft', [AdminPageController::class, 'kepDraft'])->name('admin.praoutline.kep-draft');
Route::get('/admin/praoutline/pemberitahuan', [AdminPageController::class, 'pemberitahuan'])->name('admin.praoutline.pemberitahuan');
Route::get('/admin/pengumuman', [AdminPageController::class, 'pengumuman'])->name('admin.pengumuman.index');
Route::get('/admin/pengumuman/create', [AdminPageController::class, 'createPengumuman'])->name('admin.pengumuman.create');
Route::post('/admin/pengumuman', [AdminPageController::class, 'storePengumuman'])->name('admin.pengumuman.store');
Route::get('/admin/jadwal', [AdminPageController::class, 'jadwal'])->name('admin.jadwal.index');
Route::get('/admin/jadwal/kalender', [AdminPageController::class, 'kalender'])->name('admin.jadwal.kalender');
Route::get('/admin/profile', [AdminPageController::class, 'profile'])->name('admin.profile');
Route::get('/admin/users', [AdminPageController::class, 'users'])->name('admin.users');
Route::get('/admin/pengaturan', [AdminPageController::class, 'pengaturan'])->name('admin.pengaturan');
});
Route::middleware('legacy.role:dosen')->group(function () {
Route::get('/dashboard/dosen', DosenDashboardController::class)->name('dashboard.dosen');
Route::get('/dosen/penawaran', [DosenPageController::class, 'penawaran'])->name('dosen.penawaran.index');
Route::get('/dosen/penawaran/create', [DosenPageController::class, 'createPenawaran'])->name('dosen.penawaran.create');
Route::post('/dosen/penawaran', [DosenPageController::class, 'storePenawaran'])->name('dosen.penawaran.store');
Route::get('/dosen/penawaran/{id}/edit', [DosenPageController::class, 'editPenawaran'])->whereNumber('id')->name('dosen.penawaran.edit');
Route::put('/dosen/penawaran/{id}', [DosenPageController::class, 'updatePenawaran'])->whereNumber('id')->name('dosen.penawaran.update');
Route::delete('/dosen/penawaran/{id}', [DosenPageController::class, 'destroyPenawaran'])->whereNumber('id')->name('dosen.penawaran.destroy');
Route::post('/dosen/penawaran/{id}/setuju', [DosenPageController::class, 'approvePenawaran'])->whereNumber('id')->name('dosen.penawaran.approve');
Route::post('/dosen/penawaran/{id}/tolak', [DosenPageController::class, 'rejectPenawaran'])->whereNumber('id')->name('dosen.penawaran.reject');
Route::get('/dosen/praoutline', [DosenPageController::class, 'daftarUsulan'])->name('dosen.praoutline.index');
Route::get('/dosen/praoutline/review/{id}', [DosenPageController::class, 'reviewDetail'])->whereNumber('id')->name('dosen.praoutline.review');
Route::get('/dosen/praoutline/review-saya', [DosenPageController::class, 'reviewSaya'])->name('dosen.praoutline.review-saya');
Route::get('/dosen/praoutline/cari', [DosenPageController::class, 'cari'])->name('dosen.praoutline.cari');
Route::get('/dosen/praoutline/bimbingan', [DosenPageController::class, 'bimbingan'])->name('dosen.praoutline.bimbingan');
Route::get('/dosen/praoutline/statistik', [DosenPageController::class, 'statistik'])->name('dosen.praoutline.statistik');
Route::get('/dosen/praoutline/pemberitahuan', [DosenPageController::class, 'pemberitahuan'])->name('dosen.praoutline.pemberitahuan');
Route::get('/dosen/pengumuman', [DosenPageController::class, 'pengumuman'])->name('dosen.pengumuman.index');
Route::get('/dosen/pengumuman/{id}', [DosenPageController::class, 'showPengumuman'])->whereNumber('id')->name('dosen.pengumuman.show');
Route::get('/dosen/profile', [DosenPageController::class, 'profile'])->name('dosen.profile');
Route::put('/dosen/profile', [DosenPageController::class, 'updateProfile'])->name('dosen.profile.update');
Route::get('/dosen/early-warning', [DosenPageController::class, 'earlyWarning'])->name('dosen.early-warning');
Route::get('/dosen/pra-lirs', [DosenPageController::class, 'praLirs'])->name('dosen.pra-lirs');
});

4
rebuild/storage/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*
!private/
!public/
!.gitignore

View File

@@ -0,0 +1,2 @@
*
!.gitignore

2
rebuild/storage/app/public/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

9
rebuild/storage/framework/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
compiled.php
config.php
down
events.scanned.php
maintenance.php
routes.php
routes.scanned.php
schedule-*
services.json

View File

@@ -0,0 +1,2 @@
*
!.gitignore

Some files were not shown because too many files have changed in this diff Show More