First commit - Projek Sistem Rekomendasi Jurusan C4.5
This commit is contained in:
commit
500277c48d
|
|
@ -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.yaml]
|
||||||
|
indent_size = 4
|
||||||
|
|
@ -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}"
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.phpunit.cache
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
|
/auth.json
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/vendor
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
Thumbs.db
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## About Laravel
|
||||||
|
|
||||||
|
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||||
|
|
||||||
|
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||||
|
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||||
|
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||||
|
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||||
|
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||||
|
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||||
|
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||||
|
|
||||||
|
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||||
|
|
||||||
|
## Learning Laravel
|
||||||
|
|
||||||
|
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
|
||||||
|
|
||||||
|
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||||
|
|
||||||
|
## Laravel Sponsors
|
||||||
|
|
||||||
|
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||||
|
|
||||||
|
### Premium Partners
|
||||||
|
|
||||||
|
- **[Vehikl](https://vehikl.com)**
|
||||||
|
- **[Tighten Co.](https://tighten.co)**
|
||||||
|
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||||
|
- **[64 Robots](https://64robots.com)**
|
||||||
|
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
||||||
|
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||||
|
- **[Redberry](https://redberry.international/laravel-development)**
|
||||||
|
- **[Active Logic](https://activelogic.com)**
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||||
|
|
||||||
|
## Security Vulnerabilities
|
||||||
|
|
||||||
|
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Auth\LoginRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AuthenticatedSessionController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the login view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.login');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming authentication request.
|
||||||
|
*/
|
||||||
|
public function store(LoginRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->authenticate();
|
||||||
|
|
||||||
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy an authenticated session.
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
Auth::guard('web')->logout();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ConfirmablePasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show the confirm password view.
|
||||||
|
*/
|
||||||
|
public function show(): View
|
||||||
|
{
|
||||||
|
return view('auth.confirm-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm the user's password.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if (! Auth::guard('web')->validate([
|
||||||
|
'email' => $request->user()->email,
|
||||||
|
'password' => $request->password,
|
||||||
|
])) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'password' => __('auth.password'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->session()->put('auth.password_confirmed_at', time());
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class EmailVerificationNotificationController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Send a new email verification notification.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($request->user()->hasVerifiedEmail()) {
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->user()->sendEmailVerificationNotification();
|
||||||
|
|
||||||
|
return back()->with('status', 'verification-link-sent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class EmailVerificationPromptController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the email verification prompt.
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request): RedirectResponse|View
|
||||||
|
{
|
||||||
|
return $request->user()->hasVerifiedEmail()
|
||||||
|
? redirect()->intended(route('dashboard', absolute: false))
|
||||||
|
: view('auth.verify-email');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Events\PasswordReset;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class NewPasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the password reset view.
|
||||||
|
*/
|
||||||
|
public function create(Request $request): View
|
||||||
|
{
|
||||||
|
return view('auth.reset-password', ['request' => $request]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming new password request.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'token' => ['required'],
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Here we will attempt to reset the user's password. If it is successful we
|
||||||
|
// will update the password on an actual user model and persist it to the
|
||||||
|
// database. Otherwise we will parse the error and return the response.
|
||||||
|
$status = Password::reset(
|
||||||
|
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||||
|
function (User $user) use ($request) {
|
||||||
|
$user->forceFill([
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
'remember_token' => Str::random(60),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
event(new PasswordReset($user));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the password was successfully reset, we will redirect the user back to
|
||||||
|
// the application's home authenticated view. If there is an error we can
|
||||||
|
// redirect them back to where they came from with their error message.
|
||||||
|
return $status == Password::PASSWORD_RESET
|
||||||
|
? redirect()->route('login')->with('status', __($status))
|
||||||
|
: back()->withInput($request->only('email'))
|
||||||
|
->withErrors(['email' => __($status)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class PasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Update the user's password.
|
||||||
|
*/
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validateWithBag('updatePassword', [
|
||||||
|
'current_password' => ['required', 'current_password'],
|
||||||
|
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request->user()->update([
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('status', 'password-updated');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class PasswordResetLinkController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the password reset link request view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.forgot-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming password reset link request.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// We will send the password reset link to this user. Once we have attempted
|
||||||
|
// to send the link, we will examine the response then see the message we
|
||||||
|
// need to show to the user. Finally, we'll send out a proper response.
|
||||||
|
$status = Password::sendResetLink(
|
||||||
|
$request->only('email')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $status == Password::RESET_LINK_SENT
|
||||||
|
? back()->with('status', __($status))
|
||||||
|
: back()->withInput($request->only('email'))
|
||||||
|
->withErrors(['email' => __($status)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Events\Registered;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class RegisteredUserController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the registration view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.register');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming registration request.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||||
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $request->name,
|
||||||
|
'email' => $request->email,
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
]);
|
||||||
|
|
||||||
|
event(new Registered($user));
|
||||||
|
|
||||||
|
Auth::login($user);
|
||||||
|
|
||||||
|
return redirect(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Auth\Events\Verified;
|
||||||
|
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
class VerifyEmailController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Mark the authenticated user's email address as verified.
|
||||||
|
*/
|
||||||
|
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($request->user()->hasVerifiedEmail()) {
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->user()->markEmailAsVerified()) {
|
||||||
|
event(new Verified($request->user()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class AuthController extends Controller
|
||||||
|
{
|
||||||
|
public function showLoginForm()
|
||||||
|
{
|
||||||
|
if (Auth::check()) {
|
||||||
|
$user = Auth::user();
|
||||||
|
if ($user->role === 'admin' || $user->role === 'guru') {
|
||||||
|
return redirect()->route('admin.dashboard');
|
||||||
|
}
|
||||||
|
return redirect()->route('siswa.dashboard');
|
||||||
|
}
|
||||||
|
return view('auth.login');
|
||||||
|
}
|
||||||
|
|
||||||
|
// PROSES REGISTER PUBLIK (DIKUNCI: HANYA DIBUAT DENGAN ROLE SISWA)
|
||||||
|
public function register(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'email' => 'required|string|email|max:255|unique:users',
|
||||||
|
'password' => 'required|string|min:8',
|
||||||
|
], [
|
||||||
|
'email.unique' => 'Email ini sudah terdaftar. Silakan login.',
|
||||||
|
'password.min' => 'Password minimal 8 karakter.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Role dipaksa 'siswa' secara permanen dari server side
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $request->name,
|
||||||
|
'email' => $request->email,
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
'role' => 'siswa',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Auth::login($user);
|
||||||
|
|
||||||
|
return redirect()->route('siswa.dashboard')->with('success', 'Akun berhasil dibuat!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login(Request $request)
|
||||||
|
{
|
||||||
|
$credentials = $request->validate([
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
'password' => ['required'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (Auth::attempt($credentials)) {
|
||||||
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if ($user->role === 'admin' || $user->role === 'guru') {
|
||||||
|
return redirect()->route('admin.dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('siswa.dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->withErrors([
|
||||||
|
'email' => 'Email atau password yang Anda masukkan salah.',
|
||||||
|
])->onlyInput('email');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(Request $request)
|
||||||
|
{
|
||||||
|
Auth::logout();
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Services\C45Service;
|
||||||
|
use App\Services\PreprocessingService;
|
||||||
|
use App\Models\DataTesting;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class C45Controller extends Controller
|
||||||
|
{
|
||||||
|
protected $c45Service;
|
||||||
|
protected $preprocessingService;
|
||||||
|
|
||||||
|
public function __construct(C45Service $c45Service, PreprocessingService $preprocessingService)
|
||||||
|
{
|
||||||
|
$this->c45Service = $c45Service;
|
||||||
|
$this->preprocessingService = $preprocessingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Menampilkan Halaman Dashboard & Perhitungan Algoritma C4.5
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$rootNode = $this->c45Service->getRootNode();
|
||||||
|
$totalData = $this->c45Service->getTotalData();
|
||||||
|
$entropyTotal = $this->c45Service->entropyTotal();
|
||||||
|
$jumlahJurusan = $this->c45Service->getJumlahJurusan();
|
||||||
|
|
||||||
|
$gainNilai = $this->c45Service->hitungGain('nilai');
|
||||||
|
$gainMapelFavorit = $this->c45Service->hitungGain('mapel_favorit');
|
||||||
|
$gainMinat = $this->c45Service->hitungGain('minat');
|
||||||
|
$gainCita = $this->c45Service->hitungGain('cita_cita');
|
||||||
|
$gainPreferensi = $this->c45Service->hitungGain('preferensi_studi');
|
||||||
|
|
||||||
|
$splitNilai = $this->c45Service->hitungSplitInfo('nilai');
|
||||||
|
$splitMapelFavorit = $this->c45Service->hitungSplitInfo('mapel_favorit');
|
||||||
|
$splitMinat = $this->c45Service->hitungSplitInfo('minat');
|
||||||
|
$splitCita = $this->c45Service->hitungSplitInfo('cita_cita');
|
||||||
|
$splitPreferensi = $this->c45Service->hitungSplitInfo('preferensi_studi');
|
||||||
|
|
||||||
|
$entropyRootAttribute = $this->c45Service->hitungEntropyAtribut($rootNode['root'] ?? 'nilai');
|
||||||
|
|
||||||
|
$tree = $this->c45Service->buildTree();
|
||||||
|
$treeText = $this->c45Service->tampilTree($tree);
|
||||||
|
$detailSubsets = $this->c45Service->getDetailSubsets();
|
||||||
|
$rules = $this->c45Service->generateRules($tree);
|
||||||
|
|
||||||
|
return view('c45.index', compact(
|
||||||
|
'totalData',
|
||||||
|
'entropyTotal',
|
||||||
|
'rootNode',
|
||||||
|
'jumlahJurusan',
|
||||||
|
'gainNilai',
|
||||||
|
'gainMapelFavorit',
|
||||||
|
'gainMinat',
|
||||||
|
'gainCita',
|
||||||
|
'gainPreferensi',
|
||||||
|
'splitNilai',
|
||||||
|
'splitMapelFavorit',
|
||||||
|
'splitMinat',
|
||||||
|
'splitCita',
|
||||||
|
'splitPreferensi',
|
||||||
|
'entropyRootAttribute',
|
||||||
|
'tree',
|
||||||
|
'treeText',
|
||||||
|
'detailSubsets',
|
||||||
|
'rules'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2. Menampilkan Form Input Cek Rekomendasi Admin
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
// PERBAIKAN: Ambil daftar siswa dari database yang memiliki role 'siswa'
|
||||||
|
$daftar_siswa = User::where('role', 'siswa')->orderBy('name', 'asc')->get();
|
||||||
|
|
||||||
|
return view('c45.create', compact('daftar_siswa'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3. Menampilkan Halaman Hasil Rekomendasi
|
||||||
|
*/
|
||||||
|
public function hasil()
|
||||||
|
{
|
||||||
|
return view('c45.hasil');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4. Memproses Data Input Kriteria Siswa
|
||||||
|
*/
|
||||||
|
public function hitungRekomendasi(Request $request)
|
||||||
|
{
|
||||||
|
$validatedData = $request->validate([
|
||||||
|
'nama' => 'nullable|string|max:255',
|
||||||
|
'nilai' => 'required|numeric|min:0|max:100',
|
||||||
|
'mapel_favorit' => 'required|string',
|
||||||
|
'preferensi_studi' => 'required|string',
|
||||||
|
'cita_cita' => 'required|string',
|
||||||
|
'minat' => 'required|string',
|
||||||
|
'siswa_id' => 'nullable|exists:users,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$inputRaw = [
|
||||||
|
'nilai' => $validatedData['nilai'],
|
||||||
|
'mapel_favorit' => $validatedData['mapel_favorit'],
|
||||||
|
'minat' => $validatedData['minat'],
|
||||||
|
'cita_cita' => $validatedData['cita_cita'],
|
||||||
|
'preferensi_studi' => $validatedData['preferensi_studi']
|
||||||
|
];
|
||||||
|
|
||||||
|
// 1. Preprocessing Input
|
||||||
|
$dataBersih = $this->preprocessingService->preprocess((object) $inputRaw);
|
||||||
|
|
||||||
|
// 2. Prediksi Algoritma C4.5
|
||||||
|
if (method_exists($this->c45Service, 'predictDetail')) {
|
||||||
|
$hasilPrediksi = $this->c45Service->predictDetail($inputRaw);
|
||||||
|
$hasilJurusan = $hasilPrediksi['rekomendasi_utama'] ?? 'Teknologi Informasi';
|
||||||
|
$rulePath = $hasilPrediksi['rule_path'] ?? [];
|
||||||
|
} else {
|
||||||
|
$hasilJurusan = 'Teknologi Informasi';
|
||||||
|
$rulePath = [
|
||||||
|
'Nilai = ' . ($dataBersih['nilai'] ?? $inputRaw['nilai']),
|
||||||
|
'Mapel Favorit = ' . $inputRaw['mapel_favorit'],
|
||||||
|
'Preferensi = ' . $inputRaw['preferensi_studi'],
|
||||||
|
'Minat = ' . $inputRaw['minat']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$getVal = fn($key) => is_array($dataBersih) ? ($dataBersih[$key] ?? '') : ($dataBersih->{$key} ?? '');
|
||||||
|
|
||||||
|
// 3. Penentuan user_id dan created_by
|
||||||
|
$currentUser = Auth::user();
|
||||||
|
$createdBy = $currentUser ? $currentUser->id : null;
|
||||||
|
$userId = ($currentUser && $currentUser->role === 'admin')
|
||||||
|
? $request->input('siswa_id', null)
|
||||||
|
: ($currentUser->id ?? null);
|
||||||
|
|
||||||
|
// 4. Simpan ke Database
|
||||||
|
$dataTesting = DataTesting::create([
|
||||||
|
'user_id' => $userId,
|
||||||
|
'created_by' => $createdBy,
|
||||||
|
'nama' => $request->input('nama', $currentUser->name ?? 'Calon Mahasiswa'),
|
||||||
|
'nilai_raw' => $inputRaw['nilai'],
|
||||||
|
'mapel_favorit_raw' => $inputRaw['mapel_favorit'],
|
||||||
|
'minat_raw' => $inputRaw['minat'],
|
||||||
|
'cita_cita_raw' => $inputRaw['cita_cita'],
|
||||||
|
'preferensi_studi_raw' => $inputRaw['preferensi_studi'],
|
||||||
|
'nilai' => $getVal('nilai'),
|
||||||
|
'mapel_favorit' => $getVal('mapel_favorit'),
|
||||||
|
'minat' => $getVal('minat'),
|
||||||
|
'cita_cita' => $getVal('cita_cita'),
|
||||||
|
'preferensi_studi' => $getVal('preferensi_studi'),
|
||||||
|
'hasil_rekomendasi' => $hasilJurusan,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 5. Redirect ke Halaman Hasil Rekomendasi
|
||||||
|
return redirect()->route('c45.hasil')->with([
|
||||||
|
'hasil_prediksi' => $dataTesting,
|
||||||
|
'hasilJurusan' => $hasilJurusan,
|
||||||
|
'rulePath' => $rulePath,
|
||||||
|
'dataBersih' => $dataBersih,
|
||||||
|
'success' => 'Analisis rekomendasi C4.5 berhasil diproses!'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\DataTraining;
|
||||||
|
use App\Models\DataTesting;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function index() {
|
||||||
|
return view('admin.dashboard', [
|
||||||
|
'totalTraining' => \App\Models\DataTraining::count(),
|
||||||
|
'totalSiswa' => User::where('role', 'siswa')->count(),
|
||||||
|
'totalHasil' => \App\Models\DataTesting::whereNotNull('hasil_rekomendasi')->count()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\DataTraining;
|
||||||
|
use App\Services\PreprocessingService;
|
||||||
|
|
||||||
|
class DataTrainingController extends Controller
|
||||||
|
{
|
||||||
|
protected $preprocessing;
|
||||||
|
|
||||||
|
// Inject PreprocessingService ke controller
|
||||||
|
public function __construct(PreprocessingService $preprocessing)
|
||||||
|
{
|
||||||
|
$this->preprocessing = $preprocessing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$data = DataTraining::all();
|
||||||
|
return view('training.index', compact('data'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('training.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
// 1. Tambahkan validasi untuk mapel_favorit
|
||||||
|
$request->validate([
|
||||||
|
'nilai' => 'required|numeric',
|
||||||
|
'mapel_favorit' => 'required|string', // [DITAMBAHKAN]
|
||||||
|
'minat' => 'required|string',
|
||||||
|
'cita_cita' => 'required|string',
|
||||||
|
'preferensi_studi' => 'required|string',
|
||||||
|
'jurusan' => 'required|string'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. Ambil nilai input asli dari form
|
||||||
|
$nilaiRaw = $request->input('nilai');
|
||||||
|
$mapelRaw = $request->input('mapel_favorit'); // [DITAMBAHKAN]
|
||||||
|
$minatRaw = $request->input('minat');
|
||||||
|
$citaRaw = $request->input('cita_cita');
|
||||||
|
$studiRaw = $request->input('preferensi_studi');
|
||||||
|
$jurusan = $request->input('jurusan');
|
||||||
|
|
||||||
|
// 3. Konversi menggunakan PreprocessingService yang sudah dipetakan
|
||||||
|
$nilaiKategori = $this->preprocessing->kategoriNilai($nilaiRaw);
|
||||||
|
$minatKategori = $this->preprocessing->kategoriMinat($minatRaw);
|
||||||
|
$citaKategori = $this->preprocessing->kategoriCitaCita($citaRaw);
|
||||||
|
$studiKategori = $this->preprocessing->kategoriPreferensi($studiRaw);
|
||||||
|
|
||||||
|
// 4. Simpan data teks asli (_raw) dan data hasil konversi ke database
|
||||||
|
DataTraining::create([
|
||||||
|
'nilai_raw' => $nilaiRaw,
|
||||||
|
'mapel_favorit_raw' => $mapelRaw, // [DITAMBAHKAN]
|
||||||
|
'minat_raw' => $minatRaw,
|
||||||
|
'cita_cita_raw' => $citaRaw,
|
||||||
|
'preferensi_studi_raw' => $studiRaw,
|
||||||
|
|
||||||
|
'nilai' => $nilaiKategori,
|
||||||
|
'mapel_favorit' => $mapelRaw, // [DITAMBAHKAN] jika berupa pilihan dropdown langsung
|
||||||
|
'minat' => $minatKategori,
|
||||||
|
'cita_cita' => $citaKategori,
|
||||||
|
'preferensi_studi' => $studiKategori,
|
||||||
|
'jurusan' => $jurusan
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('training.index')->with('success', 'Data Latih Berhasil Dikonversi & Disimpan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(string $id)
|
||||||
|
{
|
||||||
|
$data = DataTraining::findOrFail($id);
|
||||||
|
return view('training.edit', compact('data'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, string $id)
|
||||||
|
{
|
||||||
|
// 1. Tambahkan validasi untuk mapel_favorit
|
||||||
|
$request->validate([
|
||||||
|
'nilai' => 'required|numeric',
|
||||||
|
'mapel_favorit' => 'required|string', // [DITAMBAHKAN]
|
||||||
|
'minat' => 'required|string',
|
||||||
|
'cita_cita' => 'required|string',
|
||||||
|
'preferensi_studi' => 'required|string',
|
||||||
|
'jurusan' => 'required|string'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = DataTraining::findOrFail($id);
|
||||||
|
|
||||||
|
// 2. Update baik data mentah (_raw) maupun data hasil konversinya
|
||||||
|
$data->update([
|
||||||
|
'nilai_raw' => $request->nilai,
|
||||||
|
'mapel_favorit_raw' => $request->mapel_favorit, // [DITAMBAHKAN]
|
||||||
|
'minat_raw' => $request->minat,
|
||||||
|
'cita_cita_raw' => $request->cita_cita,
|
||||||
|
'preferensi_studi_raw' => $request->preferensi_studi,
|
||||||
|
|
||||||
|
'nilai' => $this->preprocessing->kategoriNilai($request->nilai),
|
||||||
|
'mapel_favorit' => $request->mapel_favorit, // [DITAMBAHKAN]
|
||||||
|
'minat' => $this->preprocessing->kategoriMinat($request->minat),
|
||||||
|
'cita_cita' => $this->preprocessing->kategoriCitaCita($request->cita_cita),
|
||||||
|
'preferensi_studi' => $this->preprocessing->kategoriPreferensi($request->preferensi_studi),
|
||||||
|
'jurusan' => $request->jurusan
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect('/training')->with('success', 'Data Latih Berhasil Diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(string $id)
|
||||||
|
{
|
||||||
|
DataTraining::destroy($id);
|
||||||
|
return redirect('/training')->with('success', 'Data Latih Berhasil Dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Services\C45Service;
|
||||||
|
use App\Services\PreprocessingService;
|
||||||
|
use App\Models\DataTesting;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class PrediksiController extends Controller
|
||||||
|
{
|
||||||
|
protected $c45Service;
|
||||||
|
protected $preprocessingService;
|
||||||
|
|
||||||
|
public function __construct(C45Service $c45Service, PreprocessingService $preprocessingService)
|
||||||
|
{
|
||||||
|
$this->c45Service = $c45Service;
|
||||||
|
$this->preprocessingService = $preprocessingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$previousInput = [
|
||||||
|
'nama' => $request->query('nama'),
|
||||||
|
'nilai' => $request->query('nilai'),
|
||||||
|
'mapel_favorit' => $request->query('mapel_favorit'),
|
||||||
|
'minat' => $request->query('minat'),
|
||||||
|
'cita_cita' => $request->query('cita_cita'),
|
||||||
|
'preferensi_studi' => $request->query('preferensi_studi'),
|
||||||
|
'siswa_id' => $request->query('siswa_id'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('prediksi.index', compact('previousInput'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hitungRekomendasi(Request $request)
|
||||||
|
{
|
||||||
|
// 1. VALIDASI INPUT FORM
|
||||||
|
$validatedData = $request->validate([
|
||||||
|
'nama' => 'nullable|string|max:255',
|
||||||
|
'nilai' => 'required|numeric|min:0|max:100',
|
||||||
|
'mapel_favorit' => 'required|string',
|
||||||
|
'minat' => 'required|string',
|
||||||
|
'cita_cita' => 'required|string',
|
||||||
|
'preferensi_studi' => 'required|string',
|
||||||
|
'siswa_id' => 'nullable|exists:users,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. TANGKAP INPUT MENTAH (RAW)
|
||||||
|
$inputRaw = [
|
||||||
|
'nilai' => $validatedData['nilai'],
|
||||||
|
'mapel_favorit' => $validatedData['mapel_favorit'],
|
||||||
|
'minat' => $validatedData['minat'],
|
||||||
|
'cita_cita' => $validatedData['cita_cita'],
|
||||||
|
'preferensi_studi' => $validatedData['preferensi_studi']
|
||||||
|
];
|
||||||
|
|
||||||
|
// 3. PROSES PREPROCESSING
|
||||||
|
$dataBersih = $this->preprocessingService->preprocess((object) $inputRaw);
|
||||||
|
|
||||||
|
// 4. JALANKAN PREDIKSI C4.5 LENGKAP
|
||||||
|
if (method_exists($this->c45Service, 'predictDetail')) {
|
||||||
|
$hasilPrediksi = $this->c45Service->predictDetail($inputRaw);
|
||||||
|
$hasilJurusan = $hasilPrediksi['rekomendasi_utama'] ?? 'Teknologi Informasi';
|
||||||
|
$rulePath = $hasilPrediksi['rule_path'] ?? [];
|
||||||
|
} else {
|
||||||
|
$hasilJurusan = 'Teknologi Informasi';
|
||||||
|
$rulePath = [
|
||||||
|
'Nilai = ' . ($dataBersih['nilai'] ?? $inputRaw['nilai']),
|
||||||
|
'Mapel Favorit = ' . $inputRaw['mapel_favorit'],
|
||||||
|
'Preferensi = ' . $inputRaw['preferensi_studi'],
|
||||||
|
'Minat = ' . $inputRaw['minat']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper akses data bersih
|
||||||
|
$getVal = fn($key) => is_array($dataBersih) ? ($dataBersih[$key] ?? '') : ($dataBersih->{$key} ?? '');
|
||||||
|
|
||||||
|
// 5. PENENTUAN USER_ID DAN CREATED_BY
|
||||||
|
$currentUser = Auth::user();
|
||||||
|
$createdBy = $currentUser ? $currentUser->id : null;
|
||||||
|
$userId = ($currentUser && $currentUser->role === 'admin')
|
||||||
|
? $request->input('siswa_id', null)
|
||||||
|
: ($currentUser->id ?? null);
|
||||||
|
|
||||||
|
// 6. SIMPAN KE DATABASE
|
||||||
|
$dataTesting = DataTesting::create([
|
||||||
|
'user_id' => $userId,
|
||||||
|
'created_by' => $createdBy,
|
||||||
|
'nama' => $validatedData['nama'] ?? ($currentUser->name ?? 'Siswa/User'),
|
||||||
|
'nilai_raw' => $inputRaw['nilai'],
|
||||||
|
'mapel_favorit_raw' => $inputRaw['mapel_favorit'],
|
||||||
|
'minat_raw' => $inputRaw['minat'],
|
||||||
|
'cita_cita_raw' => $inputRaw['cita_cita'],
|
||||||
|
'preferensi_studi_raw' => $inputRaw['preferensi_studi'],
|
||||||
|
'nilai' => $getVal('nilai'),
|
||||||
|
'mapel_favorit' => $getVal('mapel_favorit'),
|
||||||
|
'minat' => $getVal('minat'),
|
||||||
|
'cita_cita' => $getVal('cita_cita'),
|
||||||
|
'preferensi_studi' => $getVal('preferensi_studi'),
|
||||||
|
'hasil_rekomendasi' => $hasilJurusan,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 7. DIRECT VIEW BERDASARKAN ROLE USER
|
||||||
|
$viewData = [
|
||||||
|
'dataTesting' => $dataTesting,
|
||||||
|
'hasilJurusan' => $hasilJurusan,
|
||||||
|
'rulePath' => $rulePath,
|
||||||
|
'dataBersih' => $dataBersih,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($currentUser && $currentUser->role === 'admin') {
|
||||||
|
$viewData['isAdmin'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('prediksi.hasil', $viewData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function riwayat()
|
||||||
|
{
|
||||||
|
$riwayats = DataTesting::where('user_id', Auth::id())
|
||||||
|
->latest()
|
||||||
|
->paginate(10);
|
||||||
|
|
||||||
|
return view('siswa.riwayat', compact('riwayats'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showRiwayat($id)
|
||||||
|
{
|
||||||
|
$query = DataTesting::query();
|
||||||
|
if (Auth::user()->role !== 'admin') {
|
||||||
|
$query->where('user_id', Auth::id());
|
||||||
|
}
|
||||||
|
|
||||||
|
$dataTesting = $query->findOrFail($id);
|
||||||
|
|
||||||
|
$inputRaw = [
|
||||||
|
'nilai' => $dataTesting->nilai_raw ?? $dataTesting->nilai,
|
||||||
|
'mapel_favorit' => $dataTesting->mapel_favorit_raw ?? $dataTesting->mapel_favorit,
|
||||||
|
'minat' => $dataTesting->minat_raw ?? $dataTesting->minat,
|
||||||
|
'cita_cita' => $dataTesting->cita_cita_raw ?? $dataTesting->cita_cita,
|
||||||
|
'preferensi_studi' => $dataTesting->preferensi_studi_raw ?? $dataTesting->preferensi_studi,
|
||||||
|
];
|
||||||
|
|
||||||
|
$dataBersih = $this->preprocessingService->preprocess((object) $inputRaw);
|
||||||
|
|
||||||
|
if (method_exists($this->c45Service, 'predictDetail')) {
|
||||||
|
$hasilPrediksi = $this->c45Service->predictDetail($inputRaw);
|
||||||
|
$rulePath = $hasilPrediksi['rule_path'] ?? [];
|
||||||
|
} else {
|
||||||
|
$rulePath = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('prediksi.hasil', [
|
||||||
|
'dataTesting' => $dataTesting,
|
||||||
|
'hasilJurusan' => $dataTesting->hasil_rekomendasi,
|
||||||
|
'rulePath' => $rulePath,
|
||||||
|
'dataBersih' => $dataBersih,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyRiwayat($id)
|
||||||
|
{
|
||||||
|
$data = DataTesting::where('user_id', Auth::id())->findOrFail($id);
|
||||||
|
$data->delete();
|
||||||
|
|
||||||
|
return redirect()->route('riwayat.index')->with('success', 'Catatan riwayat rekomendasi berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class ProfileController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Menampilkan halaman edit profil.
|
||||||
|
*/
|
||||||
|
public function edit(Request $request)
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
// Jika user adalah admin, tampilkan view profil admin
|
||||||
|
if ($user->role === 'admin' || $user->is_admin) {
|
||||||
|
return view('admin.profile.index', [
|
||||||
|
'user' => $user,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika user adalah siswa, tampilkan view profil bawaan
|
||||||
|
return view('profile.edit', [
|
||||||
|
'user' => $user,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memperbarui Informasi Profil (Nama & Email) ke Database.
|
||||||
|
*/
|
||||||
|
public function update(Request $request)
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
// Validasi input
|
||||||
|
$request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,'.$user->id],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Masukkan data baru ke model user
|
||||||
|
$user->name = $request->name;
|
||||||
|
$user->email = $request->email;
|
||||||
|
|
||||||
|
// Jika email diubah, reset email_verified_at jika fitur verifikasi dipakai
|
||||||
|
if ($user->isDirty('email')) {
|
||||||
|
$user->email_verified_at = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SIMPAN KE DATABASE
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return redirect()->route('profile.edit')->with('status', 'profile-updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memperbarui Kata Sandi User ke Database.
|
||||||
|
*/
|
||||||
|
public function updatePassword(Request $request)
|
||||||
|
{
|
||||||
|
// Validasi password lama dan password baru
|
||||||
|
$validated = $request->validate([
|
||||||
|
'current_password' => ['required', 'current_password'],
|
||||||
|
'password' => ['required', 'confirmed', Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Simpan password baru yang sudah di-hash ke Database
|
||||||
|
$request->user()->update([
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('profile.edit')->with('status', 'password-updated');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\DataTesting;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class RekapController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Menampilkan Halaman Dashboard Rekapitulasi & Grafik
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// 1. Total Seluruh Konsultasi
|
||||||
|
$totalKonsultasi = DataTesting::count();
|
||||||
|
|
||||||
|
// 2. Hitung Rekapitulasi Jumlah Siswa per Rekomendasi Jurusan
|
||||||
|
$rekapJurusan = DataTesting::select('hasil_rekomendasi', DB::raw('count(*) as total'))
|
||||||
|
->groupBy('hasil_rekomendasi')
|
||||||
|
->orderBy('total', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Format data untuk Chart.js (Grafik)
|
||||||
|
$labels = $rekapJurusan->pluck('hasil_rekomendasi');
|
||||||
|
$totals = $rekapJurusan->pluck('total');
|
||||||
|
|
||||||
|
// 3. Ambil 10 Riwayat Konsultasi Terbaru
|
||||||
|
$riwayatTerbaru = DataTesting::latest()->take(10)->get();
|
||||||
|
|
||||||
|
return view('admin.rekap', compact(
|
||||||
|
'totalKonsultasi',
|
||||||
|
'rekapJurusan',
|
||||||
|
'labels',
|
||||||
|
'totals',
|
||||||
|
'riwayatTerbaru'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meng-generate dan Mengunduh Laporan Laporan Berformat PDF
|
||||||
|
*/
|
||||||
|
public function exportPdf()
|
||||||
|
{
|
||||||
|
// Ambil Seluruh Data Konsultasi dari DataTesting
|
||||||
|
$data = DataTesting::latest()->get();
|
||||||
|
$totalKonsultasi = $data->count();
|
||||||
|
$tanggalCetak = date('d F Y');
|
||||||
|
|
||||||
|
// Render template blade ke PDF ukuran A4 landscape
|
||||||
|
$pdf = Pdf::loadView('admin.pdf_rekap_konsultasi', compact('data', 'totalKonsultasi', 'tanggalCetak'))
|
||||||
|
->setPaper('a4', 'landscape');
|
||||||
|
|
||||||
|
return $pdf->download('Laporan_Rekap_Konsultasi_Siswa_' . date('Ymd_His') . '.pdf');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class UserController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Menampilkan daftar semua user & form tambah akun internal (Khusus Admin/Guru)
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$users = User::latest()->paginate(10);
|
||||||
|
return view('admin.users.index', compact('users'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menyimpan Akun Internal Baru (Admin / Guru) oleh Admin.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'email' => 'required|string|email|max:255|unique:users',
|
||||||
|
'password' => 'required|string|min:8',
|
||||||
|
'role' => 'required|in:admin', // Hanya Admin (Guru BK)
|
||||||
|
], [
|
||||||
|
'email.unique' => 'Email ini sudah terdaftar.',
|
||||||
|
'password.min' => 'Password minimal 8 karakter.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
User::create([
|
||||||
|
'name' => $request->name,
|
||||||
|
'email' => $request->email,
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
'role' => $request->role,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.users.index')->with('success', 'Akun Admin/Guru BK berhasil ditambahkan!');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menghapus user.
|
||||||
|
*/
|
||||||
|
public function destroy(User $user)
|
||||||
|
{
|
||||||
|
// Hindari menghapus akun diri sendiri yang sedang login
|
||||||
|
if (auth()->id() === $user->id) {
|
||||||
|
return back()->with('error', 'Anda tidak dapat menghapus akun sendiri!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->delete();
|
||||||
|
return back()->with('success', 'Akun berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Events\Lockout;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class LoginRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email' => ['required', 'string', 'email'],
|
||||||
|
'password' => ['required', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to authenticate the request's credentials.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function authenticate(): void
|
||||||
|
{
|
||||||
|
$this->ensureIsNotRateLimited();
|
||||||
|
|
||||||
|
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||||
|
RateLimiter::hit($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => trans('auth.failed'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
RateLimiter::clear($this->throttleKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the login request is not rate limited.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function ensureIsNotRateLimited(): void
|
||||||
|
{
|
||||||
|
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event(new Lockout($this));
|
||||||
|
|
||||||
|
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => trans('auth.throttle', [
|
||||||
|
'seconds' => $seconds,
|
||||||
|
'minutes' => ceil($seconds / 60),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rate limiting throttle key for the request.
|
||||||
|
*/
|
||||||
|
public function throttleKey(): string
|
||||||
|
{
|
||||||
|
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ProfileUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'lowercase',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique(User::class)->ignore($this->user()->id),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class DataTesting extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'data_testing';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'created_by',
|
||||||
|
'nama',
|
||||||
|
|
||||||
|
// Teks Inputan Asli (RAW) dari Siswa
|
||||||
|
'nilai_raw',
|
||||||
|
'mapel_favorit_raw',
|
||||||
|
'minat_raw',
|
||||||
|
'cita_cita_raw',
|
||||||
|
'preferensi_studi_raw',
|
||||||
|
|
||||||
|
// Hasil Konversi/Kategori dari PreprocessingService
|
||||||
|
'nilai',
|
||||||
|
'mapel_favorit',
|
||||||
|
'minat',
|
||||||
|
'cita_cita',
|
||||||
|
'preferensi_studi',
|
||||||
|
|
||||||
|
// Hasil Prediksi Akhir Algoritma C4.5
|
||||||
|
'hasil_rekomendasi'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Relasi ke Siswa pemilik data
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relasi ke User penginput (Admin / Siswa itu sendiri)
|
||||||
|
public function creator()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'created_by');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class DataTraining extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'data_training';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'nilai',
|
||||||
|
'nilai_raw',
|
||||||
|
'mapel_favorit',
|
||||||
|
'mapel_favorit_raw',
|
||||||
|
'minat',
|
||||||
|
'minat_raw',
|
||||||
|
'cita_cita',
|
||||||
|
'cita_cita_raw',
|
||||||
|
'preferensi_studi',
|
||||||
|
'preferensi_studi_raw',
|
||||||
|
'jurusan',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Database\Factories\UserFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<UserFactory> */
|
||||||
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
|
||||||
|
'name',
|
||||||
|
|
||||||
|
'email',
|
||||||
|
|
||||||
|
'password',
|
||||||
|
|
||||||
|
'role'
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
// Menetapkan aturan baku password untuk seluruh pembuatan/pembaruan akun
|
||||||
|
Password::defaults(function () {
|
||||||
|
return Password::min(8)
|
||||||
|
->letters()
|
||||||
|
->mixedCase() // Wajib huruf besar & kecil
|
||||||
|
->numbers() // Wajib angka
|
||||||
|
->symbols(); // Wajib simbol (@$!%*?&)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,568 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\DataTraining;
|
||||||
|
use App\Services\PreprocessingService;
|
||||||
|
|
||||||
|
class C45Service
|
||||||
|
{
|
||||||
|
// Atribut yang digunakan untuk memprediksi target (jurusan)
|
||||||
|
// Ditambahkan 'mapel_favorit' ke dalam atribut prediksi
|
||||||
|
private $atribut = ['nilai', 'mapel_favorit', 'minat', 'cita_cita', 'preferensi_studi'];
|
||||||
|
|
||||||
|
protected $preprocessingService;
|
||||||
|
|
||||||
|
// Cache data agar tidak berulang kali memproses database
|
||||||
|
private $preprocessedData = null;
|
||||||
|
|
||||||
|
public function __construct(PreprocessingService $preprocessingService)
|
||||||
|
{
|
||||||
|
$this->preprocessingService = $preprocessingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTrainingData()
|
||||||
|
{
|
||||||
|
return DataTraining::all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil data training dan terapkan preprocessing serta pembersihan whitespace/string.
|
||||||
|
*/
|
||||||
|
public function getPreprocessedData()
|
||||||
|
{
|
||||||
|
if ($this->preprocessedData !== null) {
|
||||||
|
return $this->preprocessedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawRecords = DataTraining::all();
|
||||||
|
$processed = collect();
|
||||||
|
|
||||||
|
foreach ($rawRecords as $row) {
|
||||||
|
// Terapkan preprocessing
|
||||||
|
$data = $this->preprocessingService->preprocess($row);
|
||||||
|
|
||||||
|
// Terapkan TRIM & Sanitasi String pada setiap kolom agar tidak ada bug matching
|
||||||
|
$sanitized = [];
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
$sanitized[$key] = is_string($value) ? trim($value) : $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$processed->push((object) $sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->preprocessedData = $processed;
|
||||||
|
return $this->preprocessedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalData()
|
||||||
|
{
|
||||||
|
return $this->getPreprocessedData()->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getJumlahJurusan()
|
||||||
|
{
|
||||||
|
return $this->getPreprocessedData()
|
||||||
|
->groupBy('jurusan')
|
||||||
|
->map(function ($items, $key) {
|
||||||
|
return (object) ['jurusan' => $key, 'jumlah' => $items->count()];
|
||||||
|
})->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menghitung Entropy Total Dataset menggunakan log2
|
||||||
|
*/
|
||||||
|
public function entropyTotal()
|
||||||
|
{
|
||||||
|
$data = $this->getPreprocessedData();
|
||||||
|
$totalData = $data->count();
|
||||||
|
if ($totalData == 0) return 0;
|
||||||
|
|
||||||
|
$kelas = $data->groupBy('jurusan');
|
||||||
|
$entropy = 0;
|
||||||
|
|
||||||
|
foreach ($kelas as $items) {
|
||||||
|
$probabilitas = $items->count() / $totalData;
|
||||||
|
if ($probabilitas > 0) {
|
||||||
|
$entropy -= $probabilitas * log($probabilitas, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return round($entropy, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menghitung Entropy untuk setiap nilai unik pada suatu atribut
|
||||||
|
*/
|
||||||
|
public function hitungEntropyAtribut($atribut)
|
||||||
|
{
|
||||||
|
$data = $this->getPreprocessedData();
|
||||||
|
|
||||||
|
// Pastikan unique value benar-benar bersih
|
||||||
|
$nilaiUnik = $data->pluck($atribut)->map(fn($val) => trim($val))->unique()->values();
|
||||||
|
$hasil = [];
|
||||||
|
|
||||||
|
foreach ($nilaiUnik as $nilai) {
|
||||||
|
// Filter dengan Strict Trim
|
||||||
|
$subset = $data->filter(function ($item) use ($atribut, $nilai) {
|
||||||
|
return trim($item->$atribut ?? '') === $nilai;
|
||||||
|
});
|
||||||
|
|
||||||
|
$jumlahSubset = $subset->count();
|
||||||
|
if ($jumlahSubset == 0) continue;
|
||||||
|
|
||||||
|
$kelas = $subset->groupBy('jurusan');
|
||||||
|
$entropy = 0;
|
||||||
|
|
||||||
|
foreach ($kelas as $items) {
|
||||||
|
$p = $items->count() / $jumlahSubset;
|
||||||
|
if ($p > 0) {
|
||||||
|
$entropy -= $p * log($p, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasil[] = [
|
||||||
|
'nilai' => $nilai,
|
||||||
|
'jumlah' => $jumlahSubset,
|
||||||
|
'entropy' => round($entropy, 6)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Urutkan berdasarkan jumlah terbanyak agar rapi di tampilan
|
||||||
|
usort($hasil, fn($a, $b) => $b['jumlah'] <=> $a['jumlah']);
|
||||||
|
|
||||||
|
return $hasil;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hitungGain($atribut)
|
||||||
|
{
|
||||||
|
$totalData = $this->getTotalData();
|
||||||
|
if ($totalData == 0) return 0;
|
||||||
|
|
||||||
|
$entropyTotal = $this->entropyTotal();
|
||||||
|
$entropyAtribut = $this->hitungEntropyAtribut($atribut);
|
||||||
|
$totalEntropyAtribut = 0;
|
||||||
|
|
||||||
|
foreach ($entropyAtribut as $item) {
|
||||||
|
$totalEntropyAtribut += ($item['jumlah'] / $totalData) * $item['entropy'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($entropyTotal - $totalEntropyAtribut, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hitungSplitInfo($atribut)
|
||||||
|
{
|
||||||
|
$data = $this->getPreprocessedData();
|
||||||
|
$total = $data->count();
|
||||||
|
if ($total == 0) return 0;
|
||||||
|
|
||||||
|
$nilaiUnik = $data->pluck($atribut)->map(fn($val) => trim($val))->unique();
|
||||||
|
$split = 0;
|
||||||
|
|
||||||
|
foreach ($nilaiUnik as $nilai) {
|
||||||
|
$jumlah = $data->filter(fn($item) => trim($item->$atribut ?? '') === $nilai)->count();
|
||||||
|
$p = $jumlah / $total;
|
||||||
|
if ($p > 0) {
|
||||||
|
$split -= $p * log($p, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return round($split, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hitungGainRatio($atribut)
|
||||||
|
{
|
||||||
|
$gain = $this->hitungGain($atribut);
|
||||||
|
$split = $this->hitungSplitInfo($atribut);
|
||||||
|
return $split == 0 ? 0 : round($gain / $split, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRootNode()
|
||||||
|
{
|
||||||
|
$gain = [];
|
||||||
|
foreach ($this->atribut as $atribut) {
|
||||||
|
$gain[$atribut] = $this->hitungGainRatio($atribut);
|
||||||
|
}
|
||||||
|
arsort($gain);
|
||||||
|
$root = array_key_first($gain);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'root' => $root,
|
||||||
|
'gain' => $gain[$root],
|
||||||
|
'semua_gain' => $gain
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSubset($atribut)
|
||||||
|
{
|
||||||
|
return $this->getPreprocessedData()
|
||||||
|
->groupBy(fn($item) => trim($item->$atribut))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// HELPER SUBSET RECURSIVE (PENANGANAN TYPE SAFETY OBJECT/ARRAY)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private function getVal($row, $key)
|
||||||
|
{
|
||||||
|
if (is_object($row)) {
|
||||||
|
return trim($row->$key ?? '');
|
||||||
|
}
|
||||||
|
return trim($row[$key] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function entropySubset($data)
|
||||||
|
{
|
||||||
|
$jumlahData = count($data);
|
||||||
|
if ($jumlahData == 0) return 0;
|
||||||
|
|
||||||
|
$kelas = [];
|
||||||
|
foreach ($data as $row) {
|
||||||
|
$jurusan = $this->getVal($row, 'jurusan');
|
||||||
|
$kelas[$jurusan] = ($kelas[$jurusan] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entropy = 0;
|
||||||
|
foreach ($kelas as $jumlah) {
|
||||||
|
$p = $jumlah / $jumlahData;
|
||||||
|
if ($p > 0) $entropy -= $p * log($p, 2);
|
||||||
|
}
|
||||||
|
return round($entropy, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function gainSubset($data, $atribut)
|
||||||
|
{
|
||||||
|
$entropyAwal = $this->entropySubset($data);
|
||||||
|
$jumlahTotal = count($data);
|
||||||
|
if ($jumlahTotal == 0) return 0;
|
||||||
|
|
||||||
|
$kelompok = [];
|
||||||
|
foreach ($data as $row) {
|
||||||
|
$val = $this->getVal($row, $atribut);
|
||||||
|
$kelompok[$val][] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entropyBaru = 0;
|
||||||
|
foreach ($kelompok as $group) {
|
||||||
|
$entropyBaru += (count($group) / $jumlahTotal) * $this->entropySubset($group);
|
||||||
|
}
|
||||||
|
return round($entropyAwal - $entropyBaru, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function splitInfoSubset($data, $atribut)
|
||||||
|
{
|
||||||
|
$jumlah = count($data);
|
||||||
|
if ($jumlah == 0) return 0;
|
||||||
|
|
||||||
|
$group = [];
|
||||||
|
foreach ($data as $row) {
|
||||||
|
$val = $this->getVal($row, $atribut);
|
||||||
|
$group[$val][] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$split = 0;
|
||||||
|
foreach ($group as $g) {
|
||||||
|
$p = count($g) / $jumlah;
|
||||||
|
if ($p > 0) $split -= $p * log($p, 2);
|
||||||
|
}
|
||||||
|
return round($split, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function gainRatioSubset($data, $atribut)
|
||||||
|
{
|
||||||
|
$gain = $this->gainSubset($data, $atribut);
|
||||||
|
$split = $this->splitInfoSubset($data, $atribut);
|
||||||
|
return $split == 0 ? 0 : round($gain / $split, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bestAttribute($data, $atributTersisa)
|
||||||
|
{
|
||||||
|
$hasil = [];
|
||||||
|
foreach ($atributTersisa as $atribut) {
|
||||||
|
$hasil[$atribut] = $this->gainRatioSubset($data, $atribut);
|
||||||
|
}
|
||||||
|
arsort($hasil);
|
||||||
|
return array_key_first($hasil);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isPure($data)
|
||||||
|
{
|
||||||
|
if (count($data) == 0) return false;
|
||||||
|
$kelas = [];
|
||||||
|
foreach ($data as $row) {
|
||||||
|
$kelas[$this->getVal($row, 'jurusan')] = true;
|
||||||
|
}
|
||||||
|
return count($kelas) == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function majorityClass($data)
|
||||||
|
{
|
||||||
|
$kelas = [];
|
||||||
|
foreach ($data as $row) {
|
||||||
|
$j = $this->getVal($row, 'jurusan');
|
||||||
|
$kelas[$j] = ($kelas[$j] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
arsort($kelas);
|
||||||
|
return array_key_first($kelas) ?? 'Tidak Diketahui';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildTree($data = null, $atribut = null)
|
||||||
|
{
|
||||||
|
if ($data === null) {
|
||||||
|
$data = $this->getPreprocessedData()->toArray();
|
||||||
|
}
|
||||||
|
if ($atribut === null) {
|
||||||
|
$atribut = $this->atribut;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan kelas mayoritas di NODE LOKAL ini untuk penanganan Unseen Value
|
||||||
|
$localMajority = $this->majorityClass($data);
|
||||||
|
|
||||||
|
// 1. Jika data murni (Pure)
|
||||||
|
if ($this->isPure($data)) {
|
||||||
|
return [
|
||||||
|
'type' => 'leaf',
|
||||||
|
'label' => $this->getVal($data[0], 'jurusan')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Jika atribut habis, jadikan Leaf dengan kelas mayoritas lokal
|
||||||
|
if (count($atribut) == 0) {
|
||||||
|
return [
|
||||||
|
'type' => 'leaf',
|
||||||
|
'label' => $localMajority
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$best = $this->bestAttribute($data, $atribut);
|
||||||
|
|
||||||
|
// Simpan 'local_majority' dan 'subset_data' pada struktur Node
|
||||||
|
$tree = [
|
||||||
|
'type' => 'node',
|
||||||
|
'attribute' => $best,
|
||||||
|
'local_majority' => $localMajority,
|
||||||
|
'children' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
$nilai = [];
|
||||||
|
foreach ($data as $row) {
|
||||||
|
$v = $this->getVal($row, $best);
|
||||||
|
$nilai[$v][] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$atributBaru = array_values(array_diff($atribut, [$best]));
|
||||||
|
|
||||||
|
foreach ($nilai as $value => $subset) {
|
||||||
|
$tree['children'][$value] = $this->buildTree($subset, $atributBaru);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tampilTree($tree, $indent = '')
|
||||||
|
{
|
||||||
|
if ($tree['type'] == 'leaf') {
|
||||||
|
return $indent . "➡ Jurusan = " . $tree['label'] . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasil = $indent . "[" . strtoupper($tree['attribute']) . "]\n";
|
||||||
|
foreach ($tree['children'] as $value => $child) {
|
||||||
|
$hasil .= $indent . "├── " . $value . "\n";
|
||||||
|
$hasil .= $this->tampilTree($child, $indent . "│ ");
|
||||||
|
}
|
||||||
|
return $hasil;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function predictDetail($inputRaw)
|
||||||
|
{
|
||||||
|
$inputObj = (object) $inputRaw;
|
||||||
|
$inputPreprocessed = $this->preprocessingService->preprocess($inputObj);
|
||||||
|
|
||||||
|
$tree = $this->buildTree();
|
||||||
|
$path = [];
|
||||||
|
|
||||||
|
$resultNode = $this->predictRecursiveWithDetail($tree, $inputPreprocessed, $path);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'rekomendasi_utama' => $resultNode['label'],
|
||||||
|
'rule_path' => $resultNode['path'],
|
||||||
|
'detail_subset' => $resultNode['subset_info'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Melakukan rekursi prediksi dengan penanganan Unseen Value yang presisi.
|
||||||
|
*/
|
||||||
|
private function predictRecursiveWithDetail($tree, $input, &$path)
|
||||||
|
{
|
||||||
|
if ($tree['type'] == 'leaf') {
|
||||||
|
return [
|
||||||
|
'label' => $tree['label'],
|
||||||
|
'path' => $path
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$atribut = $tree['attribute'];
|
||||||
|
$nilai = isset($input[$atribut]) ? trim($input[$atribut]) : null;
|
||||||
|
|
||||||
|
// 1. Jika cabang cocok secara presisi, teruskan secara normal
|
||||||
|
if ($nilai !== null && isset($tree['children'][$nilai])) {
|
||||||
|
$path[] = strtolower($atribut) . ' = ' . $nilai;
|
||||||
|
return $this->predictRecursiveWithDetail($tree['children'][$nilai], $input, $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. PENANGANAN UNSEEN VALUE / CABANG BUNTU:
|
||||||
|
// Evaluasi seluruh cabang anak untuk melihat mana yang paling COCOK dengan sisa input user
|
||||||
|
$bestChildNode = null;
|
||||||
|
$maxMatchScore = -1;
|
||||||
|
|
||||||
|
foreach ($tree['children'] as $childVal => $childNode) {
|
||||||
|
$score = $this->hitungKemiripanInput($childNode, $input);
|
||||||
|
if ($score > $maxMatchScore) {
|
||||||
|
$maxMatchScore = $score;
|
||||||
|
$bestChildNode = $childNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bestChildNode !== null) {
|
||||||
|
$path[] = strtolower($atribut) . " = '{$nilai}' (Unseen -> Dialihkan ke cabang paling relevan)";
|
||||||
|
return $this->predictRecursiveWithDetail($bestChildNode, $input, $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback terakhir jika buntu total
|
||||||
|
$fallbackLabel = $tree['local_majority'] ?? $this->majorityClass($this->getPreprocessedData()->toArray());
|
||||||
|
$path[] = strtolower($atribut) . " = '{$nilai}' (Fallback Majority: {$fallbackLabel})";
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => $fallbackLabel,
|
||||||
|
'path' => $path
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper untuk mengukur berapa banyak atribut input user yang cocok pada suatu node cabang
|
||||||
|
*/
|
||||||
|
private function hitungKemiripanInput($node, $input)
|
||||||
|
{
|
||||||
|
if ($node['type'] === 'leaf') return 1;
|
||||||
|
|
||||||
|
$score = 0;
|
||||||
|
$attr = $node['attribute'];
|
||||||
|
|
||||||
|
if (isset($input[$attr]) && isset($node['children'][$input[$attr]])) {
|
||||||
|
$score += 2; // Berikan bobot lebih tinggi jika ada kesamaan atribut
|
||||||
|
}
|
||||||
|
|
||||||
|
return $score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengambil detail perhitungan Gain Ratio untuk SEMUA level/node secara rekursif.
|
||||||
|
*/
|
||||||
|
public function getDetailSubsets($data = null, $atributTersisa = null, $nodeLevel = 1, $pathName = 'Root Node')
|
||||||
|
{
|
||||||
|
if ($data === null) {
|
||||||
|
$data = $this->getPreprocessedData()->toArray();
|
||||||
|
}
|
||||||
|
if ($atributTersisa === null) {
|
||||||
|
$atributTersisa = $this->atribut;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika data murni atau atribut habis, tidak ada pembagian subset lagi
|
||||||
|
if ($this->isPure($data) || empty($atributTersisa)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Cari Best Attribute untuk Node saat ini
|
||||||
|
$best = $this->bestAttribute($data, $atributTersisa);
|
||||||
|
if (!$best) return [];
|
||||||
|
|
||||||
|
// 2. Kelompokkan data berdasarkan atribut terbaik yang terpilih
|
||||||
|
$subsets = [];
|
||||||
|
foreach ($data as $row) {
|
||||||
|
$val = $this->getVal($row, $best);
|
||||||
|
$subsets[$val][] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sisaAtributSelanjutnya = array_values(array_diff($atributTersisa, [$best]));
|
||||||
|
$nodeDetails = [];
|
||||||
|
|
||||||
|
// 3. Hitung kalkulasi Gain Ratio untuk subset pada node ini
|
||||||
|
$detailBranches = [];
|
||||||
|
foreach ($subsets as $nilaiCabang => $items) {
|
||||||
|
$isPure = $this->isPure($items);
|
||||||
|
$jumlahData = count($items);
|
||||||
|
$kalkulasiAtribut = [];
|
||||||
|
|
||||||
|
if (!$isPure && !empty($sisaAtributSelanjutnya)) {
|
||||||
|
foreach ($sisaAtributSelanjutnya as $attr) {
|
||||||
|
$gain = $this->gainSubset($items, $attr);
|
||||||
|
$split = $this->splitInfoSubset($items, $attr);
|
||||||
|
$gainRatio = $split == 0 ? 0 : round($gain / $split, 6);
|
||||||
|
|
||||||
|
$kalkulasiAtribut[] = [
|
||||||
|
'atribut' => $attr,
|
||||||
|
'gain' => $gain,
|
||||||
|
'split_info' => $split,
|
||||||
|
'gain_ratio' => $gainRatio,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
usort($kalkulasiAtribut, fn($a, $b) => $b['gain_ratio'] <=> $a['gain_ratio']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$winnerAtribut = $isPure ? null : ($kalkulasiAtribut[0]['atribut'] ?? null);
|
||||||
|
|
||||||
|
$detailBranches[$nilaiCabang] = [
|
||||||
|
'nilai_cabang' => $nilaiCabang,
|
||||||
|
'jumlah_data' => $jumlahData,
|
||||||
|
'is_pure' => $isPure,
|
||||||
|
'pemenang' => $isPure ? $this->majorityClass($items) : ($winnerAtribut ?? '-'),
|
||||||
|
'kalkulasi' => $kalkulasiAtribut,
|
||||||
|
'items' => $items
|
||||||
|
];
|
||||||
|
|
||||||
|
// 4. REKURSI: Jika node cabang belum murni, telusuri terus ke Node-3, Node-4, dst.
|
||||||
|
if (!$isPure && !empty($sisaAtributSelanjutnya) && $winnerAtribut) {
|
||||||
|
$childPath = "{$pathName} ➔ {$best}='{$nilaiCabang}'";
|
||||||
|
$childNodes = $this->getDetailSubsets($items, $sisaAtributSelanjutnya, $nodeLevel + 1, $childPath);
|
||||||
|
$nodeDetails = array_merge($nodeDetails, $childNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gabungkan detail node saat ini dengan detail node anak-anaknya
|
||||||
|
$currentStructure = [
|
||||||
|
'level' => $nodeLevel,
|
||||||
|
'path_label' => $pathName,
|
||||||
|
'split_by' => $best,
|
||||||
|
'branches' => $detailBranches
|
||||||
|
];
|
||||||
|
|
||||||
|
return array_merge([$currentStructure], $nodeDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ekstrak pohon keputusan menjadi daftar aturan IF-THEN
|
||||||
|
*/
|
||||||
|
public function generateRules($tree = null, $currentRule = [])
|
||||||
|
{
|
||||||
|
if ($tree === null) {
|
||||||
|
$tree = $this->buildTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules = [];
|
||||||
|
|
||||||
|
if ($tree['type'] === 'leaf') {
|
||||||
|
$ruleText = "IF " . implode(" AND ", $currentRule) . " THEN Jurusan = " . $tree['label'];
|
||||||
|
return [$ruleText];
|
||||||
|
}
|
||||||
|
|
||||||
|
$attr = strtoupper(str_replace('_', ' ', $tree['attribute']));
|
||||||
|
|
||||||
|
foreach ($tree['children'] as $val => $child) {
|
||||||
|
$newRule = $currentRule;
|
||||||
|
$newRule[] = "{$attr} = '{$val}'";
|
||||||
|
$rules = array_merge($rules, $this->generateRules($child, $newRule));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rules;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,372 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class PreprocessingService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Helper privat untuk mencari nilai terbanyak (Modus/Kelas Mayoritas)
|
||||||
|
* dari tabel data training secara otomatis.
|
||||||
|
*/
|
||||||
|
private function getKelasMayoritas($kolom, $defaultFallback)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Asumsi nama tabel data training adalah 'data_training'
|
||||||
|
$modus = DB::table('data_training')
|
||||||
|
->select($kolom, DB::raw('COUNT(*) as total'))
|
||||||
|
->whereNotNull($kolom)
|
||||||
|
->where($kolom, '!=', '')
|
||||||
|
->groupBy($kolom)
|
||||||
|
->orderBy('total', 'DESC')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return $modus ? $modus->$kolom : $defaultFallback;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $defaultFallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function kategoriNilai($nilai)
|
||||||
|
{
|
||||||
|
if (is_string($nilai) && !is_numeric(trim($nilai))) {
|
||||||
|
return trim($nilai);
|
||||||
|
}
|
||||||
|
|
||||||
|
$nilai = (float) $nilai;
|
||||||
|
|
||||||
|
if ($nilai >= 90) return 'Tinggi';
|
||||||
|
if ($nilai >= 75) return 'Sedang';
|
||||||
|
|
||||||
|
return 'Rendah';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Penanganan dan Pengelompokan / Normalisasi Mata Pelajaran Favorit
|
||||||
|
*/
|
||||||
|
public function kategoriMapelFavorit($mapel)
|
||||||
|
{
|
||||||
|
if (empty($mapel)) {
|
||||||
|
return $this->getKelasMayoritas('mapel_favorit', 'Matematika');
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapelClean = trim($mapel);
|
||||||
|
|
||||||
|
$kategoriStandar = [
|
||||||
|
'Matematika', 'Bahasa Inggris', 'Biologi',
|
||||||
|
'Fisika', 'Ekonomi', 'Sejarah'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($kategoriStandar as $kat) {
|
||||||
|
if (strcasecmp($mapelClean, $kat) === 0) {
|
||||||
|
return $kat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapelRaw = strtolower($mapelClean);
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($mapelRaw, 'mat') || str_contains($mapelRaw, 'hitung') ||
|
||||||
|
str_contains($mapelRaw, 'kalkulus') || str_contains($mapelRaw, 'angka')
|
||||||
|
) {
|
||||||
|
return 'Matematika';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($mapelRaw, 'inggris') || str_contains($mapelRaw, 'english') ||
|
||||||
|
str_contains($mapelRaw, 'bing') || str_contains($mapelRaw, 'bahasa asing')
|
||||||
|
) {
|
||||||
|
return 'Bahasa Inggris';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($mapelRaw, 'bio') || str_contains($mapelRaw, 'tumbuhan') ||
|
||||||
|
str_contains($mapelRaw, 'hewan') || str_contains($mapelRaw, 'anatomi')
|
||||||
|
) {
|
||||||
|
return 'Biologi';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($mapelRaw, 'fis') || str_contains($mapelRaw, 'mekanika') ||
|
||||||
|
str_contains($mapelRaw, 'listrik')
|
||||||
|
) {
|
||||||
|
return 'Fisika';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($mapelRaw, 'eko') || str_contains($mapelRaw, 'akuntansi') ||
|
||||||
|
str_contains($mapelRaw, 'keuangan') || str_contains($mapelRaw, 'bisnis')
|
||||||
|
) {
|
||||||
|
return 'Ekonomi';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($mapelRaw, 'sej') || str_contains($mapelRaw, 'sosio') ||
|
||||||
|
str_contains($mapelRaw, 'sejarah') || str_contains($mapelRaw, 'ips')
|
||||||
|
) {
|
||||||
|
return 'Sejarah';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getKelasMayoritas('mapel_favorit', 'Matematika');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function kategoriMinat($minat)
|
||||||
|
{
|
||||||
|
if (empty($minat)) {
|
||||||
|
return $this->getKelasMayoritas('minat', 'Teknologi Info & Pemrograman');
|
||||||
|
}
|
||||||
|
|
||||||
|
$minatClean = trim($minat);
|
||||||
|
|
||||||
|
$kategoriStandar = [
|
||||||
|
'Budidaya Tanaman', 'Kimia Pangan', 'Budidaya Ternak',
|
||||||
|
'Teknologi Info & Pemrograman', 'Energi & Tek Mesin',
|
||||||
|
'Bahasa & Komunikasi', 'Bisnis', 'Kesehatan'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($kategoriStandar as $kat) {
|
||||||
|
if (strcasecmp($minatClean, $kat) === 0) {
|
||||||
|
return $kat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$minatRaw = strtolower($minatClean);
|
||||||
|
|
||||||
|
// 1. Teknologi Informasi & Pemrograman (JTI Polije)
|
||||||
|
if (
|
||||||
|
str_contains($minatRaw, 'coding') || str_contains($minatRaw, 'jaringan') ||
|
||||||
|
str_contains($minatRaw, 'rekayasa perangkat lunak') || str_contains($minatRaw, 'rpl') ||
|
||||||
|
str_contains($minatRaw, 'pemrograman') || str_contains($minatRaw, 'cyber') ||
|
||||||
|
str_contains($minatRaw, 'komputer') || str_contains($minatRaw, 'informatika') ||
|
||||||
|
str_contains($minatRaw, 'info & pemrog') || str_contains($minatRaw, 'olah data') ||
|
||||||
|
str_contains($minatRaw, 'data') || str_contains($minatRaw, 'game') ||
|
||||||
|
str_contains($minatRaw, 'web') || str_contains($minatRaw, 'aplikasi') ||
|
||||||
|
str_contains($minatRaw, 'software') || str_contains($minatRaw, 'developer')
|
||||||
|
) {
|
||||||
|
return 'Teknologi Info & Pemrograman';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Bahasa, Media & Komunikasi
|
||||||
|
if (
|
||||||
|
str_contains($minatRaw, 'masyarakat') || str_contains($minatRaw, 'bahasa') ||
|
||||||
|
str_contains($minatRaw, 'wisata') || str_contains($minatRaw, 'media') ||
|
||||||
|
str_contains($minatRaw, 'komunikasi') || str_contains($minatRaw, 'konten') ||
|
||||||
|
str_contains($minatRaw, 'content') || str_contains($minatRaw, 'kreator') ||
|
||||||
|
str_contains($minatRaw, 'creator') || str_contains($minatRaw, 'youtube') ||
|
||||||
|
str_contains($minatRaw, 'desain') || str_contains($minatRaw, 'nulis') ||
|
||||||
|
str_contains($minatRaw, 'editing') || str_contains($minatRaw, 'video') ||
|
||||||
|
str_contains($minatRaw, 'foto')
|
||||||
|
) {
|
||||||
|
return 'Bahasa & Komunikasi';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Bisnis & Manajemen Agribisnis
|
||||||
|
if (
|
||||||
|
str_contains($minatRaw, 'akuntansi') || str_contains($minatRaw, 'admin') ||
|
||||||
|
str_contains($minatRaw, 'manajemen') || str_contains($minatRaw, 'industri') ||
|
||||||
|
str_contains($minatRaw, 'keuangan') || str_contains($minatRaw, 'investasi') ||
|
||||||
|
str_contains($minatRaw, 'bisnis') || str_contains($minatRaw, 'dagang') ||
|
||||||
|
str_contains($minatRaw, 'jual') || str_contains($minatRaw, 'pemasaran') ||
|
||||||
|
str_contains($minatRaw, 'marketing') || str_contains($minatRaw, 'wirausaha')
|
||||||
|
) {
|
||||||
|
return 'Bisnis';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Budidaya Tanaman / Pertanian
|
||||||
|
if (
|
||||||
|
str_contains($minatRaw, 'tani') || str_contains($minatRaw, 'kebun') ||
|
||||||
|
str_contains($minatRaw, 'agronomi') || str_contains($minatRaw, 'benih') ||
|
||||||
|
str_contains($minatRaw, 'tanaman') || str_contains($minatRaw, 'budidaya tanaman') ||
|
||||||
|
str_contains($minatRaw, 'hortikultura') || str_contains($minatRaw, 'lahan')
|
||||||
|
) {
|
||||||
|
return 'Budidaya Tanaman';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Kimia Pangan & Teknologi Pertanian
|
||||||
|
if (
|
||||||
|
str_contains($minatRaw, 'kimia') || str_contains($minatRaw, 'pangan') ||
|
||||||
|
str_contains($minatRaw, 'proses') || str_contains($minatRaw, 'olah makanan') ||
|
||||||
|
str_contains($minatRaw, 'kuliner') || str_contains($minatRaw, 'gizi pangan')
|
||||||
|
) {
|
||||||
|
return 'Kimia Pangan';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Budidaya Ternak / Peternakan
|
||||||
|
if (
|
||||||
|
str_contains($minatRaw, 'ternak') || str_contains($minatRaw, 'hewan') ||
|
||||||
|
str_contains($minatRaw, 'pakan') || str_contains($minatRaw, 'unggas') ||
|
||||||
|
str_contains($minatRaw, 'budidaya ternak') || str_contains($minatRaw, 'pelihara')
|
||||||
|
) {
|
||||||
|
return 'Budidaya Ternak';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Energi & Teknik Mesin
|
||||||
|
if (
|
||||||
|
str_contains($minatRaw, 'mesin') || str_contains($minatRaw, 'otomotif') ||
|
||||||
|
str_contains($minatRaw, 'listrik') || str_contains($minatRaw, 'energi') ||
|
||||||
|
str_contains($minatRaw, 'tek mesin') || str_contains($minatRaw, 'robot')
|
||||||
|
) {
|
||||||
|
return 'Energi & Tek Mesin';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Kesehatan
|
||||||
|
if (
|
||||||
|
str_contains($minatRaw, 'medis') || str_contains($minatRaw, 'gizi') ||
|
||||||
|
str_contains($minatRaw, 'sehat') || str_contains($minatRaw, 'klinik') ||
|
||||||
|
str_contains($minatRaw, 'obat') || str_contains($minatRaw, 'rawat') ||
|
||||||
|
str_contains($minatRaw, 'rekam medis')
|
||||||
|
) {
|
||||||
|
return 'Kesehatan';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getKelasMayoritas('minat', 'Teknologi Info & Pemrograman');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function kategoriCitaCita($cita)
|
||||||
|
{
|
||||||
|
if (empty($cita)) {
|
||||||
|
return $this->getKelasMayoritas('cita_cita', 'IT');
|
||||||
|
}
|
||||||
|
|
||||||
|
$citaClean = trim($cita);
|
||||||
|
|
||||||
|
$kategoriStandar = [
|
||||||
|
'Ahli Agronomi', 'Peternak', 'Pengusaha', 'Ahli Gizi',
|
||||||
|
'Manager Rantai Pasok', 'Manajer', 'Quality Control',
|
||||||
|
'Petani Modern', 'Teknisi', 'Bahasa & Pariwisata', 'IT'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($kategoriStandar as $kat) {
|
||||||
|
if (strcasecmp($citaClean, $kat) === 0) {
|
||||||
|
return $kat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$citaRaw = strtolower($citaClean);
|
||||||
|
|
||||||
|
if (str_contains($citaRaw, 'agronomi') || str_contains($citaRaw, 'ahli agronomi')) return 'Ahli Agronomi';
|
||||||
|
if (str_contains($citaRaw, 'farm') || str_contains($citaRaw, 'ternak') || str_contains($citaRaw, 'peternak')) return 'Peternak';
|
||||||
|
if (str_contains($citaRaw, 'akuntan') || str_contains($citaRaw, 'pajak') || str_contains($citaRaw, 'bisnis') || str_contains($citaRaw, 'pengusaha') || str_contains($citaRaw, 'wirausaha') || str_contains($citaRaw, 'ceo') || str_contains($citaRaw, 'founder') || str_contains($citaRaw, 'pembisnis')) return 'Pengusaha';
|
||||||
|
if (str_contains($citaRaw, 'gizi') || str_contains($citaRaw, 'medis') || str_contains($citaRaw, 'rumah sakit') || str_contains($citaRaw, 'kesehatan') || str_contains($citaRaw, 'dokter') || str_contains($citaRaw, 'perawat')) return 'Ahli Gizi';
|
||||||
|
if (str_contains($citaRaw, 'rantai pasok') || str_contains($citaRaw, 'supply chain') || str_contains($citaRaw, 'manager rantai pasok') || str_contains($citaRaw, 'logistik')) return 'Manager Rantai Pasok';
|
||||||
|
if (str_contains($citaRaw, 'manajer') || str_contains($citaRaw, 'manager') || str_contains($citaRaw, 'pimpinan') || str_contains($citaRaw, 'bos')) return 'Manajer';
|
||||||
|
if (str_contains($citaRaw, 'quality control') || str_contains($citaRaw, 'qc') || str_contains($citaRaw, 'tester')) return 'Quality Control';
|
||||||
|
if (str_contains($citaRaw, 'petani modern') || str_contains($citaRaw, 'tani') || str_contains($citaRaw, 'kebun')) return 'Petani Modern';
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($citaRaw, 'software') || str_contains($citaRaw, 'data engineer') ||
|
||||||
|
str_contains($citaRaw, 'network engineer') || str_contains($citaRaw, 'programmer') ||
|
||||||
|
preg_match('/\bit\b/i', $citaRaw) || str_contains($citaRaw, 'developer') ||
|
||||||
|
str_contains($citaRaw, 'teknisi informatika') || str_contains($citaRaw, 'analis') ||
|
||||||
|
str_contains($citaRaw, 'analyst') || str_contains($citaRaw, 'data') ||
|
||||||
|
str_contains($citaRaw, 'gamer') || str_contains($citaRaw, 'hacker')
|
||||||
|
) return 'IT';
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($citaRaw, 'translator') || str_contains($citaRaw, 'penerjemah') ||
|
||||||
|
str_contains($citaRaw, 'tour') || str_contains($citaRaw, 'tour guide') ||
|
||||||
|
str_contains($citaRaw, 'pemandu') || str_contains($citaRaw, 'wisata') ||
|
||||||
|
str_contains($citaRaw, 'bahasa') || str_contains($citaRaw, 'diplomat') ||
|
||||||
|
str_contains($citaRaw, 'content') || str_contains($citaRaw, 'creator') ||
|
||||||
|
str_contains($citaRaw, 'kreator') || str_contains($citaRaw, 'youtuber') ||
|
||||||
|
str_contains($citaRaw, 'guru') || str_contains($citaRaw, 'dosen') ||
|
||||||
|
str_contains($citaRaw, 'pengajar') || str_contains($citaRaw, 'pendidik') || str_contains($citaRaw, 'penyanyi')
|
||||||
|
) return 'Bahasa & Pariwisata';
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($citaRaw, 'mekanik') || str_contains($citaRaw, 'mesin') ||
|
||||||
|
str_contains($citaRaw, 'teknisi') || str_contains($citaRaw, 'engineer') ||
|
||||||
|
str_contains($citaRaw, 'rekayasa') || str_contains($citaRaw, 'otomotif')
|
||||||
|
) return 'Teknisi';
|
||||||
|
|
||||||
|
return $this->getKelasMayoritas('cita_cita', 'IT');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function kategoriPreferensi($preferensi)
|
||||||
|
{
|
||||||
|
if (empty($preferensi)) {
|
||||||
|
return $this->getKelasMayoritas('preferensi_studi', 'Komputer & Informatika');
|
||||||
|
}
|
||||||
|
|
||||||
|
$preferensiClean = trim($preferensi);
|
||||||
|
|
||||||
|
$kategoriStandar = [
|
||||||
|
'Pertanian & Hortikultura', 'Komputer & Informatika', 'Ekonomi & Agribisnis',
|
||||||
|
'Bahasa & Komunikasi', 'Kesehatan & Gizi', 'Bisnis & Manajemen',
|
||||||
|
'Ilmu Peternakan', 'Rekayasa Otomotif', 'Sains & Teknologi'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($kategoriStandar as $kat) {
|
||||||
|
if (strcasecmp($preferensiClean, $kat) === 0) {
|
||||||
|
return $kat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$preferensiRaw = strtolower($preferensiClean);
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($preferensiRaw, 'komputer') || str_contains($preferensiRaw, 'informatika') ||
|
||||||
|
str_contains($preferensiRaw, 'sistem informasi') || str_contains($preferensiRaw, 'software') ||
|
||||||
|
str_contains($preferensiRaw, 'data') || str_contains($preferensiRaw, 'mining') ||
|
||||||
|
str_contains($preferensiRaw, 'it') || str_contains($preferensiRaw, 'digital') ||
|
||||||
|
str_contains($preferensiRaw, 'aplikasi') || str_contains($preferensiRaw, 'pemrograman') ||
|
||||||
|
str_contains($preferensiRaw, 'matematika')
|
||||||
|
) return 'Komputer & Informatika';
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($preferensiRaw, 'bahasa') || str_contains($preferensiRaw, 'komunikasi') ||
|
||||||
|
str_contains($preferensiRaw, 'pariwisata') || str_contains($preferensiRaw, 'hubungan internasional') ||
|
||||||
|
str_contains($preferensiRaw, 'media') || str_contains($preferensiRaw, 'desain') ||
|
||||||
|
str_contains($preferensiRaw, 'konten') || str_contains($preferensiRaw, 'content') || str_contains($preferensiRaw, 'kesenian')
|
||||||
|
) return 'Bahasa & Komunikasi';
|
||||||
|
|
||||||
|
if (str_contains($preferensiRaw, 'pertanian') || str_contains($preferensiRaw, 'hortikultura') || str_contains($preferensiRaw, 'tanaman') || str_contains($preferensiRaw, 'agronomi')) return 'Pertanian & Hortikultura';
|
||||||
|
if (str_contains($preferensiRaw, 'peternakan') || str_contains($preferensiRaw, 'ternak') || str_contains($preferensiRaw, 'hewan')) return 'Ilmu Peternakan';
|
||||||
|
if (str_contains($preferensiRaw, 'ekonomi') || str_contains($preferensiRaw, 'agribisnis') || str_contains($preferensiRaw, 'perbankan')) return 'Ekonomi & Agribisnis';
|
||||||
|
if (str_contains($preferensiRaw, 'kesehatan') || str_contains($preferensiRaw, 'gizi') || str_contains($preferensiRaw, 'keperawatan') || str_contains($preferensiRaw, 'kedokteran') || str_contains($preferensiRaw, 'biologi') || str_contains($preferensiRaw, 'farmasi')) return 'Kesehatan & Gizi';
|
||||||
|
if (str_contains($preferensiRaw, 'rekayasa') || str_contains($preferensiRaw, 'otomotif') || str_contains($preferensiRaw, 'mesin') || str_contains($preferensiRaw, 'elektro')) return 'Rekayasa Otomotif';
|
||||||
|
if (str_contains($preferensiRaw, 'bisnis') || str_contains($preferensiRaw, 'manajemen') || str_contains($preferensiRaw, 'pemasaran') || str_contains($preferensiRaw, 'penjualan') || str_contains($preferensiRaw, 'jual') || str_contains($preferensiRaw, 'akuntansi') || str_contains($preferensiRaw, 'ilmu manajemen')) return 'Bisnis & Manajemen';
|
||||||
|
if (str_contains($preferensiRaw, 'sains') || str_contains($preferensiRaw, 'teknologi') || str_contains($preferensiRaw, 'ilmu fisika') || str_contains($preferensiRaw, 'fisika') || str_contains($preferensiRaw, 'ilmu kimia') || str_contains($preferensiRaw, 'kimia')) return 'Sains & Teknologi';
|
||||||
|
|
||||||
|
return $this->getKelasMayoritas('preferensi_studi', 'Komputer & Informatika');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function preprocess($row)
|
||||||
|
{
|
||||||
|
$isObject = is_object($row);
|
||||||
|
|
||||||
|
$nilai = $isObject
|
||||||
|
? ($row->nilai_raw ?? $row->nilai ?? 0)
|
||||||
|
: ($row['nilai_raw'] ?? $row['nilai'] ?? 0);
|
||||||
|
|
||||||
|
$mapelFavorit = $isObject
|
||||||
|
? ($row->mapel_favorit_raw ?? $row->mapel_favorit ?? '')
|
||||||
|
: ($row['mapel_favorit_raw'] ?? $row['mapel_favorit'] ?? '');
|
||||||
|
|
||||||
|
$minat = $isObject
|
||||||
|
? ($row->minat_raw ?? $row->minat ?? '')
|
||||||
|
: ($row['minat_raw'] ?? $row['minat'] ?? '');
|
||||||
|
|
||||||
|
$citaCita = $isObject
|
||||||
|
? ($row->cita_cita_raw ?? $row->cita_cita ?? '')
|
||||||
|
: ($row['cita_cita_raw'] ?? $row['cita_cita'] ?? '');
|
||||||
|
|
||||||
|
$preferensiStudi = $isObject
|
||||||
|
? ($row->preferensi_studi_raw ?? $row->preferensi_studi ?? '')
|
||||||
|
: ($row['preferensi_studi_raw'] ?? $row['preferensi_studi'] ?? '');
|
||||||
|
|
||||||
|
$jurusan = $isObject
|
||||||
|
? ($row->jurusan ?? 'Tidak Diketahui')
|
||||||
|
: ($row['jurusan'] ?? 'Tidak Diketahui');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'nilai' => $this->kategoriNilai($nilai),
|
||||||
|
'mapel_favorit' => $this->kategoriMapelFavorit($mapelFavorit),
|
||||||
|
'minat' => $this->kategoriMinat($minat),
|
||||||
|
'cita_cita' => $this->kategoriCitaCita($citaCita),
|
||||||
|
'preferensi_studi' => $this->kategoriPreferensi($preferensiStudi),
|
||||||
|
'jurusan' => trim($jurusan)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AppLayout extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represents the component.
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layouts.app');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class GuestLayout extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represents the component.
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layouts.guest');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
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 {
|
||||||
|
//
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
//
|
||||||
|
})->create();
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
|
||||||
|
return [
|
||||||
|
AppServiceProvider::class,
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
{
|
||||||
|
"$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.2",
|
||||||
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
|
"doctrine/dbal": "^4.4",
|
||||||
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/tinker": "^2.10.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/breeze": "^2.4",
|
||||||
|
"laravel/pail": "^1.2.2",
|
||||||
|
"laravel/pint": "^1.24",
|
||||||
|
"laravel/sail": "^1.41",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"phpunit/phpunit": "^11.5.50"
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"@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
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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' => 'Asia/Jakarta',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 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'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
<?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-'),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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')),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
<?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),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
*.sqlite*
|
||||||
|
|
@ -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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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->integer('expiration')->index();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->string('owner');
|
||||||
|
$table->integer('expiration')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('cache');
|
||||||
|
Schema::dropIfExists('cache_locks');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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->unsignedTinyInteger('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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?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()
|
||||||
|
{
|
||||||
|
Schema::create('data_training', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->double('nilai');
|
||||||
|
|
||||||
|
$table->string('minat');
|
||||||
|
|
||||||
|
$table->string('cita_cita');
|
||||||
|
|
||||||
|
$table->string('preferensi_studi');
|
||||||
|
|
||||||
|
$table->string('jurusan');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('data_training');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('data_training', function (Blueprint $table) {
|
||||||
|
|
||||||
|
$table->string('nilai')->change();
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('data_training', function (Blueprint $table) {
|
||||||
|
|
||||||
|
$table->double('nilai')->change();
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?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('data_testing', function (Blueprint $table) {
|
||||||
|
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->string('nama');
|
||||||
|
|
||||||
|
$table->string('nilai');
|
||||||
|
|
||||||
|
$table->string('minat');
|
||||||
|
|
||||||
|
$table->string('cita_cita');
|
||||||
|
|
||||||
|
$table->string('preferensi_studi');
|
||||||
|
|
||||||
|
$table->string('hasil_rekomendasi')->nullable();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('data_testings');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?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::table('users', function ($table) {
|
||||||
|
|
||||||
|
$table->string('role')->default('Siswa');
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('role');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?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::table('data_training', function (Blueprint $table) {
|
||||||
|
// Menambahkan kolom raw untuk menampung teks bebas dari form
|
||||||
|
// nullable() diberikan agar data lama tidak error saat migrasi berjalan
|
||||||
|
$table->decimal('nilai_raw', 5, 2)->nullable()->after('id');
|
||||||
|
$table->text('minat_raw')->nullable()->after('nilai_raw');
|
||||||
|
$table->text('cita_cita_raw')->nullable()->after('minat_raw');
|
||||||
|
$table->text('preferensi_studi_raw')->nullable()->after('cita_cita_raw');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('data_training', function (Blueprint $table) {
|
||||||
|
// Drop kolom kembali jika migration di-rollback
|
||||||
|
$table->dropColumn(['nilai_raw', 'minat_raw', 'cita_cita_raw', 'preferensi_studi_raw']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('data_testing', function (Blueprint $table) {
|
||||||
|
// Menambahkan kolom simpanan data mentah (RAW)
|
||||||
|
$table->string('nilai_raw')->nullable()->after('nama');
|
||||||
|
$table->string('minat_raw')->nullable()->after('nilai_raw');
|
||||||
|
$table->string('cita_cita_raw')->nullable()->after('minat_raw');
|
||||||
|
$table->string('preferensi_studi_raw')->nullable()->after('cita_cita_raw');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('data_testing', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'nilai_raw',
|
||||||
|
'minat_raw',
|
||||||
|
'cita_cita_raw',
|
||||||
|
'preferensi_studi_raw'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('data_testing', function (Blueprint $table) {
|
||||||
|
// 1. Mengubah kolom 'nilai' menjadi string/varchar (untuk label 'Tinggi', dll)
|
||||||
|
$table->string('nilai')->change();
|
||||||
|
|
||||||
|
// 2. Mengubah kolom 'nilai_raw' menjadi double (untuk angka 88, 85.5, dll)
|
||||||
|
$table->double('nilai_raw')->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('data_testing', function (Blueprint $table) {
|
||||||
|
$table->double('nilai')->change();
|
||||||
|
$table->string('nilai_raw')->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// 1. Lengkapi kolom _raw di data_training
|
||||||
|
Schema::table('data_training', function (Blueprint $table) {
|
||||||
|
if (!Schema::hasColumn('data_training', 'nilai_raw')) {
|
||||||
|
$table->double('nilai_raw')->nullable()->after('jurusan');
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('data_training', 'minat_raw')) {
|
||||||
|
$table->string('minat_raw')->nullable()->after('nilai_raw');
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('data_training', 'cita_cita_raw')) {
|
||||||
|
$table->string('cita_cita_raw')->nullable()->after('minat_raw');
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('data_training', 'preferensi_studi_raw')) {
|
||||||
|
$table->string('preferensi_studi_raw')->nullable()->after('cita_cita_raw');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Perbaiki data_testing:
|
||||||
|
// - Ubah nama kolom 'nilai' yang double menjadi 'nilai_raw'
|
||||||
|
// - Buat kolom 'nilai' baru bertipe VARCHAR
|
||||||
|
// - Tambah kolom _raw lainnya
|
||||||
|
Schema::table('data_testing', function (Blueprint $table) {
|
||||||
|
// Ubah kolom 'nilai' (double) menjadi 'nilai_raw'
|
||||||
|
if (Schema::hasColumn('data_testing', 'nilai') && !Schema::hasColumn('data_testing', 'nilai_raw')) {
|
||||||
|
$table->renameColumn('nilai', 'nilai_raw');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('data_testing', function (Blueprint $table) {
|
||||||
|
// Buat kolom 'nilai' baru bertipe string/varchar untuk label kategori
|
||||||
|
if (!Schema::hasColumn('data_testing', 'nilai')) {
|
||||||
|
$table->string('nilai')->after('nama');
|
||||||
|
}
|
||||||
|
// Tambahkan sisa kolom _raw jika belum ada
|
||||||
|
if (!Schema::hasColumn('data_testing', 'minat_raw')) {
|
||||||
|
$table->string('minat_raw')->nullable()->after('nilai');
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('data_testing', 'cita_cita_raw')) {
|
||||||
|
$table->string('cita_cita_raw')->nullable()->after('minat_raw');
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('data_testing', 'preferensi_studi_raw')) {
|
||||||
|
$table->string('preferensi_studi_raw')->nullable()->after('cita_cita_raw');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Dikosongkan agar aman
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
// Menambahkan kolom role dengan default 'siswa' setelah kolom email
|
||||||
|
$table->string('role')->default('siswa')->after('email');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('role');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?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::table('data_testing', function (Blueprint $table) {
|
||||||
|
// Menambahkan user_id (Siswa pemilik data) setelah kolom id
|
||||||
|
$table->foreignId('user_id')
|
||||||
|
->nullable()
|
||||||
|
->after('id')
|
||||||
|
->constrained('users')
|
||||||
|
->nullOnDelete();
|
||||||
|
|
||||||
|
// Menambahkan created_by (Penginput data: Admin / Siswa) setelah user_id
|
||||||
|
$table->foreignId('created_by')
|
||||||
|
->nullable()
|
||||||
|
->after('user_id')
|
||||||
|
->constrained('users')
|
||||||
|
->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('data_testing', function (Blueprint $table) {
|
||||||
|
// Hapus foreign key dan kolom jika migration di-rollback
|
||||||
|
$table->dropForeign(['user_id']);
|
||||||
|
$table->dropForeign(['created_by']);
|
||||||
|
$table->dropColumn(['user_id', 'created_by']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('data_training', function (Blueprint $table) {
|
||||||
|
// Menambahkan kolom baru setelah 'nilai'
|
||||||
|
$table->string('mapel_favorit')->nullable()->after('nilai');
|
||||||
|
$table->string('mapel_favorit_raw')->nullable()->after('nilai_raw');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('data_training', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['mapel_favorit', 'mapel_favorit_raw']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?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::table('data_testing', function (Blueprint $table) {
|
||||||
|
$table->string('mapel_favorit_raw')->nullable()->after('nilai_raw');
|
||||||
|
$table->string('mapel_favorit')->nullable()->after('nilai');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('data_testing', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['mapel_favorit_raw', 'mapel_favorit']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class DatabaseSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed the application's database.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// 1. Akun Admin
|
||||||
|
User::updateOrCreate(
|
||||||
|
['email' => 'admin@gmail.com'],
|
||||||
|
[
|
||||||
|
'name' => 'Administrator',
|
||||||
|
'password' => Hash::make('admin12345'),
|
||||||
|
'role' => 'admin',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Akun Siswa
|
||||||
|
User::updateOrCreate(
|
||||||
|
['email' => 'brendha@gmail.com'],
|
||||||
|
[
|
||||||
|
'name' => 'brendha',
|
||||||
|
'password' => Hash::make('brendha12345'),
|
||||||
|
'role' => 'siswa',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://www.schemastore.org/package.json",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"dev": "vite"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"alpinejs": "^3.4.2",
|
||||||
|
"autoprefixer": "^10.4.2",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"concurrently": "^9.0.1",
|
||||||
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
|
"postcss": "^8.4.31",
|
||||||
|
"tailwindcss": "^3.1.0",
|
||||||
|
"vite": "^7.0.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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());
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import './bootstrap';
|
||||||
|
|
||||||
|
import Alpine from 'alpinejs';
|
||||||
|
|
||||||
|
window.Alpine = Alpine;
|
||||||
|
|
||||||
|
Alpine.start();
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
window.axios = axios;
|
||||||
|
|
||||||
|
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Dashboard Admin')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid p-0">
|
||||||
|
|
||||||
|
<!-- Hero Banner Modern dengan Animasi Halus -->
|
||||||
|
<div class="admin-hero-banner mb-4 d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-white text-primary fw-semibold px-3 py-2 rounded-pill mb-2 shadow-sm">
|
||||||
|
<i class="fas fa-user-shield me-1"></i> Panel Guru
|
||||||
|
</span>
|
||||||
|
<h2 class="fw-bold mb-1">Selamat Datang, Bapak/Ibu Guru! 👋</h2>
|
||||||
|
<p class="mb-0 opacity-90">Pantau performa sistem rekomendasi dan statistik minat jurusan siswa secara real-time.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-none d-lg-block opacity-75 me-3">
|
||||||
|
<i class="fas fa-chart-pie fa-5x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<!-- Total Data Training -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card card-stat shadow-sm p-3">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<div>
|
||||||
|
<span class="text-muted small fw-semibold text-uppercase">Dataset Training</span>
|
||||||
|
<h2 class="fw-bold text-dark mt-1 mb-0">{{ $totalTraining }}</h2>
|
||||||
|
<span class="badge bg-primary-subtle text-primary mt-2 rounded-pill">
|
||||||
|
<i class="fas fa-check-circle me-1"></i>Siap Digunakan
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="icon-box bg-primary-subtle text-primary">
|
||||||
|
<i class="fas fa-database"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Total Siswa -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card card-stat shadow-sm p-3">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<div>
|
||||||
|
<span class="text-muted small fw-semibold text-uppercase">Siswa Terdaftar</span>
|
||||||
|
<h2 class="fw-bold text-dark mt-1 mb-0">{{ $totalSiswa }}</h2>
|
||||||
|
<span class="badge bg-success-subtle text-success mt-2 rounded-pill">
|
||||||
|
<i class="fas fa-users me-1"></i>Pengguna Aktif
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="icon-box bg-success-subtle text-success">
|
||||||
|
<i class="fas fa-user-graduate"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Total Hasil Konsultasi -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card card-stat shadow-sm p-3">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<div>
|
||||||
|
<span class="text-muted small fw-semibold text-uppercase">Total Konsultasi</span>
|
||||||
|
<h2 class="fw-bold text-dark mt-1 mb-0">{{ $totalHasil }}</h2>
|
||||||
|
<span class="badge bg-purple-subtle text-purple mt-2 rounded-pill" style="background-color: #f3e8ff; color: #7e22ce;">
|
||||||
|
<i class="fas fa-chart-line me-1"></i>Rekomendasi Selesai
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="icon-box" style="background-color: #f3e8ff; color: #7e22ce;">
|
||||||
|
<i class="fas fa-poll"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section Visualisasi Chart & Aktivitas -->
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<!-- Chart Grafik -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 p-4 h-100">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div>
|
||||||
|
<h5 class="fw-bold mb-0">Visualisasi Ringkasan Sistem</h5>
|
||||||
|
<small class="text-muted">Perbandingan data siswa dan hasil konsultasi C4.5</small>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-light text-dark border">
|
||||||
|
<i class="fas fa-calendar-alt me-1"></i> Overview
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="height: 260px; position: relative;">
|
||||||
|
<canvas id="adminOverviewChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Panel Informasi / Status Ringkasan -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 p-4 h-100">
|
||||||
|
<h5 class="fw-bold mb-3"><i class="fas fa-info-circle text-indigo me-2"></i>Status Algoritma</h5>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-content-between small fw-semibold mb-1">
|
||||||
|
<span>Akurasi Model Data</span>
|
||||||
|
<span class="text-success">Siap Klasifikasi</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 8px;">
|
||||||
|
<div class="progress-bar bg-success rounded" role="progressbar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 bg-light rounded-3 mb-3 border">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<i class="fas fa-robot fs-3 text-primary"></i>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold small">Metode Klasifikasi</div>
|
||||||
|
<div class="text-muted small">Pohon Keputusan (C4.5) berbasis Gain Ratio</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 bg-light rounded-3 border">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<i class="fas fa-file-export fs-3 text-success"></i>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold small">Laporan Rekapitulasi</div>
|
||||||
|
<div class="text-muted small">Tersedia fitur pencetakan laporan untuk siswa</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Action / Menu Pintas (Termasuk Cek Rekomendasi C4.5) -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 p-4">
|
||||||
|
<h5 class="fw-bold mb-3"><i class="fas fa-bolt text-warning me-2"></i>Akses Cepat Admin</h5>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="{{ route('training.create') }}" class="btn btn-quick-action w-100 p-3 text-start d-flex align-items-center gap-3 text-decoration-none">
|
||||||
|
<div class="icon-box bg-primary-subtle text-primary flex-shrink-0">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong class="text-dark d-block">Tambah Training</strong>
|
||||||
|
<span class="small text-muted">Input data training baru</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="{{ route('c45.index') }}" class="btn btn-quick-action w-100 p-3 text-start d-flex align-items-center gap-3 text-decoration-none">
|
||||||
|
<div class="icon-box bg-warning-subtle text-warning flex-shrink-0">
|
||||||
|
<i class="fas fa-microchip"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong class="text-dark d-block">Cek Rekomendasi</strong>
|
||||||
|
<span class="small text-muted">Simulasi Hitung C4.5</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="{{ route('admin.rekap') }}" class="btn btn-quick-action w-100 p-3 text-start d-flex align-items-center gap-3 text-decoration-none">
|
||||||
|
<div class="icon-box bg-success-subtle text-success flex-shrink-0">
|
||||||
|
<i class="fas fa-file-pdf"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong class="text-dark d-block">Rekap Laporan</strong>
|
||||||
|
<span class="small text-muted">Cetak & Export PDF</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="{{ route('training.index') }}" class="btn btn-quick-action w-100 p-3 text-start d-flex align-items-center gap-3 text-decoration-none">
|
||||||
|
<div class="icon-box bg-dark-subtle text-dark flex-shrink-0">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong class="text-dark d-block">Kelola Dataset</strong>
|
||||||
|
<span class="small text-muted">Lihat & edit dataset</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const ctx = document.getElementById('adminOverviewChart').getContext('2d');
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['Dataset Training', 'Siswa Terdaftar', 'Hasil Konsultasi'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah Data',
|
||||||
|
data: [{{ $totalTraining }}, {{ $totalSiswa }}, {{ $totalHasil }}],
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(79, 70, 229, 0.85)',
|
||||||
|
'rgba(16, 185, 129, 0.85)',
|
||||||
|
'rgba(126, 34, 206, 0.85)'
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'#4f46e5',
|
||||||
|
'#10b981',
|
||||||
|
'#7e22ce'
|
||||||
|
],
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderRadius: 10,
|
||||||
|
barThickness: 45
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
color: '#f1f5f9'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
precision: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Laporan Rekapitulasi Hasil Konsultasi Siswa</title>
|
||||||
|
<style>
|
||||||
|
/* Setup Ukuran Halaman Landscape via CSS Dompdf */
|
||||||
|
@page {
|
||||||
|
size: A4 landscape;
|
||||||
|
margin: 1.5cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kop-surat {
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 3px double #000;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.kop-surat h2 { margin: 0; font-size: 15px; text-transform: uppercase; }
|
||||||
|
.kop-surat p { margin: 3px 0 0 0; font-size: 10px; color: #555; }
|
||||||
|
|
||||||
|
.meta-info { margin-bottom: 12px; width: 100%; border-collapse: collapse; }
|
||||||
|
.meta-info td { font-size: 10px; vertical-align: top; padding: 2px 0; }
|
||||||
|
|
||||||
|
table.data-table { width: 100%; border-collapse: collapse; margin-top: 5px; }
|
||||||
|
table.data-table th, table.data-table td {
|
||||||
|
border: 1px solid #333;
|
||||||
|
padding: 5px 6px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
table.data-table th {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 9.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.fw-bold { font-weight: bold; }
|
||||||
|
.badge-jurusan { font-weight: bold; color: #166534; background-color: #f0fdf4; }
|
||||||
|
|
||||||
|
.ttd-container {
|
||||||
|
margin-top: 25px;
|
||||||
|
float: right;
|
||||||
|
width: 220px;
|
||||||
|
text-align: center;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
.ttd-space { height: 50px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Kop Laporan Resmi -->
|
||||||
|
<div class="kop-surat">
|
||||||
|
<h2>REKAPITULASI HASIL REKOMENDASI SISWA</h2>
|
||||||
|
<p>Sistem Rekomendasi Pemilihan Jurusan Berbasis Algoritma C4.5</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informasi Dokumen -->
|
||||||
|
<table class="meta-info">
|
||||||
|
<tr>
|
||||||
|
<td width="12%"><strong>Tanggal Cetak</strong></td>
|
||||||
|
<td width="1%">:</td>
|
||||||
|
<td width="37%">{{ $tanggalCetak }}</td>
|
||||||
|
<td width="15%"><strong>Total Konsultasi</strong></td>
|
||||||
|
<td width="1%">:</td>
|
||||||
|
<td width="34%">{{ $totalKonsultasi }} Siswa</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Metode</strong></td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>Decision Tree (Algoritma C4.5)</td>
|
||||||
|
<td><strong>Status Laporan</strong></td>
|
||||||
|
<td>:</td>
|
||||||
|
<td>Laporan Rekapitulasi Resmi</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Tabel Data Rekapitulasi Lengkap Atribut C4.5 -->
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="4%">No</th>
|
||||||
|
<th width="15%">Nama Siswa</th>
|
||||||
|
<th width="10%">Nilai</th>
|
||||||
|
<th width="14%">Minat Utama</th>
|
||||||
|
<th width="12%">Cita-Cita</th>
|
||||||
|
<th width="16%">Preferensi Studi</th>
|
||||||
|
<th width="16%">Hasil Rekomendasi</th>
|
||||||
|
<th width="13%">Waktu Konsultasi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($data as $index => $row)
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: center;">{{ $index + 1 }}</td>
|
||||||
|
<td class="fw-bold">{{ $row->nama }}</td>
|
||||||
|
<td style="text-align: center;">{{ $row->nilai_raw }}</td>
|
||||||
|
<td>{{ $row->minat_raw }}</td>
|
||||||
|
<td>{{ $row->cita_cita_raw }}</td>
|
||||||
|
<td>{{ $row->preferensi_studi_raw }}</td>
|
||||||
|
<td class="text-center badge-jurusan">{{ $row->hasil_rekomendasi }}</td>
|
||||||
|
<td class="text-center">{{ \Carbon\Carbon::parse($row->created_at)->format('d/m/Y H:i') }} WIB</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center">Belum ada data konsultasi.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Blok Tanda Tangan BK / Admin -->
|
||||||
|
<div class="ttd-container">
|
||||||
|
<p>Mengetahui,<br><strong>Guru Bimbingan Konseling</strong></p>
|
||||||
|
<div class="ttd-space"></div>
|
||||||
|
<p>( ........................................... )<br>NIP. ......................................</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Pengaturan Profil Admin')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Header Halaman -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h4 class="fw-bold mb-1"><i class="fas fa-id-card text-primary me-2"></i>Pengaturan Profil</h4>
|
||||||
|
<p class="text-muted small mb-0">Kelola informasi akun dan kata sandi Anda.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('admin.dashboard') }}" class="btn btn-outline-secondary rounded-3 px-3">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i> Kembali ke Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (session('status') === 'profile-updated')
|
||||||
|
<div class="alert alert-success alert-dismissible fade show rounded-3 border-0 shadow-sm mb-4" role="alert">
|
||||||
|
<i class="fas fa-check-circle me-1"></i> Profil berhasil diperbarui!
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (session('status') === 'password-updated')
|
||||||
|
<div class="alert alert-success alert-dismissible fade show rounded-3 border-0 shadow-sm mb-4" role="alert">
|
||||||
|
<i class="fas fa-check-circle me-1"></i> Kata sandi berhasil diubah!
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Kolom Kiri: Form Ubah Nama & Email -->
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h5 class="fw-bold mb-1"><i class="fas fa-user-edit text-primary me-2"></i>Informasi Akun</h5>
|
||||||
|
<p class="text-muted small mb-4">Ubah nama dan email Anda di sini.</p>
|
||||||
|
|
||||||
|
<form action="{{ route('profile.update') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PATCH')
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nama Lengkap</label>
|
||||||
|
<input type="text" name="name" class="form-control rounded-3 @error('name') is-invalid @enderror" value="{{ old('name', auth()->user()->name) }}" required>
|
||||||
|
@error('name')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-semibold">Alamat Email</label>
|
||||||
|
<input type="email" name="email" class="form-control rounded-3 @error('email') is-invalid @enderror" value="{{ old('email', auth()->user()->email) }}" required>
|
||||||
|
@error('email')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-end">
|
||||||
|
<button type="submit" class="btn btn-primary rounded-3 px-4">
|
||||||
|
<i class="fas fa-save me-1"></i> Simpan Profil
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Ganti Password -->
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h5 class="fw-bold mb-1"><i class="fas fa-key text-warning me-2"></i>Ganti Kata Sandi</h5>
|
||||||
|
<p class="text-muted small mb-4">Gunakan kata sandi yang aman demi keamanan akun.</p>
|
||||||
|
|
||||||
|
<form action="{{ route('profile.password.update') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<!-- Kata Sandi Saat Ini -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Kata Sandi Saat Ini</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" name="current_password" id="current_password" class="form-control rounded-start-3 @error('current_password') is-invalid @enderror" required>
|
||||||
|
<button class="btn btn-outline-secondary rounded-end-3" type="button" onclick="togglePasswordVisibility('current_password', 'icon_current_password')">
|
||||||
|
<i class="fas fa-eye" id="icon_current_password"></i>
|
||||||
|
</button>
|
||||||
|
@error('current_password')
|
||||||
|
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kata Sandi Baru -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label fw-semibold">Kata Sandi Baru</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" name="password" id="new_password" class="form-control rounded-start-3 @error('password') is-invalid @enderror" onkeyup="checkPasswordStrength(this.value)" required>
|
||||||
|
<button class="btn btn-outline-secondary rounded-end-3" type="button" onclick="togglePasswordVisibility('new_password', 'icon_new_password')">
|
||||||
|
<i class="fas fa-eye" id="icon_new_password"></i>
|
||||||
|
</button>
|
||||||
|
@error('password')
|
||||||
|
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Checklist Syarat Kata Sandi Kuat -->
|
||||||
|
<div class="p-3 bg-light rounded-3 mb-3 border">
|
||||||
|
<p class="fw-semibold small text-muted mb-2">Syarat Kata Sandi Kuat:</p>
|
||||||
|
<div class="row g-2 small">
|
||||||
|
<div class="col-6" id="req-length"><i class="fas fa-times-circle text-danger me-1"></i> Minimal 8 Karakter</div>
|
||||||
|
<div class="col-6" id="req-uppercase"><i class="fas fa-times-circle text-danger me-1"></i> Huruf Besar (A-Z)</div>
|
||||||
|
<div class="col-6" id="req-number"><i class="fas fa-times-circle text-danger me-1"></i> Mengandung Angka (0-9)</div>
|
||||||
|
<div class="col-6" id="req-symbol"><i class="fas fa-times-circle text-danger me-1"></i> Simbol (@$!%*?&)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Konfirmasi Kata Sandi Baru -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-semibold">Konfirmasi Kata Sandi Baru</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" name="password_confirmation" id="password_confirmation" class="form-control rounded-start-3" required>
|
||||||
|
<button class="btn btn-outline-secondary rounded-end-3" type="button" onclick="togglePasswordVisibility('password_confirmation', 'icon_password_confirmation')">
|
||||||
|
<i class="fas fa-eye" id="icon_password_confirmation"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-end">
|
||||||
|
<button type="submit" class="btn btn-warning text-white rounded-3 px-4">
|
||||||
|
<i class="fas fa-lock me-1"></i> Ubah Kata Sandi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kolom Kanan: Card Ringkasan Profil -->
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 text-center p-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="bg-primary-subtle text-primary rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
|
||||||
|
<i class="fas fa-user-circle fs-1"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="fw-bold mb-1">{{ auth()->user()->name }}</h5>
|
||||||
|
<p class="text-muted small mb-3">{{ auth()->user()->email }}</p>
|
||||||
|
|
||||||
|
<span class="badge bg-primary px-3 py-2 rounded-pill fs-6 fw-semibold mb-4">
|
||||||
|
<i class="fas fa-shield-alt me-1"></i> ADMIN / GURU BK
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<hr class="my-3">
|
||||||
|
|
||||||
|
<div class="text-start text-muted small">
|
||||||
|
<p class="mb-1"><i class="fas fa-calendar-alt me-2 text-primary"></i> Terdaftar Pada:</p>
|
||||||
|
<p class="fw-semibold text-dark mb-0">{{ \Carbon\Carbon::parse(auth()->user()->created_at)->translatedFormat('d F Y') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Script Toggle Password & Checklist Realtime -->
|
||||||
|
<script>
|
||||||
|
function togglePasswordVisibility(inputId, iconId) {
|
||||||
|
const inputField = document.getElementById(inputId);
|
||||||
|
const icon = document.getElementById(iconId);
|
||||||
|
|
||||||
|
if (inputField.type === "password") {
|
||||||
|
inputField.type = "text";
|
||||||
|
icon.classList.remove("fa-eye");
|
||||||
|
icon.classList.add("fa-eye-slash");
|
||||||
|
} else {
|
||||||
|
inputField.type = "password";
|
||||||
|
icon.classList.remove("fa-eye-slash");
|
||||||
|
icon.classList.add("fa-eye");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPasswordStrength(password) {
|
||||||
|
const rules = [
|
||||||
|
{ id: 'req-length', valid: password.length >= 8 },
|
||||||
|
{ id: 'req-uppercase', valid: /[A-Z]/.test(password) },
|
||||||
|
{ id: 'req-number', valid: /[0-9]/.test(password) },
|
||||||
|
{ id: 'req-symbol', valid: /[@$!%*?&]/.test(password) }
|
||||||
|
];
|
||||||
|
|
||||||
|
rules.forEach(rule => {
|
||||||
|
const element = document.getElementById(rule.id);
|
||||||
|
if (rule.valid) {
|
||||||
|
element.className = "col-6 text-success fw-semibold";
|
||||||
|
element.querySelector('i').className = "fas fa-check-circle text-success me-1";
|
||||||
|
} else {
|
||||||
|
element.className = "col-6 text-muted";
|
||||||
|
element.querySelector('i').className = "fas fa-times-circle text-danger me-1";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,478 @@
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Rekapitulasi Konsultasi Siswa')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<!-- Custom Styling Tambahan -->
|
||||||
|
<style>
|
||||||
|
/* Stats Card Gradient */
|
||||||
|
.card-stat-primary {
|
||||||
|
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-stat-dark {
|
||||||
|
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover Interactive Card */
|
||||||
|
.interactive-card {
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
.interactive-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Rank Badge */
|
||||||
|
.rank-badge {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Avatar Initial */
|
||||||
|
.avatar-circle {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background-color: #e0f2fe;
|
||||||
|
color: #0369a1;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Media Print */
|
||||||
|
@media print {
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container-fluid px-3 py-2">
|
||||||
|
|
||||||
|
<!-- Header & Action -->
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center mb-4 pb-3 border-bottom gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<span class="badge bg-primary-subtle text-primary border border-primary-subtle rounded-pill px-3 py-1 fs-7 fw-semibold">
|
||||||
|
<i class="fas fa-chart-line me-1"></i> Executive Summary
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="fw-bold text-dark m-0 d-flex align-items-center gap-2 fs-3">
|
||||||
|
<i class="fas fa-file-invoice text-primary"></i> Rekapitulasi Konsultasi Siswa
|
||||||
|
</h2>
|
||||||
|
<p class="text-muted small mb-0 mt-1">Ringkasan laporan statistik dan grafik distribusi hasil rekomendasi jurusan berbasis C4.5</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ringkasan Statistik Cards -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<!-- Total Konsultasi -->
|
||||||
|
<div class="col-md-6 col-xl-4">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 card-stat-primary overflow-hidden position-relative h-100 interactive-card">
|
||||||
|
<!-- Ikon Watermark Transparan di Pojok Kanan Bawah -->
|
||||||
|
<div class="position-absolute" style="right: -10px; bottom: -15px; font-size: 5.5rem; opacity: 0.12; color: #ffffff; pointer-events: none; z-index: 0;">
|
||||||
|
<i class="fas fa-graduation-cap"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body p-4 position-relative" style="z-index: 1;">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<span class="text-uppercase fw-bold text-white opacity-75 small d-flex align-items-center gap-2" style="letter-spacing: 0.5px; font-size: 11px;">
|
||||||
|
<i class="fas fa-users"></i> Total Konsultasi
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-black bg-opacity-25 text-white rounded-pill px-3 py-1.5 small border border-white border-opacity-20" style="font-size: 10px;">
|
||||||
|
Akumulasi
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-baseline gap-2 mb-2">
|
||||||
|
<h1 class="display-5 fw-bold mb-0 text-white font-monospace">
|
||||||
|
{{ number_format($totalKonsultasi) }}
|
||||||
|
</h1>
|
||||||
|
<span class="fs-6 text-white opacity-75">Siswa</span>
|
||||||
|
</div>
|
||||||
|
<div class="pt-2">
|
||||||
|
<small class="text-white opacity-75 d-block" style="font-size: 11.5px; line-height: 1.3;">
|
||||||
|
<i class="fas fa-check-circle me-1"></i>Siswa yang telah menyelesaikan sesi tes
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Total Opsi Jurusan Terrekomendasi -->
|
||||||
|
<div class="col-md-6 col-xl-4">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 card-stat-dark overflow-hidden position-relative h-100 interactive-card">
|
||||||
|
<!-- Ikon Watermark Transparan di Pojok Kanan Bawah -->
|
||||||
|
<div class="position-absolute" style="right: -10px; bottom: -15px; font-size: 5.5rem; opacity: 0.12; color: #ffffff; pointer-events: none; z-index: 0;">
|
||||||
|
<i class="fas fa-layer-group"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body p-4 position-relative" style="z-index: 1;">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<span class="text-uppercase fw-bold text-white opacity-75 small d-flex align-items-center gap-2" style="letter-spacing: 0.5px; font-size: 11px;">
|
||||||
|
<i class="fas fa-layer-group text-warning"></i> Sebaran Jurusan
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-white bg-opacity-10 text-white rounded-pill px-3 py-1.5 small border border-white border-opacity-20" style="font-size: 10px;">
|
||||||
|
Kategori
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-baseline gap-2 mb-2">
|
||||||
|
<h1 class="display-5 fw-bold mb-0 text-white font-monospace">
|
||||||
|
{{ count($rekapJurusan) }}
|
||||||
|
</h1>
|
||||||
|
<span class="fs-6 text-white opacity-75">Jurusan</span>
|
||||||
|
</div>
|
||||||
|
<div class="pt-2">
|
||||||
|
<small class="text-white opacity-75 d-block" style="font-size: 11.5px; line-height: 1.3;">
|
||||||
|
<i class="fas fa-info-circle me-1"></i>Variasi jurusan yang muncul pada hasil sistem
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rata-rata per Jurusan -->
|
||||||
|
<div class="col-md-12 col-xl-4">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 bg-white overflow-hidden position-relative h-100 interactive-card">
|
||||||
|
<div class="card-body p-4 position-relative" style="z-index: 1;">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<span class="text-uppercase fw-bold text-muted small d-flex align-items-center gap-2" style="letter-spacing: 0.5px; font-size: 11px;">
|
||||||
|
<i class="fas fa-calculator text-primary"></i> Rata-rata / Jurusan
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary-subtle text-primary rounded-pill px-3 py-1.5 small" style="font-size: 10px;">
|
||||||
|
Metrik
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-baseline gap-2 mb-2">
|
||||||
|
<h1 class="display-5 fw-bold mb-0 text-dark font-monospace">
|
||||||
|
{{ count($rekapJurusan) > 0 ? number_format($totalKonsultasi / count($rekapJurusan), 1) : 0 }}
|
||||||
|
</h1>
|
||||||
|
<span class="fs-6 text-muted">Siswa/Jurusan</span>
|
||||||
|
</div>
|
||||||
|
<div class="pt-2">
|
||||||
|
<small class="text-muted d-block" style="font-size: 11.5px; line-height: 1.3;">
|
||||||
|
<i class="fas fa-chart-pie me-1 text-primary"></i>Distribusi rata-rata minat per program studi
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grafik & Peringkat -->
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<!-- Visualisasi Grafik Chart.js -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 h-100 overflow-hidden">
|
||||||
|
<div class="card-header bg-white py-3 px-4 border-bottom d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="fw-bold text-dark mb-0 d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-chart-bar text-primary"></i> Visualisasi Distribusi Rekomendasi
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-light text-secondary border rounded-pill px-3 py-1 small">Bar Chart</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4 d-flex align-items-center">
|
||||||
|
<div class="w-100" style="position: relative; min-height: 280px;">
|
||||||
|
<canvas id="chartJurusan"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabel Peringkat Jurusan Terfavorit -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 h-100 overflow-hidden">
|
||||||
|
<div class="card-header bg-white py-3 px-4 border-bottom d-flex align-items-center justify-content-between">
|
||||||
|
<h6 class="fw-bold text-dark mb-0 d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-trophy text-warning"></i> Peringkat Jurusan
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle rounded-pill px-3 py-1 small">Top Dominan</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
@forelse($rekapJurusan as $idx => $item)
|
||||||
|
<div class="list-group-item d-flex align-items-center justify-content-between p-3 border-bottom border-light">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<span class="rank-badge {{ $idx == 0 ? 'bg-warning text-dark' : ($idx == 1 ? 'bg-secondary text-white' : ($idx == 2 ? 'bg-danger-subtle text-danger' : 'bg-light text-muted border')) }}">
|
||||||
|
{{ $idx + 1 }}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold text-dark small">{{ $item->hasil_rekomendasi }}</div>
|
||||||
|
<div class="text-muted" style="font-size: 11px;">
|
||||||
|
{{ $totalKonsultasi > 0 ? number_format(($item->total / $totalKonsultasi) * 100, 1) : 0 }}% dari total
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-primary-subtle text-primary border border-primary-subtle rounded-pill px-3 py-1.5 font-monospace fs-7">
|
||||||
|
{{ $item->total }} Siswa
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="fas fa-inbox fs-2 mb-2 opacity-30"></i>
|
||||||
|
<p class="mb-0 small">Belum ada data konsultasi.</p>
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Tabel Riwayat Terbaru -->
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 overflow-hidden mb-4">
|
||||||
|
<div class="card-header bg-white py-3 px-4 border-bottom d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="fw-bold text-dark mb-0 d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-history text-primary"></i> Riwayat 10 Konsultasi Terbaru
|
||||||
|
</h6>
|
||||||
|
<!-- Diganti dari Live Data menjadi Tombol Export PDF -->
|
||||||
|
<a href="{{ route('admin.rekap.pdf') }}" class="btn btn-danger btn-sm rounded-pill px-3 py-1-5 d-inline-flex align-items-center gap-2 shadow-sm">
|
||||||
|
<i class="fas fa-file-pdf"></i> Export PDF Laporan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="bg-light-subtle text-muted small text-uppercase" style="font-size: 11px;">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4 py-3 text-center" style="width: 60px;">No</th>
|
||||||
|
<th class="py-3">Nama Siswa</th>
|
||||||
|
<th class="py-3">Waktu Konsultasi</th>
|
||||||
|
<th class="text-center py-3">Hasil Rekomendasi Jurusan</th>
|
||||||
|
<th class="text-center py-3 pe-4" style="width: 130px;">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="border-top-0">
|
||||||
|
@forelse($riwayatTerbaru as $idx => $row)
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 text-center font-monospace text-muted small">{{ $idx + 1 }}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span class="fw-bold text-dark">{{ $row->nama }}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-muted small">
|
||||||
|
<i class="far fa-clock me-1 opacity-75"></i>
|
||||||
|
{{ \Carbon\Carbon::parse($row->created_at)->format('d M Y, H:i') }} WIB
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge bg-success-subtle text-success border border-success-subtle px-3 py-2 rounded-pill fw-bold">
|
||||||
|
<i class="fas fa-check-circle me-1"></i>{{ $row->hasil_rekomendasi }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center pe-4">
|
||||||
|
<!-- Tombol Detail (Ikon & Teks Berdampingan Horizontal) -->
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-primary rounded-pill px-3 py-1 d-inline-flex align-items-center gap-1 btn-detail-konsultasi"
|
||||||
|
data-nama="{{ $row->nama }}"
|
||||||
|
data-waktu="{{ \Carbon\Carbon::parse($row->created_at)->format('d M Y, H:i') }} WIB"
|
||||||
|
data-hasil="{{ $row->hasil_rekomendasi }}"
|
||||||
|
data-nilai="{{ $row->nilai_raw }} ({{ $row->nilai }})"
|
||||||
|
data-minat="{{ $row->minat_raw }} ({{ $row->minat }})"
|
||||||
|
data-citacita="{{ $row->cita_cita_raw }} ({{ $row->cita_cita }})"
|
||||||
|
data-preferensi="{{ $row->preferensi_studi_raw }} ({{ $row->preferensi_studi }})">
|
||||||
|
<i class="fas fa-eye"></i> Detail
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center text-muted py-5">
|
||||||
|
<i class="fas fa-folder-open fs-2 mb-2 opacity-30"></i>
|
||||||
|
<p class="mb-0 small">Belum ada riwayat data konsultasi.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart.js Engine -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const ctx = document.getElementById('chartJurusan').getContext('2d');
|
||||||
|
|
||||||
|
// Custom Gradient Fill
|
||||||
|
const gradient = ctx.createLinearGradient(0, 0, 0, 300);
|
||||||
|
gradient.addColorStop(0, 'rgba(37, 99, 235, 0.85)');
|
||||||
|
gradient.addColorStop(1, 'rgba(59, 130, 246, 0.15)');
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: {!! json_encode($labels) !!},
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah Siswa',
|
||||||
|
data: {!! json_encode($totals) !!},
|
||||||
|
backgroundColor: gradient,
|
||||||
|
borderColor: '#2563eb',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderSkipped: false,
|
||||||
|
barThickness: 32,
|
||||||
|
maxBarThickness: 48
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: '#0f172a',
|
||||||
|
titleFont: { size: 13, weight: 'bold' },
|
||||||
|
bodyFont: { size: 12 },
|
||||||
|
padding: 12,
|
||||||
|
cornerRadius: 8,
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return ` Total Siswa: ${context.parsed.y} Orang`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
font: { family: "'Inter', sans-serif", size: 11, weight: '600' },
|
||||||
|
color: '#64748b'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
color: '#f1f5f9',
|
||||||
|
drawBorder: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
stepSize: 1,
|
||||||
|
font: { family: "'Inter', sans-serif", size: 11 },
|
||||||
|
color: '#64748b'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const detailButtons = document.querySelectorAll('.btn-detail-konsultasi');
|
||||||
|
const modalElement = new bootstrap.Modal(document.getElementById('modalDetailKonsultasi'));
|
||||||
|
|
||||||
|
detailButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
// Ambil data atribut dari tombol
|
||||||
|
const nama = this.getAttribute('data-nama');
|
||||||
|
const waktu = this.getAttribute('data-waktu');
|
||||||
|
const hasil = this.getAttribute('data-hasil');
|
||||||
|
const nilai = this.getAttribute('data-nilai');
|
||||||
|
const minat = this.getAttribute('data-minat');
|
||||||
|
const citacita = this.getAttribute('data-citacita');
|
||||||
|
const preferensi = this.getAttribute('data-preferensi');
|
||||||
|
|
||||||
|
// Masukkan data ke dalam elemen modal
|
||||||
|
document.getElementById('detailNamaSiswa').innerText = nama ?? '-';
|
||||||
|
document.getElementById('detailWaktuKonsultasi').innerText = waktu ?? '-';
|
||||||
|
document.getElementById('detailHasilRekomendasi').innerText = hasil ?? '-';
|
||||||
|
|
||||||
|
document.getElementById('detailNilai').innerText = nilai ?? '-';
|
||||||
|
document.getElementById('detailMinat').innerText = minat ?? '-';
|
||||||
|
document.getElementById('detailCitaCita').innerText = citacita ?? '-';
|
||||||
|
document.getElementById('detailPreferensi').innerText = preferensi ?? '-';
|
||||||
|
|
||||||
|
// Tampilkan Modal
|
||||||
|
modalElement.show();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
<!-- Modal Detail Konsultasi -->
|
||||||
|
<div class="modal fade" id="modalDetailKonsultasi" tabindex="-1" aria-labelledby="modalDetailLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
|
<div class="modal-content border-0 shadow rounded-4 overflow-hidden">
|
||||||
|
<div class="modal-header bg-primary text-white py-3 px-4">
|
||||||
|
<h5 class="modal-title fw-bold fs-6 d-flex align-items-center gap-2" id="modalDetailLabel">
|
||||||
|
<i class="fas fa-file-alt"></i> Rincian Input Data Konsultasi Siswa
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-4">
|
||||||
|
<!-- Header Informasi Siswa -->
|
||||||
|
<div class="p-3 bg-light rounded-3 mb-4 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<div>
|
||||||
|
<span class="text-muted small d-block">Nama Siswa</span>
|
||||||
|
<h6 class="fw-bold text-dark mb-0 fs-5" id="detailNamaSiswa">-</h6>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-muted small d-block">Waktu Konsultasi</span>
|
||||||
|
<span class="fw-semibold text-dark small" id="detailWaktuKonsultasi">-</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-muted small d-block">Hasil Rekomendasi C4.5</span>
|
||||||
|
<span class="badge bg-success fs-6" id="detailHasilRekomendasi">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="fw-bold text-dark mb-3">
|
||||||
|
<i class="fas fa-list-check text-primary me-1"></i> Data Input Kuesioner (Raw & Transformed)
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
<!-- Tabel Rincian Kolom Database -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-striped align-middle mb-0">
|
||||||
|
<thead class="table-light small text-uppercase">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 35%;">Kriteria / Atribut</th>
|
||||||
|
<th>Nilai / Jawaban Inputan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-semibold text-muted small">Nilai Akademik</td>
|
||||||
|
<td class="fw-bold text-dark" id="detailNilai">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-semibold text-muted small">Minat Utama</td>
|
||||||
|
<td class="fw-bold text-dark" id="detailMinat">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-semibold text-muted small">Cita-Cita / Karir</td>
|
||||||
|
<td class="fw-bold text-dark" id="detailCitaCita">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-semibold text-muted small">Preferensi Studi</td>
|
||||||
|
<td class="fw-bold text-dark" id="detailPreferensi">-</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer bg-light border-0 py-2 px-4">
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm rounded-pill px-4" data-bs-dismiss="modal">Tutup</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Daftar Pengguna')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<!-- Style Khusus Modal Fix Backdrop Z-Index -->
|
||||||
|
<style>
|
||||||
|
.modal-backdrop {
|
||||||
|
z-index: 1040 !important;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
z-index: 1050 !important;
|
||||||
|
}
|
||||||
|
.modal-content-custom {
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.modal-header-custom {
|
||||||
|
background: linear-gradient(135deg, #1e40af 0%, #2563eb 100%);
|
||||||
|
}
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Header Halaman -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h4 class="fw-bold mb-1"><i class="fas fa-users-cog text-primary me-2"></i>Daftar Pengguna Sistem</h4>
|
||||||
|
<p class="text-muted small mb-0">Kelola akun akses sistem untuk Guru Bimbingan Konseling (BK) dan pendaftaran siswa.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary rounded-pill px-4 shadow-sm fw-semibold" data-bs-toggle="modal" data-bs-target="#modalTambahUser">
|
||||||
|
<i class="fas fa-user-plus me-2"></i>Tambah Akun Guru
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (session('success'))
|
||||||
|
<div class="alert alert-success alert-dismissible fade show rounded-3 border-0 shadow-sm mb-4" role="alert">
|
||||||
|
<i class="fas fa-check-circle me-1"></i> {{ session('success') }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (session('error'))
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show rounded-3 border-0 shadow-sm mb-4" role="alert">
|
||||||
|
<i class="fas fa-exclamation-triangle me-1"></i> {{ session('error') }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Tabel Daftar User -->
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-3">No</th>
|
||||||
|
<th>Nama Lengkap</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Peran Akses</th>
|
||||||
|
<th>Tanggal Terdaftar</th>
|
||||||
|
<th class="text-center">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($users as $index => $u)
|
||||||
|
<tr>
|
||||||
|
<td class="ps-3 fw-medium text-muted">{{ $users->firstItem() + $index }}</td>
|
||||||
|
<td class="fw-bold text-dark">{{ $u->name }}</td>
|
||||||
|
<td>{{ $u->email }}</td>
|
||||||
|
<td>
|
||||||
|
@if($u->role === 'admin' || $u->role === 'guru')
|
||||||
|
<span class="badge bg-primary-subtle text-primary border border-primary-subtle px-3 py-1 rounded-pill fw-semibold">
|
||||||
|
<i class="fas fa-user-tie me-1"></i> Guru
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-info-subtle text-info-emphasis border border-info-subtle px-3 py-1 rounded-pill fw-medium">
|
||||||
|
<i class="fas fa-user-graduate me-1"></i> Siswa
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>{{ \Carbon\Carbon::parse($u->created_at)->translatedFormat('d M Y') }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
@if(auth()->id() === $u->id)
|
||||||
|
<span class="badge bg-light text-primary border fw-medium px-3 py-1 rounded-pill">Akun Anda</span>
|
||||||
|
@elseif($u->role === 'siswa')
|
||||||
|
<form action="{{ route('admin.users.destroy', $u->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Apakah Anda yakin ingin menghapus akun Siswa ini?')">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger rounded-3 px-2 py-1" title="Hapus Akun Siswa">
|
||||||
|
<i class="fas fa-trash-alt me-1"></i> Hapus
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-light text-secondary border fw-medium px-3 py-1 rounded-pill">
|
||||||
|
<i class="fas fa-shield-alt me-1"></i> Akun Guru
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center text-muted py-4">Belum ada data pengguna terdaftar.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- Footer Card & Pagination Rapi -->
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center mt-4 pt-3 border-top gap-2">
|
||||||
|
<div class="small text-muted">
|
||||||
|
Menampilkan <strong>{{ $users->firstItem() ?? 0 }}</strong> - <strong>{{ $users->lastItem() ?? 0 }}</strong> dari <strong>{{ $users->total() }}</strong> pengguna
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ $users->appends(request()->query())->links('pagination::bootstrap-5') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
{{-- MODAL TAMBAH AKUN GURU BK --}}
|
||||||
|
<div class="modal fade" id="modalTambahUser" tabindex="-1" aria-labelledby="modalTambahUserLabel" aria-hidden="true" data-bs-backdrop="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content modal-content-custom rounded-4 overflow-hidden">
|
||||||
|
|
||||||
|
<!-- Modal Header -->
|
||||||
|
<div class="modal-header modal-header-custom text-white p-4">
|
||||||
|
<div>
|
||||||
|
<h5 class="modal-title fw-bold mb-1 text-white" id="modalTambahUserLabel">
|
||||||
|
<i class="fas fa-user-plus me-2"></i>Tambah Akun Guru
|
||||||
|
</h5>
|
||||||
|
<p class="small text-white opacity-75 mb-0">Buatkan akun akses baru untuk pengelola Bimbingan Konseling</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{{ route('admin.users.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="role" value="admin">
|
||||||
|
|
||||||
|
<div class="modal-body p-4 bg-white">
|
||||||
|
|
||||||
|
<!-- Alert Petunjuk Pengisian yang Umum & Fleksibel -->
|
||||||
|
<div class="alert alert-primary bg-primary-subtle border-0 rounded-3 p-3 mb-4">
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<i class="fas fa-info-circle text-primary fs-5 me-3 mt-1"></i>
|
||||||
|
<div class="small text-primary-emphasis">
|
||||||
|
<strong>Informasi Registrasi:</strong><br>
|
||||||
|
Isikan data diri Guru BK yang akan diberi hak akses. Email dan kata sandi ini akan digunakan untuk masuk ke dalam sistem.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Nama Lengkap -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold text-dark small text-uppercase">Nama Lengkap</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-white border-end-0 rounded-start-3 text-muted">
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text" name="name" class="form-control bg-white border-start-0 rounded-end-3 py-2" placeholder="Contoh: Siti Rahmawati (gelar opsional)" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Email -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold text-dark small text-uppercase">Alamat Email</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-white border-end-0 rounded-start-3 text-muted">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</span>
|
||||||
|
<input type="email" name="email" class="form-control bg-white border-start-0 rounded-end-3 py-2" placeholder="Contoh: nama@gmail.com" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Password dengan Toggle Lihat Password -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold text-dark small text-uppercase">Kata Sandi (Password)</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-white border-end-0 rounded-start-3 text-muted">
|
||||||
|
<i class="fas fa-lock"></i>
|
||||||
|
</span>
|
||||||
|
<input type="password" name="password" id="passwordInput" class="form-control bg-white border-start-0 border-end-0 py-2" placeholder="Masukkan kata sandi (minimal 8 karakter)" required>
|
||||||
|
<span class="input-group-text bg-white border-start-0 rounded-end-3 text-muted cursor-pointer" id="togglePassword">
|
||||||
|
<i class="fas fa-eye" id="togglePasswordIcon"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer bg-light border-0 p-3 px-4">
|
||||||
|
<button type="button" class="btn btn-outline-secondary rounded-pill px-4" data-bs-dismiss="modal">Batal</button>
|
||||||
|
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">
|
||||||
|
<i class="fas fa-save me-1"></i> Simpan Akun Guru
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SCRIPT UNTUK TOGGLE EYE SEE/HIDE PASSWORD -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const togglePassword = document.querySelector('#togglePassword');
|
||||||
|
const passwordInput = document.querySelector('#passwordInput');
|
||||||
|
const toggleIcon = document.querySelector('#togglePasswordIcon');
|
||||||
|
|
||||||
|
if (togglePassword && passwordInput && toggleIcon) {
|
||||||
|
togglePassword.addEventListener('click', function () {
|
||||||
|
// Toggle tipe atribut input
|
||||||
|
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||||
|
passwordInput.setAttribute('type', type);
|
||||||
|
|
||||||
|
// Toggle ikon mata
|
||||||
|
toggleIcon.classList.toggle('fa-eye');
|
||||||
|
toggleIcon.classList.toggle('fa-eye-slash');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<x-guest-layout>
|
||||||
|
<div class="mb-4 text-sm text-gray-600">
|
||||||
|
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('password.confirm') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="password" :value="__('Password')" />
|
||||||
|
|
||||||
|
<x-text-input id="password" class="block mt-1 w-full"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required autocomplete="current-password" />
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<x-primary-button>
|
||||||
|
{{ __('Confirm') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</x-guest-layout>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<x-guest-layout>
|
||||||
|
<div class="mb-4 text-sm text-gray-600">
|
||||||
|
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Session Status -->
|
||||||
|
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('password.email') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="email" :value="__('Email')" />
|
||||||
|
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
|
||||||
|
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end mt-4">
|
||||||
|
<x-primary-button>
|
||||||
|
{{ __('Email Password Reset Link') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</x-guest-layout>
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
<!-- resources/views/auth/login.blade.php -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - Sistem Rekomendasi Prodi C4.5</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
|
||||||
|
<style>
|
||||||
|
body { background: #f8fafc; font-family: 'Inter', sans-serif; }
|
||||||
|
.auth-container { min-height: 100vh; }
|
||||||
|
.auth-banner {
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
|
||||||
|
border-radius: 0 2rem 2rem 0;
|
||||||
|
}
|
||||||
|
.btn-purple { background: #6366f1; color: white; font-weight: 600; border-radius: 0.75rem; }
|
||||||
|
.btn-purple:hover { background: #4f46e5; color: white; }
|
||||||
|
.form-control-lg { border-radius: 0.75rem; font-size: 0.95rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container-fluid p-0">
|
||||||
|
<div class="row g-0 auth-container">
|
||||||
|
<!-- Banner Kiri -->
|
||||||
|
<div class="col-lg-6 d-none d-lg-flex auth-banner p-5 text-white flex-column justify-content-between">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<i class="fa-solid fa-graduation-cap fs-2"></i>
|
||||||
|
<span class="fw-bold fs-4">Sistem Rekomendasi Jurusan</span>
|
||||||
|
</div>
|
||||||
|
<div class="my-auto animate__animated animate__fadeInLeft">
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Temukan Masa Depan Akademik Secara Presisi.</h1>
|
||||||
|
<p class="lead opacity-75">Platform berbasis Algoritma C4.5 untuk membantu pemilihan program studi secara transparan dan terukur.</p>
|
||||||
|
</div>
|
||||||
|
<div class="small opacity-50">© {{ date('Y') }} Sistem Rekomendasi C4.5</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Kanan -->
|
||||||
|
<div class="col-lg-6 d-flex align-items-center justify-content-center p-4 p-md-5">
|
||||||
|
<div class="w-100" style="max-width: 450px;">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h3 class="fw-bold text-dark">Selamat Datang!</h3>
|
||||||
|
<p class="text-muted small">Silakan login atau buat akun untuk mengakses sistem</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alert Error -->
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="alert alert-danger rounded-3 py-2 px-3 mb-3 small">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation me-1"></i> {{ $errors->first() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Tabs Switcher -->
|
||||||
|
<ul class="nav nav-pills nav-justified mb-4 bg-light p-1 rounded-3" id="authTab">
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link active rounded-3" id="login-tab" data-bs-toggle="pill" data-bs-target="#login-panel">Login</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link rounded-3" id="register-tab" data-bs-toggle="pill" data-bs-target="#register-panel">Register Siswa</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="authTabContent">
|
||||||
|
<!-- Panel Login -->
|
||||||
|
<div class="tab-pane fade show active" id="login-panel">
|
||||||
|
<form action="{{ route('login') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-secondary fw-semibold">Alamat Email</label>
|
||||||
|
<input type="email" name="email" class="form-control form-control-lg" placeholder="nama@email.com" value="{{ old('email') }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-secondary fw-semibold">Password</label>
|
||||||
|
<input type="password" name="password" class="form-control form-control-lg" placeholder="••••••••" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-purple btn-lg w-100 py-3 mt-2 shadow-sm">
|
||||||
|
<i class="fa-solid fa-right-to-bracket me-2"></i> Masuk Sekarang
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Panel Register (Khusus Siswa) -->
|
||||||
|
<div class="tab-pane fade" id="register-panel">
|
||||||
|
<form action="{{ route('register') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-secondary fw-semibold">Nama Lengkap Siswa</label>
|
||||||
|
<input type="text" name="name" class="form-control form-control-lg" placeholder="Masukkan nama lengkap" value="{{ old('name') }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-secondary fw-semibold">Alamat Email</label>
|
||||||
|
<input type="email" name="email" class="form-control form-control-lg" placeholder="nama@email.com" value="{{ old('email') }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-secondary fw-semibold">Password</label>
|
||||||
|
<input type="password" name="password" class="form-control form-control-lg" placeholder="Minimal 8 karakter" required>
|
||||||
|
</div>
|
||||||
|
<!-- Role dikunci sebagai siswa -->
|
||||||
|
<input type="hidden" name="role" value="siswa">
|
||||||
|
<button type="submit" class="btn btn-purple btn-lg w-100 py-3 mt-2 shadow-sm">
|
||||||
|
<i class="fa-solid fa-user-plus me-2"></i> Buat Akun Siswa Baru
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
<x-guest-layout>
|
||||||
|
<form method="POST" action="{{ route('register') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Name -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="name" :value="__('Name')" />
|
||||||
|
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
|
||||||
|
<x-input-error :messages="$errors->get('name')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="email" :value="__('Email')" />
|
||||||
|
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
|
||||||
|
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="password" :value="__('Password')" />
|
||||||
|
|
||||||
|
<x-text-input id="password" class="block mt-1 w-full"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required autocomplete="new-password" />
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Password -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||||
|
|
||||||
|
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||||
|
type="password"
|
||||||
|
name="password_confirmation" required autocomplete="new-password" />
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end mt-4">
|
||||||
|
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('login') }}">
|
||||||
|
{{ __('Already registered?') }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<x-primary-button class="ms-4">
|
||||||
|
{{ __('Register') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</x-guest-layout>
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<x-guest-layout>
|
||||||
|
<form method="POST" action="{{ route('password.store') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<!-- Password Reset Token -->
|
||||||
|
<input type="hidden" name="token" value="{{ $request->route('token') }}">
|
||||||
|
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="email" :value="__('Email')" />
|
||||||
|
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
|
||||||
|
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="password" :value="__('Password')" />
|
||||||
|
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
|
||||||
|
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Password -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||||
|
|
||||||
|
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||||
|
type="password"
|
||||||
|
name="password_confirmation" required autocomplete="new-password" />
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end mt-4">
|
||||||
|
<x-primary-button>
|
||||||
|
{{ __('Reset Password') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</x-guest-layout>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<x-guest-layout>
|
||||||
|
<div class="mb-4 text-sm text-gray-600">
|
||||||
|
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (session('status') == 'verification-link-sent')
|
||||||
|
<div class="mb-4 font-medium text-sm text-green-600">
|
||||||
|
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center justify-between">
|
||||||
|
<form method="POST" action="{{ route('verification.send') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<x-primary-button>
|
||||||
|
{{ __('Resend Verification Email') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<button type="submit" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
|
{{ __('Log Out') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</x-guest-layout>
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
{{-- resources/views/c45/_tree_node.blade.php --}}
|
||||||
|
@php
|
||||||
|
$isRoot = $isRoot ?? false;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if($tree['type'] === 'leaf')
|
||||||
|
{{-- LEAF NODE (KOTAK HIJAU - JURUSAN) --}}
|
||||||
|
<div class="leaf-box-result">
|
||||||
|
<i class="fas fa-graduation-cap me-1"></i>
|
||||||
|
<span>{{ $tree['label'] }}</span>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
{{-- NODE ATRIBUT (LINGKARAN BIRU) --}}
|
||||||
|
<div class="{{ $isRoot ? 'circle-node-root' : 'circle-node-child' }}">
|
||||||
|
@if($isRoot)
|
||||||
|
<span style="font-size: 8px; opacity: 0.8;" class="text-uppercase">1. ROOT</span>
|
||||||
|
@else
|
||||||
|
<span style="font-size: 8px; opacity: 0.8;" class="text-uppercase">{{ $tree['level'] ?? 2 }}. NODE</span>
|
||||||
|
@endif
|
||||||
|
<span>{{ strtoupper(str_replace('_', ' ', $tree['attribute'])) }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- CABANG / ANAK NODE --}}
|
||||||
|
@if($tree['type'] === 'node' && !empty($tree['children']))
|
||||||
|
<ul>
|
||||||
|
@foreach($tree['children'] as $nilaiCabang => $childNode)
|
||||||
|
<li>
|
||||||
|
{{-- LABEL CRITERIA PADA GARIS CABANG --}}
|
||||||
|
<div class="branch-label-wrapper">
|
||||||
|
<span class="branch-label-dosen">
|
||||||
|
{{ $nilaiCabang }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- REKURSI KE ANAK BERSAMAAN DENGAN NODE KETURUNANNYA --}}
|
||||||
|
@include('c45._tree_node', ['tree' => $childNode, 'isRoot' => false])
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@endif
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Input Cek Rekomendasi')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid px-3">
|
||||||
|
|
||||||
|
<!-- Header & Tombol Kembali -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between my-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="h4 mb-0 text-gray-800 font-weight-bold">
|
||||||
|
<i class="fa-solid fa-compass text-primary me-2"></i>Input Data Cek Rekomendasi
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted small mb-0">Masukkan data kriteria siswa untuk dianalisis oleh algoritma C4.5.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('c45.index') }}" class="btn btn-outline-secondary btn-sm rounded-pill px-3 shadow-sm">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i> Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Alert Jika Ada Error Validasi --}}
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show rounded-3 mb-3 shadow-sm" role="alert">
|
||||||
|
<strong><i class="fas fa-exclamation-triangle me-1"></i> Periksa Inputan Anda:</strong>
|
||||||
|
<ul class="mb-0 mt-2 ps-3 small">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Card Form Input Standalone -->
|
||||||
|
<div class="card shadow-sm border-0 mb-4 rounded-3">
|
||||||
|
<div class="card-header py-3 bg-white border-bottom">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary small">
|
||||||
|
<i class="fas fa-user-graduate me-1"></i> Form Kriteria Rekomendasi Siswa
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{{ route('prediksi.hitung') }}" method="POST" id="formRekomendasi">
|
||||||
|
@csrf
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Sisi Kiri: Form Input Kriteria -->
|
||||||
|
<div class="col-lg-8 border-end pr-lg-4">
|
||||||
|
<h6 class="font-weight-bold text-primary mb-3">
|
||||||
|
<i class="fas fa-list me-1"></i> Atribut Kriteria Siswa (Features)
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
<!-- Input Nama Siswa (Searchable Datalist / Ketik Manual) -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label font-weight-bold text-secondary small">Nama Siswa <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-light"><i class="fas fa-user text-muted"></i></span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="nama"
|
||||||
|
id="nama_siswa"
|
||||||
|
class="form-control"
|
||||||
|
list="list_siswa"
|
||||||
|
placeholder="Ketik atau pilih nama siswa..."
|
||||||
|
value="{{ old('nama') }}"
|
||||||
|
autocomplete="off"
|
||||||
|
oninput="updateSiswaId(this)"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<datalist id="list_siswa">
|
||||||
|
@if(isset($daftar_siswa))
|
||||||
|
@foreach($daftar_siswa as $siswa)
|
||||||
|
<option value="{{ $siswa->name }}" data-id="{{ $siswa->id }}"></option>
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted d-block mt-1" style="font-size: 0.78rem;">
|
||||||
|
*Pilih siswa yang sudah terdaftar atau ketik nama langsung jika belum terdaftar.
|
||||||
|
</small>
|
||||||
|
<!-- Hidden input untuk menyimpan user_id jika siswa terdaftar -->
|
||||||
|
<input type="hidden" name="siswa_id" id="siswa_id" value="{{ old('siswa_id') }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Nilai Rapor -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label font-weight-bold text-secondary small">Nilai Rata-rata Rapor <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-light"><i class="fas fa-award text-muted"></i></span>
|
||||||
|
<input type="number" name="nilai" step="0.01" min="0" max="100" class="form-control" placeholder="Contoh: 85.50" value="{{ old('nilai') }}" required>
|
||||||
|
<span class="input-group-text bg-light text-secondary">Skala 100</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Mata Pelajaran Favorit -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label font-weight-bold text-secondary small">Mata Pelajaran Favorit <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-light"><i class="fas fa-book text-muted"></i></span>
|
||||||
|
<input type="text" name="mapel_favorit" list="list_mapel_siswa" class="form-control" placeholder="Ketik atau pilih mata pelajaran..." value="{{ old('mapel_favorit') }}" autocomplete="off" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Preferensi Studi -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label font-weight-bold text-secondary small">Preferensi Studi / Rumpun Keilmuan <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-light"><i class="fas fa-book-reader text-muted"></i></span>
|
||||||
|
<input type="text" name="preferensi_studi" list="list_studi_siswa" class="form-control" placeholder="Ketik atau pilih rumpun studi..." value="{{ old('preferensi_studi') }}" autocomplete="off" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Cita-Cita -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label font-weight-bold text-secondary small">Cita-Cita / Orientasi Karier <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-light"><i class="fas fa-briefcase text-muted"></i></span>
|
||||||
|
<input type="text" name="cita_cita" list="list_cita_siswa" class="form-control" placeholder="Ketik cita-cita..." value="{{ old('cita_cita') }}" autocomplete="off" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Minat -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label font-weight-bold text-secondary small">Minat Spesifik Siswa <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-light"><i class="fas fa-heart text-muted"></i></span>
|
||||||
|
<input type="text" name="minat" list="list_minat_siswa" class="form-control" placeholder="Ketik minat spesifik..." value="{{ old('minat') }}" autocomplete="off" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sisi Kanan: Panduan Pengisian -->
|
||||||
|
<div class="col-lg-4 pl-lg-4 mt-3 mt-lg-0">
|
||||||
|
<div class="p-3 bg-light rounded border">
|
||||||
|
<h6 class="font-weight-bold text-dark small mb-2">
|
||||||
|
<i class="fas fa-info-circle text-info me-1"></i> Panduan Pengisian
|
||||||
|
</h6>
|
||||||
|
<p class="small text-muted mb-2">
|
||||||
|
Isikan kriteria siswa sesuai dengan data riil. Algoritma C4.5 akan melakukan pencocokan data ini berdasarkan pohon keputusan (*decision tree*) yang terbentuk dari data training.
|
||||||
|
</p>
|
||||||
|
<hr class="my-2">
|
||||||
|
<small class="text-secondary d-block">
|
||||||
|
<i class="fas fa-check-circle text-success me-1"></i> Pilih nama dari daftar rekomendasi jika siswa sudah memiliki akun.
|
||||||
|
</small>
|
||||||
|
<small class="text-secondary d-block mt-1">
|
||||||
|
<i class="fas fa-check-circle text-success me-1"></i> Anda dapat langsung mengetikkan nama siswa baru jika belum terdaftar.
|
||||||
|
</small>
|
||||||
|
<small class="text-secondary d-block mt-1">
|
||||||
|
<i class="fas fa-check-circle text-success me-1"></i> Nilai diisi dalam rentang 0 - 100.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tombol Aksi -->
|
||||||
|
<div class="card-footer bg-light py-2 px-4 d-flex justify-content-end gap-2">
|
||||||
|
<a href="{{ route('c45.index') }}" class="btn btn-secondary btn-sm rounded-pill px-3">Batal</a>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm rounded-pill font-weight-bold px-4 shadow-sm">
|
||||||
|
<i class="fa-solid fa-wand-magic-sparkles me-1"></i> Analisis Rekomendasi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Datalist Autocomplete Features -->
|
||||||
|
<datalist id="list_mapel_siswa">
|
||||||
|
<option value="Matematika">
|
||||||
|
<option value="Bahasa Inggris">
|
||||||
|
<option value="Biologi">
|
||||||
|
<option value="Fisika">
|
||||||
|
<option value="Ekonomi">
|
||||||
|
<option value="Sejarah">
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<datalist id="list_studi_siswa">
|
||||||
|
<option value="Pertanian & Hortikultura">
|
||||||
|
<option value="Komputer & Informatika">
|
||||||
|
<option value="Ekonomi & Agribisnis">
|
||||||
|
<option value="Bahasa & Komunikasi">
|
||||||
|
<option value="Kesehatan & Gizi">
|
||||||
|
<option value="Bisnis & Manajemen">
|
||||||
|
<option value="Ilmu Peternakan">
|
||||||
|
<option value="Rekayasa Otomotif">
|
||||||
|
<option value="Sains & Teknologi">
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<datalist id="list_cita_siswa">
|
||||||
|
<option value="Ahli Agronomi">
|
||||||
|
<option value="Peternak">
|
||||||
|
<option value="Pengusaha">
|
||||||
|
<option value="Ahli Gizi">
|
||||||
|
<option value="Manager Rantai Pasok">
|
||||||
|
<option value="Manajer">
|
||||||
|
<option value="Quality Control">
|
||||||
|
<option value="Petani Modern">
|
||||||
|
<option value="Teknisi">
|
||||||
|
<option value="Bahasa & Pariwisata">
|
||||||
|
<option value="IT">
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<datalist id="list_minat_siswa">
|
||||||
|
<option value="Budidaya Tanaman">
|
||||||
|
<option value="Kimia Pangan">
|
||||||
|
<option value="Budidaya Ternak">
|
||||||
|
<option value="Teknologi Info & Pemrograman">
|
||||||
|
<option value="Energi & Tek Mesin">
|
||||||
|
<option value="Bahasa & Komunikasi">
|
||||||
|
<option value="Bisnis">
|
||||||
|
<option value="Kesehatan">
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<!-- JavaScript Sinkronisasi ID Siswa Jika Nama Cocok -->
|
||||||
|
<script>
|
||||||
|
function updateSiswaId(input) {
|
||||||
|
var options = document.getElementById('list_siswa').options;
|
||||||
|
var siswaIdInput = document.getElementById('siswa_id');
|
||||||
|
|
||||||
|
siswaIdInput.value = ''; // Reset ID
|
||||||
|
|
||||||
|
for (var i = 0; i < options.length; i++) {
|
||||||
|
if (options[i].value.trim().toLowerCase() === input.value.trim().toLowerCase()) {
|
||||||
|
siswaIdInput.value = options[i].getAttribute('data-id');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Hasil Rekomendasi C4.5')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid px-3">
|
||||||
|
|
||||||
|
<!-- Header & Tombol Kembali -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between my-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="h4 mb-0 text-gray-800 font-weight-bold">
|
||||||
|
<i class="fa-solid fa-square-poll-vertical text-primary me-2"></i>Hasil Rekomendasi C4.5
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted small mb-0">Hasil analisis kriteria siswa berdasarkan algoritma C4.5 Decision Tree.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="{{ route('c45.create') }}" class="btn btn-primary btn-sm rounded-pill px-3 shadow-sm me-2">
|
||||||
|
<i class="fas fa-plus me-1"></i> Cek Rekomendasi Lagi
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('c45.index') }}" class="btn btn-outline-secondary btn-sm rounded-pill px-3 shadow-sm">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i> Kembali ke Menu
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@php
|
||||||
|
// Mengambil data dari session atau $data jika dipass dari controller
|
||||||
|
$dataTesting = session('hasil_prediksi') ?? $dataTesting ?? null;
|
||||||
|
$rulePath = session('rulePath') ?? $rulePath ?? [];
|
||||||
|
$hasilJurusan = session('hasilJurusan') ?? (is_object($dataTesting) ? ($dataTesting->hasil_rekomendasi ?? '') : '');
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if($dataTesting)
|
||||||
|
<div id="section-hasil-prediksi" class="mb-4">
|
||||||
|
<div class="card shadow-sm border-0 rounded-3">
|
||||||
|
<div class="card-header py-3 bg-white border-bottom d-flex align-items-center justify-content-between">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary small">
|
||||||
|
<i class="fas fa-square-poll-vertical me-1"></i> Output Klasifikasi C4.5
|
||||||
|
</h6>
|
||||||
|
@if(!empty(is_object($dataTesting) ? $dataTesting->nama : ($dataTesting['nama'] ?? '')))
|
||||||
|
<span class="badge bg-light text-dark border font-weight-normal px-3 py-1.5 rounded-pill">
|
||||||
|
<i class="fas fa-user text-muted me-1"></i> {{ is_object($dataTesting) ? $dataTesting->nama : $dataTesting['nama'] }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<!-- Highlight Jurusan Hasil Rekomendasi -->
|
||||||
|
<div class="p-4 bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25 rounded-3 text-center my-2 shadow-sm">
|
||||||
|
<span class="small text-uppercase tracking-wider font-weight-bold text-muted">Program Studi Rekomendasi Utama:</span>
|
||||||
|
<h2 class="display-6 font-weight-bold text-primary mb-0 mt-2">
|
||||||
|
{{ $hasilJurusan }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layout 2 Kolom: Kiri Detail Input, Kanan Jalur Keputusan -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<!-- Sisi Kiri: Rincian Input & Pemetaan -->
|
||||||
|
<div class="col-lg-7 border-end pr-lg-4">
|
||||||
|
<h6 class="font-weight-bold text-primary mb-3 small">
|
||||||
|
<i class="fas fa-list-check me-1"></i> Rincian Input & Hasil Preprocessing:
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
<ul class="list-group list-group-flush rounded-3 border mb-3">
|
||||||
|
<!-- Nilai Rapor -->
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center py-2.5">
|
||||||
|
<div>
|
||||||
|
<small class="text-muted d-block">Nilai Rapor (Raw)</small>
|
||||||
|
<strong class="text-dark">{{ is_object($dataTesting) ? ($dataTesting->nilai_raw ?? $dataTesting->nilai) : ($dataTesting['nilai_raw'] ?? $dataTesting['nilai'] ?? '-') }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<small class="text-muted d-block">Kategori C4.5</small>
|
||||||
|
<span class="badge bg-info text-dark px-2.5 py-1.5 fs-6">{{ is_object($dataTesting) ? $dataTesting->nilai : ($dataTesting['nilai'] ?? '-') }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Mata Pelajaran Favorit (DITAMBAHKAN) -->
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center py-2.5">
|
||||||
|
<div style="max-width: 60%;">
|
||||||
|
<small class="text-muted d-block">Mata Pelajaran Favorit</small>
|
||||||
|
<span class="fst-italic text-dark">"{{ is_object($dataTesting) ? ($dataTesting->mapel_favorit_raw ?? $dataTesting->mapel_favorit) : ($dataTesting['mapel_favorit_raw'] ?? $dataTesting['mapel_favorit'] ?? '-') }}"</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<small class="text-muted d-block">Pemetaan</small>
|
||||||
|
<span class="badge bg-warning text-dark px-2.5 py-1.5 fs-6">{{ is_object($dataTesting) ? $dataTesting->mapel_favorit : ($dataTesting['mapel_favorit'] ?? '-') }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Preferensi Studi -->
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center py-2.5">
|
||||||
|
<div style="max-width: 60%;">
|
||||||
|
<small class="text-muted d-block">Preferensi Studi</small>
|
||||||
|
<span class="fst-italic text-dark">"{{ is_object($dataTesting) ? ($dataTesting->preferensi_studi_raw ?? $dataTesting->preferensi_studi) : ($dataTesting['preferensi_studi_raw'] ?? $dataTesting['preferensi_studi'] ?? '-') }}"</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<small class="text-muted d-block">Pemetaan</small>
|
||||||
|
<span class="badge bg-secondary px-2.5 py-1.5 fs-6">{{ is_object($dataTesting) ? $dataTesting->preferensi_studi : ($dataTesting['preferensi_studi'] ?? '-') }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Cita-Cita / Karier -->
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center py-2.5">
|
||||||
|
<div style="max-width: 60%;">
|
||||||
|
<small class="text-muted d-block">Cita-Cita / Karier</small>
|
||||||
|
<span class="fst-italic text-dark">"{{ is_object($dataTesting) ? ($dataTesting->cita_cita_raw ?? $dataTesting->cita_cita) : ($dataTesting['cita_cita_raw'] ?? $dataTesting['cita_cita'] ?? '-') }}"</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<small class="text-muted d-block">Pemetaan</small>
|
||||||
|
<span class="badge bg-success px-2.5 py-1.5 fs-6">{{ is_object($dataTesting) ? $dataTesting->cita_cita : ($dataTesting['cita_cita'] ?? '-') }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Minat Spesifik -->
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center py-2.5">
|
||||||
|
<div style="max-width: 60%;">
|
||||||
|
<small class="text-muted d-block">Minat Spesifik</small>
|
||||||
|
<span class="fst-italic text-dark">"{{ is_object($dataTesting) ? ($dataTesting->minat_raw ?? $dataTesting->minat) : ($dataTesting['minat_raw'] ?? $dataTesting['minat'] ?? '-') }}"</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<small class="text-muted d-block">Pemetaan</small>
|
||||||
|
<span class="badge bg-primary px-2.5 py-1.5 fs-6">{{ is_object($dataTesting) ? $dataTesting->minat : ($dataTesting['minat'] ?? '-') }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sisi Kanan: Jalur Keputusan (Rules) -->
|
||||||
|
<div class="col-lg-5 pl-lg-4 mt-3 mt-lg-0">
|
||||||
|
<div class="p-3 bg-light rounded border">
|
||||||
|
<h6 class="font-weight-bold text-dark small mb-2">
|
||||||
|
<i class="fas fa-sitemap text-info me-1"></i> Jalur Keputusan (Rules)
|
||||||
|
</h6>
|
||||||
|
<p class="small text-muted mb-3">
|
||||||
|
Berikut langkah inferensi yang dilalui kriteria siswa pada *Decision Tree*:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if(!empty($rulePath))
|
||||||
|
<ul class="list-unstyled mb-2">
|
||||||
|
@foreach($rulePath as $step)
|
||||||
|
<li class="mb-2">
|
||||||
|
<span class="badge bg-white text-secondary border px-3 py-2 fs-6 shadow-sm w-100 text-start text-wrap">
|
||||||
|
<i class="fas fa-angle-right text-primary me-2"></i>{{ $step }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
<hr class="my-2">
|
||||||
|
<small class="text-secondary d-block mt-2">
|
||||||
|
<i class="fas fa-circle-check text-success me-1"></i> Kesimpulan Akhir: <strong class="text-success">{{ $hasilJurusan }}</strong>
|
||||||
|
</small>
|
||||||
|
@else
|
||||||
|
<small class="text-muted d-block italic">Jalur rule tidak tersedia.</small>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tombol Aksi Bawah -->
|
||||||
|
<div class="card-footer bg-light py-2 px-4 d-flex justify-content-end gap-2 no-print">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm rounded-pill font-weight-bold px-4 shadow-sm" onclick="window.print()">
|
||||||
|
<i class="fa-solid fa-print me-1"></i> Cetak Laporan (PDF)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="alert alert-warning text-center">
|
||||||
|
Data hasil rekomendasi tidak ditemukan. Silakan <a href="{{ route('c45.create') }}">input data kembali</a>.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,759 @@
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', 'Cek Rekomendasi')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<!-- Custom CSS Tambahan untuk Form Interaktif & Pohon Keputusan -->
|
||||||
|
<style>
|
||||||
|
/* 1. Hero Card Gradient Modern & Glass Effect */
|
||||||
|
.hero-card-dominant {
|
||||||
|
background: linear-gradient(135deg, #1e40af 0%, #2563eb 60%, #3b82f6 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 14px 28px rgba(37, 99, 235, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title-main {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.25;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title-highlight {
|
||||||
|
background: linear-gradient(90deg, #fde047 0%, #f59e0b 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-badge-hero {
|
||||||
|
background: rgba(255, 255, 255, 0.15) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling Tombol CTA Utama (Highlight Kuning Emas Premium) */
|
||||||
|
.btn-cta-highlight {
|
||||||
|
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%) !important;
|
||||||
|
color: #0f172a !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 12px 22px !important;
|
||||||
|
border: 2px solid #fef08a !important;
|
||||||
|
box-shadow: 0 8px 20px rgba(245, 158, 11, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.5) !important;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cta-highlight:hover {
|
||||||
|
transform: translateY(-3px) scale(1.03) !important;
|
||||||
|
background: linear-gradient(135deg, #fef08a 0%, #fbbf24 100%) !important;
|
||||||
|
box-shadow: 0 12px 25px rgba(245, 158, 11, 0.55) !important;
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cta-highlight .icon-circle {
|
||||||
|
background: rgba(15, 23, 42, 0.12);
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decorative Light Effect inside Hero */
|
||||||
|
.hero-glow-circle {
|
||||||
|
position: absolute;
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(40px);
|
||||||
|
top: -50px;
|
||||||
|
right: 15%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS Tree Layout Sesuai Standar Dosen */
|
||||||
|
.tree-dosen-container {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 30px 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dosen {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dosen ul {
|
||||||
|
padding-top: 25px;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.5s;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dosen li {
|
||||||
|
text-align: center;
|
||||||
|
list-style-type: none;
|
||||||
|
position: relative;
|
||||||
|
padding: 25px 8px 0 8px;
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garis Penghubung Cabang */
|
||||||
|
.tree-dosen li::before, .tree-dosen li::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 50%;
|
||||||
|
border-top: 2px solid #475569;
|
||||||
|
width: 50%;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dosen li::after {
|
||||||
|
right: auto;
|
||||||
|
left: 50%;
|
||||||
|
border-left: 2px solid #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dosen li:only-child::after, .tree-dosen li:only-child::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dosen li:only-child {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dosen li:first-child::before, .tree-dosen li:last-child::after {
|
||||||
|
border: 0 none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dosen li:last-child::before {
|
||||||
|
border-right: 2px solid #475569;
|
||||||
|
border-radius: 0 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dosen li:first-child::after {
|
||||||
|
border-radius: 8px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dosen ul ul::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
border-left: 2px solid #475569;
|
||||||
|
width: 0;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node Lingkaran Utama (Root) */
|
||||||
|
.circle-node-root {
|
||||||
|
width: 110px;
|
||||||
|
height: 110px;
|
||||||
|
border: 3px solid #1e40af;
|
||||||
|
background-color: #dbeafe;
|
||||||
|
color: #1e3a8a;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 12px;
|
||||||
|
box-shadow: 0 4px 10px rgba(30, 64, 175, 0.2);
|
||||||
|
margin: 0 auto 10px auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
white-space: normal;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node Lingkaran Sub-Level (Node 2, Node 3) */
|
||||||
|
.circle-node-child {
|
||||||
|
width: 95px;
|
||||||
|
height: 95px;
|
||||||
|
border: 3px solid #0284c7;
|
||||||
|
background-color: #e0f2fe;
|
||||||
|
color: #0369a1;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11px;
|
||||||
|
box-shadow: 0 4px 8px rgba(2, 132, 199, 0.2);
|
||||||
|
margin: 0 auto 10px auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
white-space: normal;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Label Kriteria pada Garis Cabang */
|
||||||
|
.branch-label-wrapper {
|
||||||
|
position: relative;
|
||||||
|
top: -14px;
|
||||||
|
z-index: 3;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-label-dosen {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f172a;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1.5px solid #64748b;
|
||||||
|
display: inline-block;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.06);
|
||||||
|
white-space: normal;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leaf Node (Hasil Akhir) */
|
||||||
|
.leaf-box-result {
|
||||||
|
border: 2px solid #059669;
|
||||||
|
background-color: #ecfdf5;
|
||||||
|
color: #065f46;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11px;
|
||||||
|
display: inline-block;
|
||||||
|
box-shadow: 0 4px 6px rgba(5, 150, 105, 0.15);
|
||||||
|
white-space: normal;
|
||||||
|
max-width: 140px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print Style agar hasil cetak rapi */
|
||||||
|
@media print {
|
||||||
|
body * {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
#section-hasil-prediksi, #section-hasil-prediksi * {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
#section-hasil-prediksi {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="c45-analytics-wrapper">
|
||||||
|
|
||||||
|
{{-- ALERT SUKSES --}}
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="alert alert-success alert-dismissible fade show rounded-3 mb-4 shadow-sm" role="alert">
|
||||||
|
<i class="fas fa-check-circle me-1"></i> {{ session('success') }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- 1. HERO HEADER UTAMA (DESAIN REVISI BERSIH & MEWAH) -->
|
||||||
|
<div class="card hero-card-dominant rounded-4 mb-4 overflow-hidden position-relative border-0">
|
||||||
|
<!-- Glow Light Effect Background -->
|
||||||
|
<div class="hero-glow-circle"></div>
|
||||||
|
|
||||||
|
<!-- Watermark Topi Wisuda (Pindah sedikit agar tidak tabrakan dengan tombol) -->
|
||||||
|
<div class="position-absolute end-0 bottom-0 opacity-10 me-2 mb-n4 text-white pointer-events-none" style="font-size: 12rem; line-height: 1;">
|
||||||
|
<i class="fas fa-user-graduate"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body p-4 p-lg-5 position-relative z-1">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
|
||||||
|
<!-- Sisi Kiri: Teks & Informational Content -->
|
||||||
|
<div class="col-lg-7 col-xl-8 mb-4 mb-lg-0">
|
||||||
|
<!-- Top Badge -->
|
||||||
|
<div class="d-inline-flex align-items-center gap-2 custom-badge-hero rounded-pill px-3 py-1 mb-3">
|
||||||
|
<i class="fas fa-sparkles text-warning fs-7"></i>
|
||||||
|
<span class="small fw-bold text-uppercase text-white" style="letter-spacing: 0.6px; font-size: 11px;">
|
||||||
|
Sistem Pendukung Keputusan C4.5
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Judul Utama dengan Typography Highlight -->
|
||||||
|
<h1 class="hero-title-main text-white mb-3">
|
||||||
|
Cek Rekomendasi <br>
|
||||||
|
<span class="hero-title-highlight">Jurusan Siswa</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Deskripsi Singkat & Jelas -->
|
||||||
|
<p class="text-white opacity-90 fs-6 mb-0 style-description" style="max-width: 620px; line-height: 1.6; font-weight: 400;">
|
||||||
|
Lakukan analisis penentuan program studi perguruan tinggi untuk siswa secara presisi berbasis algoritma C4.5 berdasarkan evaluasi rapor, minat, serta cita-cita karir.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sisi Kanan: Action Button / CTA Highlighted -->
|
||||||
|
<div class="col-lg-5 col-xl-4 text-center text-lg-end">
|
||||||
|
<a href="{{ route('c45.create') }}" class="btn btn-cta-highlight rounded-pill d-inline-flex align-items-center justify-content-center gap-2">
|
||||||
|
<i class="fas fa-user-plus fs-6"></i>
|
||||||
|
<span>Mulai Rekomendasi Siswa</span>
|
||||||
|
<span class="icon-circle ms-1">
|
||||||
|
<i class="fas fa-arrow-right fs-7"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. SECTION DIVIDER & SUB-HEADER ANALISIS MODEL -->
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center my-4 pt-2 pb-3 border-bottom">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<span class="badge bg-primary-subtle text-primary border border-primary-subtle rounded-pill px-3 py-1 fs-7 fw-semibold">
|
||||||
|
<i class="fas fa-brain me-1"></i> Model Analytics
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="fw-bold text-dark m-0 d-flex align-items-center gap-2 fs-4">
|
||||||
|
<i class="fas fa-microchip text-primary"></i> Transparansi & Analisis Algoritma C4.5
|
||||||
|
</h3>
|
||||||
|
<p class="text-muted small mb-0 mt-1">Laporan rincian kalkulasi matematis, entropy, gain ratio, dan pembentukan pohon keputusan.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 mt-md-0">
|
||||||
|
<div class="card border-0 bg-primary text-white shadow-sm rounded-pill px-3 py-2">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-database fs-5 text-white-50"></i>
|
||||||
|
<div>
|
||||||
|
<div class="text-uppercase opacity-75 lh-1" style="font-size: 10px; letter-spacing: 0.5px;">Dataset Training</div>
|
||||||
|
<div class="fw-bold fs-6 lh-1 mt-1">{{ number_format($totalData) }} Record</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cards Overview -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<!-- Entropy Total -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 text-white overflow-hidden h-100 position-relative" style="background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);">
|
||||||
|
<div class="position-absolute end-0 bottom-0 opacity-10 me-3 mb-2" style="font-size: 5rem; pointer-events: none;">
|
||||||
|
<i class="fas fa-calculator"></i>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4 position-relative z-1">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<span class="text-uppercase fw-bold text-light opacity-75 small d-flex align-items-center gap-2" style="letter-spacing: 0.5px;">
|
||||||
|
<i class="fas fa-calculator text-info"></i> Entropy Total System
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-white bg-opacity-10 text-light rounded-pill px-3 py-1 small border border-white border-opacity-10">Sistem Utama</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-baseline gap-2 mb-2">
|
||||||
|
<h1 class="display-5 fw-bold mb-0 text-white font-monospace">
|
||||||
|
{{ number_format($entropyTotal, 4) }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<small class="text-light opacity-50 d-block">
|
||||||
|
<i class="fas fa-info-circle me-1"></i>Tingkat ketidakpastian/heterogenitas data awal sebelum pembagian
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Root Node Terpilih -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 text-white overflow-hidden h-100 position-relative" style="background: linear-gradient(135deg, #059669 0%, #047857 100%);">
|
||||||
|
<div class="position-absolute end-0 bottom-0 opacity-10 me-3 mb-2" style="font-size: 5rem; pointer-events: none;">
|
||||||
|
<i class="fas fa-sitemap"></i>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4 position-relative z-1">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<span class="text-uppercase fw-bold text-light opacity-90 small d-flex align-items-center gap-2" style="letter-spacing: 0.5px;">
|
||||||
|
<i class="fas fa-sitemap text-warning"></i> Root Node Terpilih (Iterasi 1)
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-white bg-opacity-20 text-white rounded-pill px-3 py-1 small fw-semibold border border-white border-opacity-20">
|
||||||
|
Utama
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="fw-bold mb-2 text-white text-truncate fs-2">
|
||||||
|
{{ ucwords(str_replace('_', ' ', $rootNode['root'])) }}
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-white text-success fw-bold px-3 py-2 rounded-pill shadow-sm font-monospace fs-6">
|
||||||
|
<i class="fas fa-chart-line me-1"></i> Gain Ratio: {{ number_format($rootNode['gain'], 4) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ringkasan Statistik Data & Gain Ratio -->
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<!-- Distribusi Target -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 h-100 overflow-hidden">
|
||||||
|
<div class="card-header bg-white py-3 px-4 border-bottom d-flex align-items-center justify-content-between">
|
||||||
|
<h6 class="fw-bold text-dark mb-0 d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-chart-pie text-primary"></i> Distribusi Target (Jurusan)
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-light text-secondary rounded-pill border px-2 py-1 small">{{ count($jumlahJurusan) }} Kelas</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="bg-light-subtle text-muted small text-uppercase" style="font-size: 11px;">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4 py-3">Jurusan Target</th>
|
||||||
|
<th class="text-center pe-4 py-3">Jumlah</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="border-top-0">
|
||||||
|
@foreach($jumlahJurusan as $item)
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 fw-semibold text-dark">
|
||||||
|
<i class="fas fa-graduation-cap text-muted me-2 opacity-50"></i>{{ $item->jurusan }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center pe-4">
|
||||||
|
<span class="badge bg-primary-subtle text-primary border border-primary-subtle px-3 py-1 rounded-pill fw-bold font-monospace">
|
||||||
|
{{ $item->jumlah }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Peringkat Gain Ratio -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 h-100 overflow-hidden">
|
||||||
|
<div class="card-header bg-white py-3 px-4 border-bottom d-flex align-items-center justify-content-between">
|
||||||
|
<h6 class="fw-bold text-dark mb-0 d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-list-ol text-primary"></i> Peringkat Gain Ratio Kriteria (Root Node)
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-success-subtle text-success border border-success-subtle rounded-pill px-3 py-1 small">
|
||||||
|
<i class="fas fa-check-circle me-1"></i>Akar Terpilih
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="bg-light-subtle text-muted small text-uppercase" style="font-size: 11px;">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4 py-3">Atribut / Kriteria</th>
|
||||||
|
<th class="text-center py-3">Info Gain</th>
|
||||||
|
<th class="text-center py-3">Split Info</th>
|
||||||
|
<th class="text-center py-3">Gain Ratio</th>
|
||||||
|
<th class="text-center pe-4 py-3">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="border-top-0">
|
||||||
|
@foreach($rootNode['semua_gain'] as $atribut => $gainRatio)
|
||||||
|
<tr class="{{ $atribut == $rootNode['root'] ? 'table-success-subtle fw-semibold' : '' }}">
|
||||||
|
<td class="ps-4">
|
||||||
|
@if($atribut == $rootNode['root'])
|
||||||
|
<i class="fas fa-star text-warning me-2"></i>
|
||||||
|
@else
|
||||||
|
<i class="far fa-circle text-muted me-2 opacity-50"></i>
|
||||||
|
@endif
|
||||||
|
<span class="{{ $atribut == $rootNode['root'] ? 'text-success fw-bold' : 'text-dark' }}">
|
||||||
|
{{ ucfirst(str_replace('_', ' ', $atribut)) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center font-monospace text-muted">
|
||||||
|
@if($atribut == 'nilai') {{ number_format($gainNilai, 4) }}
|
||||||
|
@elseif($atribut == 'minat') {{ number_format($gainMinat, 4) }}
|
||||||
|
@elseif($atribut == 'mapel_favorit') {{ number_format($gainMapelFavorit ?? 0, 4) }}
|
||||||
|
@elseif($atribut == 'cita_cita') {{ number_format($gainCita, 4) }}
|
||||||
|
@else {{ number_format($gainPreferensi, 4) }} @endif
|
||||||
|
</td>
|
||||||
|
<td class="text-center font-monospace text-muted">
|
||||||
|
@if($atribut == 'nilai') {{ number_format($splitNilai, 4) }}
|
||||||
|
@elseif($atribut == 'minat') {{ number_format($splitMinat, 4) }}
|
||||||
|
@elseif($atribut == 'mapel_favorit') {{ number_format($splitMapelFavorit ?? 0, 4) }}
|
||||||
|
@elseif($atribut == 'cita_cita') {{ number_format($splitCita, 4) }}
|
||||||
|
@else {{ number_format($splitPreferensi, 4) }} @endif
|
||||||
|
</td>
|
||||||
|
<td class="text-center font-monospace fw-bold {{ $atribut == $rootNode['root'] ? 'text-success fs-6' : 'text-dark' }}">
|
||||||
|
{{ number_format($gainRatio, 4) }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center pe-4">
|
||||||
|
@if($atribut == $rootNode['root'])
|
||||||
|
<span class="badge bg-success text-white rounded-pill px-3 py-1 shadow-sm">
|
||||||
|
<i class="fas fa-check me-1"></i> Root Node
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-light text-secondary border rounded-pill px-3 py-1">Cabang</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VISUALISASI DECISION TREE C4.5 -->
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4 overflow-hidden">
|
||||||
|
<div class="card-header bg-white py-3 px-4 border-bottom d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="fw-bold text-dark mb-0 d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-project-diagram text-primary"></i> Visualisasi Pohon Keputusan (Decision Tree C4.5)
|
||||||
|
</h6>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="badge bg-primary-subtle text-primary border border-primary-subtle rounded-pill px-3 py-1">
|
||||||
|
<i class="fas fa-eye me-1"></i> Interactive View
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4 bg-light-subtle">
|
||||||
|
<div class="tree-dosen-container p-3 rounded-4 bg-white border shadow-sm overflow-auto">
|
||||||
|
<div class="tree-dosen">
|
||||||
|
<ul class="mb-0 ps-0">
|
||||||
|
<li class="list-unstyled">
|
||||||
|
{{-- PANGGILAN PERTAMA DARI ROOT NODE --}}
|
||||||
|
@include('c45._tree_node', ['tree' => $tree, 'isRoot' => true])
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Accordion Subset Cabang & Iterasi Rekursif (Semua Node) -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
|
<h5 class="fw-bold text-dark mb-0 d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-layer-group text-primary"></i> Detail Iterasi Rekursif & Subset Cabang
|
||||||
|
</h5>
|
||||||
|
<span class="badge bg-secondary-subtle text-secondary border rounded-pill px-3 py-1">
|
||||||
|
{{ count($detailSubsets) }} Level Node Terbentuk
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach($detailSubsets as $nodeIdx => $nodeInfo)
|
||||||
|
<div class="card shadow-sm rounded-4 mb-3 overflow-hidden border-start border-primary" style="border-left-width: 4px !important;">
|
||||||
|
<div class="card-header bg-white py-3 px-4 border-bottom d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-2">
|
||||||
|
<h6 class="fw-bold text-dark mb-0 d-flex align-items-center gap-2">
|
||||||
|
<span class="badge bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 26px; height: 26px;">
|
||||||
|
{{ $nodeInfo['level'] }}
|
||||||
|
</span>
|
||||||
|
<span>Pembagian Atribut:</span>
|
||||||
|
<span class="badge bg-primary-subtle text-primary border border-primary-subtle px-3 py-1 rounded-pill">
|
||||||
|
{{ strtoupper(str_replace('_', ' ', $nodeInfo['split_by'])) }}
|
||||||
|
</span>
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle rounded-pill px-3 py-1">
|
||||||
|
<i class="fas fa-route me-1"></i>Jalur: {{ $nodeInfo['path_label'] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-3 bg-light-subtle">
|
||||||
|
<div class="accordion accordion-flush" id="accordionSubset-{{ $nodeIdx }}">
|
||||||
|
@foreach($nodeInfo['branches'] as $branchIdx => $branch)
|
||||||
|
@php $accordionId = "collapse-" . $nodeIdx . "-" . $loop->index; @endphp
|
||||||
|
<div class="accordion-item border-0 rounded-3 mb-2 shadow-sm overflow-hidden">
|
||||||
|
<h2 class="accordion-header" id="heading-{{ $accordionId }}">
|
||||||
|
<button class="accordion-button {{ ! $loop->first ? 'collapsed' : '' }} bg-white py-3 px-3 fw-semibold"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#{{ $accordionId }}"
|
||||||
|
aria-expanded="{{ $loop->first ? 'true' : 'false' }}">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center w-100 me-3 gap-2">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-code-branch text-primary"></i>
|
||||||
|
<span class="text-dark">
|
||||||
|
Cabang Nilai: <strong class="text-primary fs-6">{{ $branch['nilai_cabang'] }}</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 align-items-center">
|
||||||
|
<span class="badge bg-secondary-subtle text-secondary border rounded-pill px-2 py-1 font-monospace">
|
||||||
|
{{ $branch['jumlah_data'] }} Data
|
||||||
|
</span>
|
||||||
|
@if($branch['is_pure'])
|
||||||
|
<span class="badge bg-success-subtle text-success border border-success-subtle rounded-pill px-3 py-1">
|
||||||
|
<i class="fas fa-leaf me-1"></i> Leaf Node ({{ $branch['pemenang'] }})
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle rounded-pill px-3 py-1">
|
||||||
|
<i class="fas fa-split me-1"></i> Pemecah: {{ strtoupper(str_replace('_', ' ', $branch['pemenang'])) }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="{{ $accordionId }}"
|
||||||
|
class="accordion-collapse collapse {{ $loop->first ? 'show' : '' }}"
|
||||||
|
data-bs-parent="#accordionSubset-{{ $nodeIdx }}">
|
||||||
|
<div class="accordion-body bg-white p-4 border-top">
|
||||||
|
|
||||||
|
@if($branch['is_pure'])
|
||||||
|
<div class="alert alert-success border-0 bg-success-subtle text-success-emphasis rounded-3 d-flex align-items-center mb-3">
|
||||||
|
<i class="fas fa-check-circle fs-4 me-3 text-success"></i>
|
||||||
|
<div>
|
||||||
|
<strong>Node Murni (Pure Leaf Node):</strong> Seluruh {{ $branch['jumlah_data'] }} data mengarah pada jurusan <strong class="text-decoration-underline">{{ $branch['pemenang'] }}</strong> (Entropy = 0).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<h6 class="fw-bold text-dark mb-2 d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-calculator text-primary"></i> Kalkulasi Atribut Tersisa:
|
||||||
|
</h6>
|
||||||
|
<div class="table-responsive mb-4">
|
||||||
|
<table class="table table-sm table-hover border align-middle mb-0">
|
||||||
|
<thead class="bg-light-subtle text-muted small text-uppercase" style="font-size: 11px;">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-3 py-2">Atribut Tersisa</th>
|
||||||
|
<th class="text-center py-2">Information Gain</th>
|
||||||
|
<th class="text-center py-2">Split Info</th>
|
||||||
|
<th class="text-center py-2">Gain Ratio</th>
|
||||||
|
<th class="text-center pe-3 py-2">Keputusan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($branch['kalkulasi'] as $calcIdx => $calc)
|
||||||
|
<tr class="{{ $calcIdx === 0 ? 'table-warning-subtle fw-bold' : '' }}">
|
||||||
|
<td class="ps-3">
|
||||||
|
@if($calcIdx === 0)
|
||||||
|
<i class="fas fa-star text-warning me-1"></i>
|
||||||
|
@endif
|
||||||
|
{{ strtoupper(str_replace('_', ' ', $calc['atribut'])) }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center font-monospace">{{ number_format($calc['gain'], 4) }}</td>
|
||||||
|
<td class="text-center font-monospace">{{ number_format($calc['split_info'], 4) }}</td>
|
||||||
|
<td class="text-center font-monospace fw-bold {{ $calcIdx === 0 ? 'text-warning-emphasis fs-6' : '' }}">{{ number_format($calc['gain_ratio'], 4) }}</td>
|
||||||
|
<td class="text-center pe-3">
|
||||||
|
@if($calcIdx === 0)
|
||||||
|
<span class="badge bg-warning text-dark border border-warning shadow-sm">Pemecah Cabang</span>
|
||||||
|
@else
|
||||||
|
<span class="text-muted small">Lanjutan</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||||
|
<h6 class="fw-bold text-secondary mb-0 small text-uppercase d-flex align-items-center gap-1">
|
||||||
|
<i class="fas fa-table text-muted"></i> Sampel Data Cabang Ini:
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-light text-muted border rounded-pill px-2 py-1 small">Total {{ count($branch['items']) }} baris</span>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive rounded-3 border">
|
||||||
|
<table class="table table-sm table-striped align-middle mb-0">
|
||||||
|
<thead class="bg-light text-muted small text-uppercase" style="font-size: 10px;">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-3">Nilai</th>
|
||||||
|
<th>Minat</th>
|
||||||
|
<th>Mapel Favorit</th>
|
||||||
|
<th>Cita-cita</th>
|
||||||
|
<th>Preferensi</th>
|
||||||
|
<th class="text-success pe-3">Target Jurusan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="small">
|
||||||
|
@foreach($branch['items'] as $d)
|
||||||
|
<tr>
|
||||||
|
<td class="ps-3">{{ is_object($d) ? $d->nilai : $d['nilai'] }}</td>
|
||||||
|
<td>{{ is_object($d) ? $d->minat : $d['minat'] }}</td>
|
||||||
|
<td>{{ is_object($d) ? ($d->mapel_favorit ?? $d['mapel_favorit'] ?? '-') : ($d['mapel_favorit'] ?? '-') }}</td>
|
||||||
|
<td>{{ is_object($d) ? $d->cita_cita : $d['cita_cita'] }}</td>
|
||||||
|
<td>{{ is_object($d) ? $d->preferensi_studi : $d['preferensi_studi'] }}</td>
|
||||||
|
<td class="fw-bold text-success pe-3">
|
||||||
|
<i class="fas fa-check-circle me-1 small"></i>{{ is_object($d) ? $d->jurusan : $d['jurusan'] }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Visualisasi Decision Rules (IF-THEN) -->
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-5 overflow-hidden">
|
||||||
|
<div class="card-header bg-white py-3 px-4 border-bottom d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="fw-bold text-dark mb-0 d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-scroll text-primary"></i> Aturan Keputusan Sistem (Decision Rules)
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-primary rounded-pill px-3 py-1 font-monospace">{{ count($rules) }} Aturan Terbentuk</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4 bg-light-subtle">
|
||||||
|
<div class="row g-3">
|
||||||
|
@foreach($rules as $ruleIdx => $rule)
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow-sm rounded-4 mb-3 overflow-hidden border-start border-primary" style="border-left-width: 4px !important;">
|
||||||
|
<div class="card-body p-3 bg-white d-flex align-items-center justify-content-between">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<span class="badge bg-primary text-white rounded-circle d-flex align-items-center justify-content-center shadow-sm" style="width: 32px; height: 32px; font-size: 13px;">
|
||||||
|
{{ $ruleIdx + 1 }}
|
||||||
|
</span>
|
||||||
|
<div class="font-monospace fs-6 text-dark lh-base">
|
||||||
|
{!! str_replace(
|
||||||
|
['IF', 'THEN', 'AND'],
|
||||||
|
['<span class="badge bg-dark px-2 py-1 me-1 shadow-sm">IF</span>', '<span class="badge bg-success px-2 py-1 mx-1 shadow-sm">THEN</span>', '<span class="badge bg-secondary px-2 py-1 mx-1 shadow-sm">AND</span>'],
|
||||||
|
is_array($rule) ? implode(' ', $rule) : $rule
|
||||||
|
) !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
var btnMulai = document.querySelector('[data-bs-target="#modalCekRekomendasi"]');
|
||||||
|
if (btnMulai) {
|
||||||
|
btnMulai.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var modalEl = document.getElementById('modalCekRekomendasi');
|
||||||
|
var myModal = new bootstrap.Modal(modalEl);
|
||||||
|
myModal.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||||
|
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
|
|
@ -0,0 +1,7 @@
|
||||||
|
@props(['status'])
|
||||||
|
|
||||||
|
@if ($status)
|
||||||
|
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
|
||||||
|
{{ $status }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</button>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white'])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$alignmentClasses = match ($align) {
|
||||||
|
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
|
||||||
|
'top' => 'origin-top',
|
||||||
|
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
|
||||||
|
};
|
||||||
|
|
||||||
|
$width = match ($width) {
|
||||||
|
'48' => 'w-48',
|
||||||
|
default => $width,
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
|
||||||
|
<div @click="open = ! open">
|
||||||
|
{{ $trigger }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="open"
|
||||||
|
x-transition:enter="transition ease-out duration-200"
|
||||||
|
x-transition:enter-start="opacity-0 scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 scale-100"
|
||||||
|
x-transition:leave="transition ease-in duration-75"
|
||||||
|
x-transition:leave-start="opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 scale-95"
|
||||||
|
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
|
||||||
|
style="display: none;"
|
||||||
|
@click="open = false">
|
||||||
|
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
|
||||||
|
{{ $content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
@props(['messages'])
|
||||||
|
|
||||||
|
@if ($messages)
|
||||||
|
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 space-y-1']) }}>
|
||||||
|
@foreach ((array) $messages as $message)
|
||||||
|
<li>{{ $message }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@endif
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
@props(['value'])
|
||||||
|
|
||||||
|
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
|
||||||
|
{{ $value ?? $slot }}
|
||||||
|
</label>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue