update sistem analisis sentimen

This commit is contained in:
MufridaFaraDiani27 2026-05-12 15:41:15 +07:00
parent 67156e700a
commit 638ae703c0
14 changed files with 1594 additions and 152 deletions

View File

@ -9,7 +9,6 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View; use Illuminate\View\View;
class RegisteredUserController extends Controller class RegisteredUserController extends Controller
@ -30,9 +29,40 @@ public function create(): View
public function store(Request $request): RedirectResponse public function store(Request $request): RedirectResponse
{ {
$request->validate([ $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 'name' => [
'password' => ['required', 'confirmed', Rules\Password::defaults()], 'required',
'string',
'max:255'
],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
'unique:users'
],
'password' => [
'required',
'confirmed',
'string',
'min:8',
'regex:/[a-z]/',
'regex:/[A-Z]/',
'regex:/[0-9]/',
],
], [
'password.min' => 'Password wajib minimal 8 karakter.',
'password.regex' => 'Password harus mengandung huruf besar, huruf kecil, dan angka.',
'password.confirmed' => 'Konfirmasi password tidak sesuai.',
]); ]);
$user = User::create([ $user = User::create([
@ -47,4 +77,4 @@ public function store(Request $request): RedirectResponse
return redirect(route('dashboard', absolute: false)); return redirect(route('dashboard', absolute: false));
} }
} }

View File

@ -0,0 +1,123 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class UserController extends Controller
{
public function index(Request $request)
{
$search = $request->search;
$users = User::when($search, function ($query) use ($search) {
$query->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
})->paginate(5);
return view('kelolauser.index', compact('users'));
}
/*
|--------------------------------------------------------------------------
| UPDATE USER
|--------------------------------------------------------------------------
*/
public function update(Request $request, int $id)
{
$request->validate([
'name' => 'required',
'email' => 'required|email',
'role' => 'required',
]);
$user = User::findOrFail($id);
$user->update([
'name' => $request->name,
'email' => $request->email,
'role' => $request->role,
]);
return redirect()
->back()
->with('success', 'Pengguna berhasil diperbarui.');
}
/*
|--------------------------------------------------------------------------
| RESET PASSWORD
|--------------------------------------------------------------------------
*/
public function updatePassword(Request $request, int $id)
{
$request->validate([
'password' => [
'required',
'confirmed',
'min:8',
'regex:/[a-z]/',
'regex:/[A-Z]/',
'regex:/[0-9]/',
],
], [
'password.min' =>
'Password minimal 8 karakter.',
'password.regex' =>
'Password harus mengandung huruf besar, huruf kecil, dan angka.',
]);
$user = User::findOrFail($id);
$user->update([
'password' => Hash::make($request->password)
]);
return redirect()
->back()
->with('success', 'Password berhasil direset.');
}
/*
|--------------------------------------------------------------------------
| DELETE USER
|--------------------------------------------------------------------------
*/
public function destroy(int $id)
{
User::findOrFail($id)->delete();
return redirect()
->back()
->with('success', 'Pengguna berhasil dihapus.');
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'role' => 'required|in:admin,staff',
'password' => 'required|min:8|confirmed',
]);
User::create([
'name' => $request->name,
'email' => $request->email,
'role' => $request->role,
'password' => bcrypt($request->password),
]);
return redirect()->route('kelola-pengguna.index')
->with('success', 'Pengguna berhasil ditambahkan.');
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; // ← tambah ini
class RoleMiddleware
{
public function handle(Request $request, Closure $next, string $role)
{
if (!Auth::check() || Auth::user()->role !== $role) // ← ganti auth() ke Auth::
{
abort(403, 'Akses ditolak.');
}
return $next($request);
}
}

View File

@ -2,42 +2,26 @@
namespace App\Models; namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
class User extends Authenticatable class User extends Authenticatable
{ {
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable; use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [ protected $fillable = [
'name', 'name',
'email', 'email',
'password', 'password',
'role', // ← tambah ini
]; ];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [ protected $hidden = [
'password', 'password',
'remember_token', 'remember_token',
]; ];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array protected function casts(): array
{ {
return [ return [
@ -45,4 +29,15 @@ protected function casts(): array
'password' => 'hashed', 'password' => 'hashed',
]; ];
} }
}
// ← tambah 2 method ini di bawah casts()
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function isStaff(): bool
{
return $this->role === 'staff';
}
}

View File

@ -10,9 +10,11 @@
commands: __DIR__.'/../routes/console.php', commands: __DIR__.'/../routes/console.php',
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware) {
// $middleware->alias([
'role' => \App\Http\Middleware\RoleMiddleware::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// //
})->create(); })->create();

View File

@ -0,0 +1,25 @@
<?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::table('users', function (Blueprint $table) {
$table->enum('role', ['admin', 'staff'])->default('staff')->after('email');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role');
});
}
};

8
package-lock.json generated
View File

@ -1,5 +1,5 @@
{ {
"name": "TugasAkhirFarah-main", "name": "SistemAnalisisSentimen",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@ -1418,6 +1418,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@ -2690,6 +2691,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@ -3114,6 +3116,7 @@
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",
@ -3234,6 +3237,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -3322,6 +3326,7 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@ -3426,6 +3431,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },

View File

@ -1,25 +1,559 @@
<x-guest-layout> <!DOCTYPE html>
<div class="mb-4 text-sm text-gray-600"> <html lang="id">
{{ __('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.') }} <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lupa Password - SENTARA</title>
@vite(['resources/css/app.css','resources/js/app.js'])
<style>
body{
background:#f5f7fb;
font-family:'Poppins', sans-serif;
}
.main-wrapper{
min-height:100vh;
padding:40px 20px;
position:relative;
overflow:hidden;
display:flex;
align-items:center;
justify-content:center;
}
/* BACKGROUND */
.circle-top{
position:absolute;
top:-120px;
right:-120px;
width:350px;
height:350px;
background:#edf3ff;
border-radius:50%;
z-index:0;
}
.circle-bottom{
position:absolute;
bottom:-150px;
left:-150px;
width:320px;
height:320px;
background:#edf3ff;
border-radius:50%;
z-index:0;
}
.content{
position:relative;
z-index:2;
width:100%;
max-width:950px;
}
/* CARD */
.card-box{
background:#fff;
border-radius:26px;
padding:50px;
box-shadow:0 10px 35px rgba(0,0,0,0.06);
}
/* ICON */
.icon-wrapper{
width:110px;
height:110px;
background:#edf3ff;
border-radius:50%;
display:flex;
align-items:center;
justify-content:center;
margin:0 auto 25px;
}
/* TITLE */
.title{
text-align:center;
font-size:54px;
font-weight:700;
color:#0d1b52;
line-height:1.2;
}
.subtitle{
text-align:center;
font-size:22px;
color:#475569;
margin-top:18px;
line-height:1.7;
}
.subtitle span{
color:#2563eb;
font-weight:700;
}
/* ALERT */
.info-box{
margin-top:35px;
background:#f4f8ff;
border:1px solid #dbeafe;
border-radius:18px;
padding:22px 24px;
display:flex;
align-items:flex-start;
gap:16px;
}
.info-box p{
color:#334155;
font-size:18px;
line-height:1.6;
}
/* SECTION */
.section-title{
display:flex;
align-items:center;
gap:12px;
margin-top:45px;
margin-bottom:30px;
}
.section-title h2{
font-size:34px;
font-weight:700;
color:#0d1b52;
}
/* STEP */
.step{
display:flex;
gap:20px;
margin-bottom:28px;
}
.step-number{
min-width:52px;
height:52px;
background:#2563eb;
color:#fff;
border-radius:50%;
display:flex;
align-items:center;
justify-content:center;
font-weight:700;
font-size:22px;
}
.step-content h3{
font-size:28px;
font-weight:700;
color:#0f172a;
margin-bottom:6px;
}
.step-content p{
color:#475569;
font-size:20px;
line-height:1.7;
}
/* CONTACT */
.contact-box{
margin-top:40px;
background:#f8fbff;
border:1px solid #e2e8f0;
border-radius:22px;
padding:32px;
}
.contact-header{
display:flex;
align-items:center;
gap:18px;
margin-bottom:18px;
}
.contact-icon{
width:70px;
height:70px;
background:#edf3ff;
border-radius:50%;
display:flex;
align-items:center;
justify-content:center;
}
.contact-header h3{
font-size:30px;
font-weight:700;
color:#2563eb;
}
.contact-header p{
font-size:18px;
color:#475569;
margin-top:4px;
}
.contact-list{
display:flex;
flex-wrap:wrap;
gap:24px;
align-items:center;
margin-top:18px;
padding-left:88px;
}
.contact-item{
display:flex;
align-items:center;
gap:12px;
font-size:22px;
font-weight:600;
color:#0f172a;
}
/* FOOTER */
.footer{
display:flex;
justify-content:space-between;
margin-top:28px;
color:#64748b;
font-size:18px;
padding:0 8px;
}
/* MOBILE */
@media(max-width:768px){
.main-wrapper{
padding:20px 14px;
}
.card-box{
padding:28px 22px;
border-radius:22px;
}
.icon-wrapper{
width:90px;
height:90px;
}
.title{
font-size:36px;
}
.subtitle{
font-size:17px;
line-height:1.7;
}
.info-box{
padding:18px;
}
.info-box p{
font-size:15px;
}
.section-title h2{
font-size:25px;
}
.step{
gap:14px;
}
.step-number{
min-width:42px;
height:42px;
font-size:18px;
}
.step-content h3{
font-size:21px;
}
.step-content p{
font-size:15px;
}
.contact-box{
padding:22px;
}
.contact-header{
align-items:flex-start;
}
.contact-header h3{
font-size:24px;
}
.contact-header p{
font-size:15px;
}
.contact-list{
padding-left:0;
flex-direction:column;
align-items:flex-start;
gap:18px;
}
.contact-item{
font-size:16px;
word-break:break-word;
}
.footer{
flex-direction:column;
gap:10px;
text-align:center;
font-size:14px;
}
}
</style>
</head>
<body>
<div class="main-wrapper">
<div class="circle-top"></div>
<div class="circle-bottom"></div>
<div class="content">
<!-- CARD -->
<div class="card-box">
<!-- ICON -->
<div class="icon-wrapper">
<svg xmlns="http://www.w3.org/2000/svg"
width="55"
height="55"
fill="none"
viewBox="0 0 24 24"
stroke="#2563eb"
stroke-width="2">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M16 11V7a4 4 0 10-8 0v4m-2 0h12a2 2 0 012 2v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5a2 2 0 012-2z"/>
<circle cx="18.5" cy="18.5" r="3.5" stroke="#2563eb"/>
<path d="M18.5 17v3M17 18.5h3" stroke="#2563eb"/>
</svg>
</div>
<!-- TITLE -->
<h1 class="title">
Lupa Password?
</h1>
<p class="subtitle">
Untuk keamanan akun, pengaturan ulang password hanya dapat dilakukan
<br>
<span>oleh Administrator.</span>
</p>
<!-- INFO -->
<div class="info-box">
<svg xmlns="http://www.w3.org/2000/svg"
width="34"
height="34"
fill="none"
viewBox="0 0 24 24"
stroke="#2563eb"
stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 16v-4"/>
<path d="M12 8h.01"/>
</svg>
<p>
Silakan hubungi Administrator sistem untuk memperbarui password akun Anda.
</p>
</div>
<!-- SECTION TITLE -->
<div class="section-title">
<svg xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
fill="none"
viewBox="0 0 24 24"
stroke="#2563eb"
stroke-width="2">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M17 20h5V4H2v16h5"/>
<path stroke-linecap="round"
stroke-linejoin="round"
d="M9 20h6"/>
<circle cx="12" cy="8" r="3"/>
<path d="M7 16c1.333-2 3-3 5-3s3.667 1 5 3"/>
</svg>
<h2>Cara Memperbarui Password</h2>
</div>
<!-- STEP -->
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h3>Hubungi Administrator</h3>
<p>
Sampaikan kepada Administrator bahwa Anda lupa password akun SENTARA.
</p>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h3>Verifikasi Identitas</h3>
<p>
Administrator akan memverifikasi identitas Anda sebagai pengguna sistem.
</p>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<h3>Password Baru</h3>
<p>
Administrator akan membuat password baru dan memberikannya kepada Anda.
</p>
</div>
</div>
<!-- CONTACT -->
<div class="contact-box">
<div class="contact-header">
<div class="contact-icon">
<svg xmlns="http://www.w3.org/2000/svg"
width="38"
height="38"
fill="none"
viewBox="0 0 24 24"
stroke="#2563eb"
stroke-width="2">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4-.917L2 17l1.083-3.25A6.963 6.963 0 012 10c0-3.866 3.582-7 8-7s8 3.134 8 7z"/>
<path d="M15 8h.01"/>
<path d="M9 8h.01"/>
<path d="M8 12c1 1 2 1.5 4 1.5s3-.5 4-1.5"/>
</svg>
</div>
<div>
<h3>Kontak Administrator</h3>
<p>
Silakan hubungi administrator melalui kontak resmi berikut:
</p>
</div>
</div>
<div class="contact-list">
<!-- PHONE -->
<div class="contact-item">
<svg xmlns="http://www.w3.org/2000/svg"
width="26"
height="26"
fill="none"
viewBox="0 0 24 24"
stroke="#2563eb"
stroke-width="2">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M3 5a2 2 0 012-2h2.28a2 2 0 011.94 1.515l.57 2.28a2 2 0 01-.45 1.91l-1.27 1.27a16 16 0 006.59 6.59l1.27-1.27a2 2 0 011.91-.45l2.28.57A2 2 0 0121 16.72V19a2 2 0 01-2 2h-1C9.163 21 3 14.837 3 7V5z"/>
</svg>
0881-0267-1527
</div>
<!-- EMAIL -->
<div class="contact-item">
<svg xmlns="http://www.w3.org/2000/svg"
width="26"
height="26"
fill="none"
viewBox="0 0 24 24"
stroke="#2563eb"
stroke-width="2">
<path stroke-linecap="round"
stroke-linejoin="round"
d="M4 4h16v16H4z"/>
<path stroke-linecap="round"
stroke-linejoin="round"
d="M22 6l-10 7L2 6"/>
</svg>
e31231226@student.polije.ac.id
</div>
</div>
</div>
</div>
<!-- FOOTER -->
<div class="footer">
<div>© 2025 SENTARA. All rights reserved.</div>
<div>Dinas Pariwisata Jember</div>
</div>
</div> </div>
<!-- Session Status --> </div>
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('password.email') }}"> </body>
@csrf </html>
<!-- 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>

View File

@ -0,0 +1,432 @@
@extends('layouts.sentara')
@section('content')
<div class="space-y-6">
{{-- ALERT SUCCESS --}}
@if(session('success'))
<div class="bg-green-50 border border-green-200 text-green-700 px-5 py-4 rounded-2xl">
{{ session('success') }}
</div>
@endif
{{-- ALERT ERROR --}}
@if(session('error'))
<div class="bg-red-50 border border-red-200 text-red-700 px-5 py-4 rounded-2xl">
{{ session('error') }}
</div>
@endif
<!-- HEADER -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 class="text-3xl font-bold text-slate-800">Kelola Pengguna</h1>
<p class="text-slate-500 mt-1">Kelola akun pengguna sistem SENTARA.</p>
</div>
<!-- BUTTON TAMBAH -->
<button
onclick="openTambahModal()"
class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-3 rounded-2xl text-sm font-semibold shadow transition">
+ Tambah Pengguna
</button>
</div>
<!-- CARD -->
<div class="bg-white rounded-3xl shadow-sm border border-slate-100 p-5">
<!-- TABLE -->
<div class="overflow-x-auto mt-6">
<table class="w-full text-sm min-w-[900px]">
<thead>
<tr class="text-slate-500 border-b">
<th class="py-4 text-left">No</th>
<th class="py-4 text-left">Nama</th>
<th class="py-4 text-left">Email</th>
<th class="py-4 text-left">Role</th>
<th class="py-4 text-left">Status</th>
<th class="py-4 text-left">Terakhir Aktif</th>
<th class="py-4 text-center">Aksi</th>
</tr>
</thead>
<tbody class="text-slate-700">
@forelse ($users as $user)
<tr class="border-b hover:bg-slate-50 transition">
<td class="py-4">{{ $loop->iteration }}</td>
<td class="py-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-slate-200 flex items-center justify-center text-xs font-bold text-slate-600">
{{ strtoupper(substr($user->name, 0, 2)) }}
</div>
<span class="font-medium">{{ $user->name }}</span>
</div>
</td>
<td class="py-4">{{ $user->email }}</td>
<td class="py-4">
@if($user->role == 'admin')
<span class="bg-purple-100 text-purple-700 px-3 py-1 rounded-full text-xs font-semibold">Admin</span>
@else
<span class="bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-xs font-semibold">Staff</span>
@endif
</td>
<td class="py-4">
<span class="bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-semibold">Aktif</span>
</td>
<td class="py-4">{{ $user->updated_at->format('d M Y, H:i') }}</td>
<td class="py-4">
<div class="flex justify-center gap-2">
<button
onclick="openEditModal('{{ $user->id }}', '{{ addslashes($user->name) }}', '{{ $user->email }}', '{{ $user->role }}')"
class="w-11 h-11 rounded-2xl border border-blue-200 text-blue-600 hover:bg-blue-50 flex items-center justify-center transition"
title="Edit Pengguna">✏️</button>
<button
onclick="openPasswordModal('{{ $user->id }}')"
class="w-11 h-11 rounded-2xl border border-amber-200 text-amber-500 hover:bg-amber-50 flex items-center justify-center transition"
title="Reset Password">🔒</button>
<button
onclick="openDeleteModal('{{ $user->id }}', '{{ $user->role }}')"
class="w-11 h-11 rounded-2xl border border-red-200 text-red-500 hover:bg-red-50 flex items-center justify-center transition"
title="Hapus Pengguna">🗑️</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="py-10 text-center text-slate-400">
Data pengguna belum tersedia.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- PAGINATION -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mt-6">
<p class="text-sm text-slate-500">
Menampilkan {{ $users->firstItem() ?? 0 }} - {{ $users->lastItem() ?? 0 }}
dari {{ $users->total() }} pengguna
</p>
{{ $users->links() }}
</div>
</div>
</div>
<!-- ================= MODAL TAMBAH PENGGUNA ================= -->
<div id="tambahModal"
class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center px-4">
<div class="bg-white rounded-3xl p-6 shadow-2xl w-full" style="max-width: 480px;">
<div class="flex items-start justify-between mb-5">
<div>
<h2 class="text-2xl font-bold text-slate-800">Tambah Pengguna</h2>
<p class="text-slate-500 text-sm mt-1">Buat akun pengguna baru untuk sistem SENTARA.</p>
</div>
<button onclick="closeTambahModal()"
class="text-slate-400 hover:text-slate-700 text-2xl leading-none ml-4 flex-shrink-0">×</button>
</div>
{{-- SESUDAH --}}
<form method="POST" action="/kelola-pengguna" id="tambahForm">
@csrf
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Nama Pengguna</label>
<input type="text" name="name" placeholder="Masukkan nama lengkap"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" required>
</div>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Email</label>
<input type="email" name="email" placeholder="contoh@email.com"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" required>
</div>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Role Pengguna</label>
<select name="role"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none">
<option value="staff">Staff</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Password</label>
<input type="password" name="password" placeholder="Minimal 8 karakter"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" required>
</div>
<div class="mb-6">
<label class="block text-sm font-semibold text-slate-700 mb-2">Konfirmasi Password</label>
<input type="password" name="password_confirmation" placeholder="Ulangi password"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" required>
</div>
<div class="flex gap-3">
<button type="button" onclick="closeTambahModal()"
class="flex-1 py-3 rounded-2xl border border-slate-200 text-slate-600 text-sm font-medium hover:bg-slate-100 transition">
Batal
</button>
<button type="submit"
class="flex-1 py-3 rounded-2xl bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold transition">
Tambah
</button>
</div>
</form>
</div>
</div>
<!-- ================= MODAL EDIT ================= -->
<div id="editModal"
class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center px-4">
<div class="bg-white rounded-3xl p-6 shadow-2xl w-full" style="max-width: 480px;">
<div class="flex items-start justify-between mb-5">
<div>
<h2 class="text-2xl font-bold text-slate-800">Edit Pengguna</h2>
<p class="text-slate-500 text-sm mt-1">Perbarui informasi akun pengguna SENTARA.</p>
</div>
<button onclick="closeEditModal()"
class="text-slate-400 hover:text-slate-700 text-2xl leading-none ml-4 flex-shrink-0">×</button>
</div>
<form method="POST" id="editForm">
@csrf
@method('PUT')
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Nama Pengguna</label>
<input type="text" name="name" id="editName"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none">
</div>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Email</label>
<input type="email" name="email" id="editEmail"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none">
</div>
<div class="mb-6">
<label class="block text-sm font-semibold text-slate-700 mb-2">Role Pengguna</label>
<select name="role" id="editRole"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none">
<option value="admin">Admin</option>
<option value="staff">Staff</option>
</select>
</div>
<div class="flex gap-3">
<button type="button" onclick="closeEditModal()"
class="flex-1 py-3 rounded-2xl border border-slate-200 text-slate-600 text-sm font-medium hover:bg-slate-100 transition">
Batal
</button>
<button type="submit"
class="flex-1 py-3 rounded-2xl bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold transition">
Simpan
</button>
</div>
</form>
</div>
</div>
<!-- ================= MODAL RESET PASSWORD ================= -->
<div id="passwordModal"
class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center px-4">
<div class="bg-white rounded-3xl p-6 shadow-2xl w-full" style="max-width: 480px;">
<div class="flex items-start justify-between mb-5">
<div>
<h2 class="text-2xl font-bold text-slate-800">Reset Password</h2>
<p class="text-slate-500 text-sm mt-1">Buat password baru pengguna.</p>
</div>
<button onclick="closePasswordModal()"
class="text-slate-400 hover:text-slate-700 text-2xl leading-none ml-4 flex-shrink-0">×</button>
</div>
<div class="mb-5 bg-amber-50 border border-amber-200 rounded-2xl p-4">
<p class="text-sm text-amber-700 leading-relaxed">
Password minimal 8 karakter dan harus kombinasi huruf besar, huruf kecil, dan angka.
</p>
</div>
<form method="POST" id="passwordForm">
@csrf
@method('PUT')
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Password Baru</label>
<input type="password" name="password"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-amber-400 focus:outline-none">
</div>
<div class="mb-6">
<label class="block text-sm font-semibold text-slate-700 mb-2">Konfirmasi Password</label>
<input type="password" name="password_confirmation"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-amber-400 focus:outline-none">
</div>
<div class="flex gap-3">
<button type="button" onclick="closePasswordModal()"
class="flex-1 py-3 rounded-2xl border border-slate-200 text-slate-600 text-sm font-medium hover:bg-slate-100 transition">
Batal
</button>
<button type="submit"
class="flex-1 py-3 rounded-2xl bg-amber-500 hover:bg-amber-600 text-white text-sm font-semibold transition">
Reset
</button>
</div>
</form>
</div>
</div>
<!-- ================= MODAL DELETE ================= -->
<div id="deleteModal"
class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center px-4">
<div class="bg-white rounded-3xl p-6 shadow-2xl w-full text-center" style="max-width: 400px;">
<div class="w-16 h-16 mx-auto rounded-full bg-red-100 flex items-center justify-center text-3xl mb-4">🗑️</div>
<h2 class="text-2xl font-bold text-slate-800">Hapus Pengguna?</h2>
<p class="text-slate-500 text-sm mt-2 leading-relaxed">
Data pengguna akan dihapus permanen dan tidak dapat dikembalikan.
</p>
<form method="POST" id="deleteForm" class="mt-6">
@csrf
@method('DELETE')
<div class="flex gap-3">
<button type="button" onclick="closeDeleteModal()"
class="flex-1 py-3 rounded-2xl border border-slate-200 text-slate-600 text-sm font-medium hover:bg-slate-100 transition">
Batal
</button>
<button type="submit"
class="flex-1 py-3 rounded-2xl bg-red-500 hover:bg-red-600 text-white text-sm font-semibold transition">
Hapus
</button>
</div>
</form>
</div>
</div>
<!-- ================= MODAL ADMIN TIDAK BISA DIHAPUS ================= -->
<div id="adminAlertModal"
class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center px-4">
<div class="bg-white rounded-3xl p-6 shadow-2xl w-full text-center" style="max-width: 400px;">
<div class="w-16 h-16 mx-auto rounded-full bg-amber-100 flex items-center justify-center text-3xl mb-4">⚠️</div>
<h2 class="text-2xl font-bold text-slate-800">Akses Ditolak</h2>
<p class="text-slate-500 text-sm mt-2 leading-relaxed">
Akun administrator tidak dapat dihapus demi menjaga keamanan sistem.
</p>
<button onclick="closeAdminAlertModal()"
class="w-full mt-6 py-3 rounded-2xl bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold transition">
Mengerti
</button>
</div>
</div>
<!-- ================= SCRIPT ================= -->
<script>
/* ---- TAMBAH ---- */
function openTambahModal() {
document.getElementById('tambahModal').classList.remove('hidden');
document.getElementById('tambahForm').reset();
}
function closeTambahModal() {
document.getElementById('tambahModal').classList.add('hidden');
}
/* ---- EDIT ---- */
function openEditModal(id, name, email, role) {
document.getElementById('editModal').classList.remove('hidden');
document.getElementById('editName').value = name;
document.getElementById('editEmail').value = email;
document.getElementById('editRole').value = role;
document.getElementById('editForm').action = '/kelola-pengguna/' + id;
}
function closeEditModal() {
document.getElementById('editModal').classList.add('hidden');
}
/* ---- RESET PASSWORD ---- */
function openPasswordModal(id) {
document.getElementById('passwordModal').classList.remove('hidden');
document.getElementById('passwordForm').action =
'/kelola-pengguna/' + id + '/reset-password';
}
function closePasswordModal() {
document.getElementById('passwordModal').classList.add('hidden');
}
/* ---- DELETE ---- */
function openDeleteModal(id, role) {
if (role === 'admin') {
document.getElementById('adminAlertModal').classList.remove('hidden');
return;
}
document.getElementById('deleteModal').classList.remove('hidden');
document.getElementById('deleteForm').action = '/kelola-pengguna/' + id;
}
function closeDeleteModal() {
document.getElementById('deleteModal').classList.add('hidden');
}
function closeAdminAlertModal() {
document.getElementById('adminAlertModal').classList.add('hidden');
}
</script>
@endsection

View File

@ -86,11 +86,27 @@ class="block hover:text-gray-300">
</header> </header>
@endisset @endisset
<!-- Page Content --> <!-- Page Content -->
<main class="p-6"> <main class="p-6">
@yield('content')
</main>
{{-- ALERT SUCCESS --}}
@if(session('success'))
<div class="mb-4 bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-2xl">
{{ session('success') }}
</div>
@endif
{{-- ALERT ERROR --}}
@if(session('error'))
<div class="mb-4 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-2xl">
{{ session('error') }}
</div>
@endif
@yield('content')
</main>
</div> </div>
</div> </div>
</div> </div>

View File

@ -7,12 +7,14 @@
@vite(['resources/css/app.css','resources/js/app.js']) @vite(['resources/css/app.css','resources/js/app.js'])
<!-- FEATHER ICON -->
<script src="https://unpkg.com/feather-icons"></script> <script src="https://unpkg.com/feather-icons"></script>
</head> </head>
<body class="bg-gray-100"> <body class="bg-gray-100 overflow-x-hidden">
<div class="min-h-screen"> <div class="min-h-screen flex">
<!-- SIDEBAR --> <!-- SIDEBAR -->
<aside id="sidebar" <aside id="sidebar"
@ -25,7 +27,8 @@ class="fixed top-0 left-0 h-full w-64
<!-- LOGO --> <!-- LOGO -->
<div class="flex justify-center mt-6 mb-8"> <div class="flex justify-center mt-6 mb-8">
<img src="{{ asset('images/tulisan-sentara.png') }}" <img src="{{ asset('images/tulisan-sentara.png') }}"
class="w-44"> class="w-44"
alt="Logo SENTARA">
</div> </div>
<!-- MENU --> <!-- MENU -->
@ -39,7 +42,9 @@ class="flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-blue-800 transition
<i data-feather="grid" class="w-5 h-5"></i> <i data-feather="grid" class="w-5 h-5"></i>
</div> </div>
<span class="text-[15px]">Dashboard Sentimen</span> <span class="text-[15px] leading-5">
Dashboard Sentimen
</span>
</a> </a>
<!-- DATA ULASAN --> <!-- DATA ULASAN -->
@ -50,7 +55,9 @@ class="flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-blue-800 transition
<i data-feather="file-text" class="w-5 h-5"></i> <i data-feather="file-text" class="w-5 h-5"></i>
</div> </div>
<span class="text-[15px]">Data Ulasan</span> <span class="text-[15px] leading-5">
Data Ulasan
</span>
</a> </a>
<!-- RIWAYAT --> <!-- RIWAYAT -->
@ -61,54 +68,103 @@ class="flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-blue-800 transition
<i data-feather="clock" class="w-5 h-5"></i> <i data-feather="clock" class="w-5 h-5"></i>
</div> </div>
<span class="text-[15px]">Riwayat Analisis</span> <span class="text-[15px] leading-5">
Riwayat Analisis
</span>
</a> </a>
<!-- KHUSUS ADMIN -->
@if(auth()->user()->role == 'admin')
<a href="{{ route('kelola-pengguna.index') }}"
class="flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-blue-800 transition">
<div class="w-10 h-10 flex items-center justify-center bg-blue-800 rounded-lg">
<i data-feather="users" class="w-5 h-5"></i>
</div>
<span class="text-[15px] leading-5">
Kelola Pengguna
</span>
</a>
@endif
</nav> </nav>
<!-- USER SECTION --> <!-- USER -->
<div class="px-6 pb-6"> <div class="px-6 pb-6">
<div class="relative"> <div class="relative">
<!-- USER BUTTON --> <!-- USER BUTTON -->
<button onclick="toggleUserMenu()" <button onclick="toggleUserMenu()"
class="w-full flex items-center gap-3 bg-blue-800 hover:bg-blue-900 px-4 py-3 rounded-xl transition"> class="w-full flex items-center gap-3
bg-blue-800 hover:bg-blue-900
px-4 py-3 rounded-2xl transition">
<!-- ICON -->
<div class="w-11 h-11 rounded-full bg-blue-700
flex items-center justify-center">
<i data-feather="user"
class="w-5 h-5"></i>
<div class="w-10 h-10 flex items-center justify-center bg-blue-700 rounded-full">
<i data-feather="user" class="w-5 h-5"></i>
</div> </div>
<div class="text-left"> <!-- TEXT -->
<p class="text-sm font-semibold"> <div class="text-left flex-1">
<p class="text-sm font-semibold capitalize">
{{ auth()->user()->name }} {{ auth()->user()->name }}
</p> </p>
<p class="text-xs text-blue-200">Staff</p>
<p class="text-xs text-blue-200 capitalize">
{{ auth()->user()->role }}
</p>
</div> </div>
<!-- ARROW -->
<i data-feather="chevron-up"
class="w-4 h-4 text-blue-200"></i>
</button> </button>
<!-- DROPDOWN --> <!-- DROPDOWN -->
<div id="userMenu" <div id="userMenu"
class="hidden absolute bottom-16 left-0 w-full class="hidden absolute bottom-16 left-0 w-full
bg-white text-gray-700 rounded-xl shadow-lg overflow-hidden"> bg-white text-gray-700 rounded-2xl shadow-2xl overflow-hidden">
<!-- PROFILE --> <!-- PROFILE -->
<a href="{{ route('profile') }}" <a href="{{ route('profile') }}"
class="flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-100"> class="flex items-center gap-3 px-4 py-4 text-sm hover:bg-gray-100 transition">
<i data-feather="user" class="w-4 h-4"></i>
<i data-feather="user"
class="w-4 h-4"></i>
Profile Profile
</a> </a>
<!-- LOGOUT --> <!-- LOGOUT -->
<form id="logoutForm" method="POST" action="{{ route('logout') }}"> <form id="logoutForm"
@csrf method="POST"
<button type="button" action="{{ route('logout') }}">
onclick="confirmLogout()"
class="w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-100"> @csrf
<i data-feather="log-out" class="w-4 h-4"></i>
Logout <button type="submit"
</button> class="w-full flex items-center gap-3 px-4 py-4 text-sm hover:bg-gray-100 transition text-left">
</form>
<i data-feather="log-out"
class="w-4 h-4"></i>
Logout
</button>
</form>
</div> </div>
@ -119,11 +175,14 @@ class="w-full flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-100">
</aside> </aside>
<!-- CONTENT --> <!-- CONTENT -->
<div class="flex-1 p-4 md:p-6 md:ml-64 ml-0"> <div class="flex-1 p-4 md:p-6 md:ml-64">
<!-- MOBILE BUTTON -->
<button onclick="toggleSidebar()" <button onclick="toggleSidebar()"
class="md:hidden mb-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow"> class="md:hidden mb-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow">
</button> </button>
@yield('content') @yield('content')
@ -134,28 +193,44 @@ class="md:hidden mb-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow">
<!-- MODAL LOGOUT --> <!-- MODAL LOGOUT -->
<div id="logoutModal" <div id="logoutModal"
class="hidden fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50"> class="hidden fixed inset-0 bg-black/40 flex items-center justify-center z-50 px-4">
<div class="bg-white rounded-xl p-6 w-80 text-center shadow-lg"> <div class="bg-white rounded-2xl p-6 w-96 text-center shadow-2xl">
<h3 class="text-lg font-semibold mb-2"> <!-- ICON -->
<div class="w-16 h-16 rounded-full
flex items-center justify-center mx-auto mb-5">
<i data-feather="log-out"
class="w-7 h-7 text-red-500"></i>
</div>
<!-- TITLE -->
<h3 class="text-xl font-bold text-gray-800 mb-2">
Konfirmasi Logout Konfirmasi Logout
</h3> </h3>
<p class="text-sm text-gray-600 mb-4"> <!-- TEXT -->
Apakah Anda ingin keluar? <p class="text-sm text-gray-500 mb-6">
Apakah Anda ingin keluar dari sistem?
</p> </p>
<!-- BUTTON -->
<div class="flex justify-center gap-3"> <div class="flex justify-center gap-3">
<button onclick="closeModal()" <button onclick="closeModal()"
class="px-4 py-2 bg-gray-200 rounded-lg"> class="px-5 py-2 rounded-xl bg-gray-200 hover:bg-gray-300 transition">
Tidak Tidak
</button> </button>
<button onclick="submitLogout()" <button onclick="submitLogout()"
class="px-4 py-2 bg-red-500 text-white rounded-lg"> class="px-5 py-2 rounded-xl bg-red-500 hover:bg-red-600 text-white transition">
Ya Ya
</button> </button>
</div> </div>
@ -166,28 +241,48 @@ class="px-4 py-2 bg-red-500 text-white rounded-lg">
<!-- SCRIPT --> <!-- SCRIPT -->
<script> <script>
function toggleSidebar() { function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('-translate-x-full'); document.getElementById('sidebar')
.classList.toggle('-translate-x-full');
} }
function toggleUserMenu() { function toggleUserMenu() {
document.getElementById('userMenu').classList.toggle('hidden'); document.getElementById('userMenu')
.classList.toggle('hidden');
} }
function confirmLogout(){ function confirmLogout() {
document.getElementById('logoutModal').classList.remove('hidden'); document.getElementById('logoutModal')
.classList.remove('hidden');
} }
function closeModal(){ function closeModal() {
document.getElementById('logoutModal').classList.add('hidden'); document.getElementById('logoutModal')
.classList.add('hidden');
} }
function submitLogout(){ function submitLogout() {
document.getElementById('logoutForm').submit(); document.getElementById('logoutForm').submit();
} }
/* CLOSE DROPDOWN KETIKA KLIK LUAR */
window.addEventListener('click', function(e){
const button = e.target.closest('button');
const menu = document.getElementById('userMenu');
if(!e.target.closest('#userMenu') &&
!e.target.closest('[onclick="toggleUserMenu()"]')) {
menu.classList.add('hidden');
}
});
feather.replace(); feather.replace();
</script> </script>
</body> </body>
</html> </html>

View File

@ -6,14 +6,20 @@
<!-- HEADER --> <!-- HEADER -->
<div class="flex items-center gap-4 mb-6"> <div class="flex items-center gap-4 mb-6">
<div> <div>
<h1 class="text-3xl font-bold text-gray-800">Profile Saya</h1> <h1 class="text-3xl font-bold text-gray-800">
Profile Saya
</h1>
<p class="text-gray-500 text-sm"> <p class="text-gray-500 text-sm">
Informasi akun pengguna <span class="text-blue-600 font-medium">SENTARA</span> Informasi akun pengguna
<span class="text-blue-600 font-medium">
SENTARA
</span>
</p> </p>
</div> </div>
</div> </div>
<!-- PROFILE CARD --> <!-- PROFILE CARD -->
@ -21,30 +27,53 @@
<div class="absolute right-0 bottom-0 w-64 h-64 bg-blue-100 opacity-30 rounded-full"></div> <div class="absolute right-0 bottom-0 w-64 h-64 bg-blue-100 opacity-30 rounded-full"></div>
<!-- FOTO -->
<div class="w-24 h-24 rounded-full bg-gradient-to-r from-blue-500 to-blue-700 <div class="w-24 h-24 rounded-full bg-gradient-to-r from-blue-500 to-blue-700
flex items-center justify-center text-white text-3xl font-bold shadow-md"> flex items-center justify-center text-white text-3xl font-bold shadow-md">
{{ strtoupper(substr(Auth::user()->name, 0, 1)) }} {{ strtoupper(substr(Auth::user()->name, 0, 1)) }}
</div> </div>
<!-- INFO -->
<div> <div>
<!-- NAMA -->
<h2 class="text-2xl font-semibold text-gray-800"> <h2 class="text-2xl font-semibold text-gray-800">
{{ Auth::user()->name }} {{ Auth::user()->name }}
</h2> </h2>
<p class="text-gray-500 mt-1"> <!-- ROLE -->
Staff - Dinas Pariwisata Jember <p class="text-gray-500 mt-1 capitalize">
{{ Auth::user()->role }} - Dinas Pariwisata Jember
</p> </p>
<span class="inline-block mt-3 text-xs bg-blue-100 text-blue-600 px-3 py-1 rounded-full font-medium"> <!-- BADGE ROLE -->
STAFF <span class="
inline-block mt-3 text-xs px-3 py-1 rounded-full font-medium uppercase
@if(Auth::user()->role == 'admin')
bg-purple-100 text-purple-700
@else
bg-blue-100 text-blue-600
@endif
">
{{ Auth::user()->role }}
</span> </span>
</div> </div>
</div> </div>
<!-- INFORMASI AKUN --> <!-- INFORMASI AKUN -->
<div class="mb-4 flex items-center gap-2 text-blue-600 font-semibold"> <div class="mb-4 flex items-center gap-2 text-blue-600 font-semibold">
<span class="text-xl">📋</span> Informasi Akun
<span class="text-xl">📋</span>
Informasi Akun
</div> </div>
<!-- GRID --> <!-- GRID -->
@ -52,48 +81,101 @@
<!-- NAMA --> <!-- NAMA -->
<div class="bg-white rounded-xl shadow-sm p-5 text-center hover:shadow-md transition"> <div class="bg-white rounded-xl shadow-sm p-5 text-center hover:shadow-md transition">
<div class="w-12 h-12 mx-auto mb-3 bg-blue-100 rounded-full flex items-center justify-center text-blue-600">
<div class="w-12 h-12 mx-auto mb-3 bg-blue-100 rounded-full
flex items-center justify-center text-blue-600">
👤 👤
</div> </div>
<p class="text-gray-400 text-sm">Nama Lengkap</p>
<p class="text-gray-400 text-sm">
Nama Lengkap
</p>
<p class="font-semibold text-gray-800 mt-1"> <p class="font-semibold text-gray-800 mt-1">
{{ Auth::user()->name }} {{ Auth::user()->name }}
</p> </p>
</div> </div>
<!-- EMAIL --> <!-- EMAIL -->
<div class="bg-white rounded-xl shadow-sm p-5 text-center hover:shadow-md transition"> <div class="bg-white rounded-xl shadow-sm p-5 text-center hover:shadow-md transition">
<div class="w-12 h-12 mx-auto mb-3 bg-blue-100 rounded-full flex items-center justify-center text-blue-600">
<div class="w-12 h-12 mx-auto mb-3 bg-blue-100 rounded-full
flex items-center justify-center text-blue-600">
✉️ ✉️
</div> </div>
<p class="text-gray-400 text-sm">Email</p>
<p class="text-gray-400 text-sm">
Email
</p>
<p class="font-semibold text-gray-800 mt-1"> <p class="font-semibold text-gray-800 mt-1">
{{ Auth::user()->email }} {{ Auth::user()->email }}
</p> </p>
</div> </div>
<!-- ROLE (FIX ICON) --> <!-- ROLE -->
<div class="bg-white rounded-xl shadow-sm p-5 text-center hover:shadow-md transition"> <div class="bg-white rounded-xl shadow-sm p-5 text-center hover:shadow-md transition">
<div class="w-12 h-12 mx-auto mb-3 bg-blue-100 rounded-full flex items-center justify-center text-blue-600">
<!-- ICON GROUP --> <div class="w-12 h-12 mx-auto mb-3 bg-blue-100 rounded-full
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> flex items-center justify-center text-blue-600">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 20h5v-1a4 4 0 00-3-3.87M9 20H4v-1a4 4 0 013-3.87m10-2.13a4 4 0 10-8 0m8 0a4 4 0 01-8 0"/> <!-- ICON -->
<svg xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-1a4 4 0 00-3-3.87
M9 20H4v-1a4 4 0 013-3.87
m10-2.13a4 4 0 10-8 0
m8 0a4 4 0 01-8 0"/>
</svg> </svg>
</div> </div>
<p class="text-gray-400 text-sm">Role</p>
<p class="font-semibold text-gray-800 mt-1">Staff</p> <p class="text-gray-400 text-sm">
Role
</p>
<p class="font-semibold text-gray-800 mt-1 capitalize">
{{ Auth::user()->role }}
</p>
</div> </div>
<!-- STATUS --> <!-- STATUS -->
<div class="bg-white rounded-xl shadow-sm p-5 text-center hover:shadow-md transition"> <div class="bg-white rounded-xl shadow-sm p-5 text-center hover:shadow-md transition">
<div class="w-12 h-12 mx-auto mb-3 bg-green-100 rounded-full flex items-center justify-center text-green-600">
<div class="w-12 h-12 mx-auto mb-3 bg-green-100 rounded-full
flex items-center justify-center text-green-600">
</div> </div>
<p class="text-gray-400 text-sm">Status Akun</p>
<span class="inline-block mt-1 text-sm bg-green-100 text-green-600 px-3 py-1 rounded-full"> <p class="text-gray-400 text-sm">
Status Akun
</p>
<span class="inline-block mt-1 text-sm bg-green-100
text-green-600 px-3 py-1 rounded-full">
Aktif Aktif
</span> </span>
</div> </div>
</div> </div>
@ -102,22 +184,37 @@
<div class="bg-white rounded-xl shadow-sm p-6 flex items-center justify-between"> <div class="bg-white rounded-xl shadow-sm p-6 flex items-center justify-between">
<div> <div>
<div class="flex items-center gap-2 text-blue-600 font-semibold mb-2"> <div class="flex items-center gap-2 text-blue-600 font-semibold mb-2">
<span class="text-xl"></span> Informasi Sistem
<span class="text-xl"></span>
Informasi Sistem
</div> </div>
<p class="text-gray-500 text-sm max-w-xl"> <p class="text-gray-500 text-sm max-w-xl">
Akun ini digunakan untuk mengakses sistem SENTARA Akun ini digunakan untuk mengakses sistem SENTARA
(Sistem Analisis Sentimen Ulasan Dinas Pariwisata Jember). (Sistem Analisis Sentimen Ulasan Dinas Pariwisata Jember).
</p> </p>
</div> </div>
</div> </div>
<!-- FOOTER --> <!-- FOOTER -->
<div class="flex justify-between text-sm text-gray-400 mt-6"> <div class="flex justify-between text-sm text-gray-400 mt-6">
<span>© 2025 SENTARA. All rights reserved.</span>
<span>Dinas Pariwisata Jember</span> <span>
© 2025 SENTARA. All rights reserved.
</span>
<span>
Dinas Pariwisata Jember
</span>
</div> </div>
</div> </div>

View File

@ -6,6 +6,8 @@
use App\Http\Controllers\UlasanController; use App\Http\Controllers\UlasanController;
use App\Http\Controllers\AnalisisController; use App\Http\Controllers\AnalisisController;
use App\Http\Controllers\RekomendasiController; use App\Http\Controllers\RekomendasiController;
use App\Http\Controllers\UserController;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -13,7 +15,6 @@
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
// hanya SATU route '/'
Route::get('/', function () { Route::get('/', function () {
return redirect('/login'); return redirect('/login');
}); });
@ -26,48 +27,114 @@
Route::middleware(['auth', 'verified'])->group(function () { Route::middleware(['auth', 'verified'])->group(function () {
// ✅ DASHBOARD /*
|--------------------------------------------------------------------------
| DASHBOARD
|--------------------------------------------------------------------------
*/
Route::get('/dashboard', [DashboardController::class, 'index']) Route::get('/dashboard', [DashboardController::class, 'index'])
->name('dashboard'); ->name('dashboard');
// ✅ PROFILE (ini yang kamu tanya) /*
|--------------------------------------------------------------------------
| PROFILE
|--------------------------------------------------------------------------
*/
Route::get('/profile', function () { Route::get('/profile', function () {
return view('profile.index'); return view('profile.index');
})->name('profile'); })->name('profile');
// kalau mau aktifkan update: /*
// Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); |--------------------------------------------------------------------------
// Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); | KELOLA PENGGUNA
|--------------------------------------------------------------------------
*/
Route::get('/kelola-pengguna',
[UserController::class, 'index']
)->name('kelola-pengguna.index');
// ✅ TAMBAHAN BARU — route untuk simpan pengguna baru
Route::post('/kelola-pengguna',
[UserController::class, 'store']
)->name('kelola-pengguna.store');
Route::get('/kelola-pengguna/{id}/edit',
[UserController::class, 'edit']
)->name('kelola-pengguna.edit');
Route::put('/kelola-pengguna/{id}',
[UserController::class, 'update']
)->name('kelola-pengguna.update');
Route::get('/kelola-pengguna/{id}/reset-password',
[UserController::class, 'resetPassword']
)->name('kelola-pengguna.reset-password');
Route::put('/kelola-pengguna/{id}/reset-password',
[UserController::class, 'updatePassword']
)->name('kelola-pengguna.update-password');
Route::delete('/kelola-pengguna/{id}',
[UserController::class, 'destroy']
)->name('kelola-pengguna.destroy');
/*
|--------------------------------------------------------------------------
| DATA ULASAN
|--------------------------------------------------------------------------
*/
// ✅ DATA ULASAN
Route::get('/ulasan', [UlasanController::class, 'index']) Route::get('/ulasan', [UlasanController::class, 'index'])
->name('ulasan.index'); ->name('ulasan.index');
Route::get('/riwayat', [UlasanController::class, 'riwayat']) Route::get('/riwayat', [UlasanController::class, 'riwayat'])
->name('riwayat.index'); ->name('riwayat.index');
Route::post('/riwayat/import', [UlasanController::class, 'importRiwayat']) Route::post('/riwayat/import',
->name('riwayat.import'); [UlasanController::class, 'importRiwayat']
)->name('riwayat.import');
Route::post('/ulasan/ambil', [UlasanController::class, 'ambilData']) Route::post('/ulasan/ambil',
->name('ulasan.ambil'); [UlasanController::class, 'ambilData']
)->name('ulasan.ambil');
Route::delete('/ulasan/periode/kosongkan', [UlasanController::class, 'kosongkanPeriode']) Route::delete('/ulasan/periode/kosongkan',
->name('ulasan.kosongkan-periode'); [UlasanController::class, 'kosongkanPeriode']
)->name('ulasan.kosongkan-periode');
// ✅ PROSES ANALISIS /*
Route::post('/proses-analisis', [UlasanController::class, 'analisisData']) |--------------------------------------------------------------------------
->name('proses.analisis'); | PROSES ANALISIS
|--------------------------------------------------------------------------
*/
// ✅ HASIL ANALISIS Route::post('/proses-analisis',
Route::get('/analisis', [AnalisisController::class, 'index']) [UlasanController::class, 'analisisData']
->name('analisis.index'); )->name('proses.analisis');
// ✅ REKOMENDASI /*
Route::get('/rekomendasi', function() { |--------------------------------------------------------------------------
return redirect('/dashboard?tab=rekomendasi'); | HASIL ANALISIS
})->name('rekomendasi.index'); |--------------------------------------------------------------------------
*/
Route::get('/analisis',
[AnalisisController::class, 'index']
)->name('analisis.index');
/*
|--------------------------------------------------------------------------
| REKOMENDASI
|--------------------------------------------------------------------------
*/
Route::get('/rekomendasi', function () {
return redirect('/dashboard?tab=rekomendasi');
})->name('rekomendasi.index');
}); });
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';

View File

@ -65,7 +65,8 @@ def read_laravel_env():
def env_value(env, key, default=""): def env_value(env, key, default=""):
value = os.getenv(key, env.get(key, default)) # .env file dibaca DULUAN, baru fallback ke os.getenv, lalu default
value = env.get(key) or os.getenv(key) or default
if value in {None, "", "null", "None"}: if value in {None, "", "null", "None"}:
return default return default
return value return value
@ -734,13 +735,12 @@ def main():
scraper = scraper_config() scraper = scraper_config()
destinations = selected_destinations(args.wisata) destinations = selected_destinations(args.wisata)
conn = pymysql.connect( conn = pymysql.connect(
host=config["host"], host='localhost',
port=config["port"], port=3306,
user=config["user"], user='root',
password=config["password"], password='', # isi password kalau ada
database=config["database"], database='sistem_analisis',
charset="utf8mb4", )
)
cursor = conn.cursor() cursor = conn.cursor()
try: try:
@ -767,4 +767,4 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()