Up project - SiPakarTebu Laravel Project

This commit is contained in:
E31232094SalsabilaJJ 2026-06-18 08:49:33 +07:00
commit 1c4cc1b35b
92 changed files with 9820 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
/vendor
/node_modules
.env
/storage/*.key
/public/hot
/public/storage
/storage/app/public
/storage/framework/cache
/storage/framework/sessions
/storage/framework/views
/storage/logs
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
.idea
.vscode

6
.htaccess Normal file
View File

@ -0,0 +1,6 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /E31232094/
RewriteCond %{REQUEST_URI} !^/E31232094/public/
RewriteRule ^(.*)$ /E31232094/public/$1 [L,R=301]
</IfModule>

View File

@ -0,0 +1,110 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class ArticleController extends Controller
{
private array $feeds = [
[
'name' => 'Kementerian Pertanian',
'url' => 'https://www.pertanian.go.id/feed/',
'logo' => null,
],
[
'name' => 'CYBEX Pertanian',
'url' => 'https://cybex.pertanian.go.id/feed',
'logo' => null,
],
[
'name' => 'Litbang Pertanian',
'url' => 'https://www.litbang.pertanian.go.id/rss/berita.xml',
'logo' => null,
],
[
'name' => 'P3GI',
'url' => 'https://p3gi.co.id/feed/',
'logo' => null,
],
];
public function fetchArticles(): array
{
return Cache::remember('rss_articles', 3600, function () {
$articles = [];
foreach ($this->feeds as $feed) {
try {
$response = Http::timeout(8)->get($feed['url']);
if (!$response->ok()) continue;
$xml = simplexml_load_string($response->body(), 'SimpleXMLElement', LIBXML_NOCDATA);
if (!$xml) continue;
$items = $xml->channel->item ?? [];
$count = 0;
foreach ($items as $item) {
if ($count >= 3) break;
// Filter keyword tebu ← TAMBAHKAN DI SINI
$keywords = ['tebu', 'gula', 'perkebunan', 'penyakit tanaman', 'pertanian'];
$title = strtolower((string) $item->title);
$desc = strtolower((string) $item->description);
$relevant = false;
foreach ($keywords as $kw) {
if (str_contains($title, $kw) || str_contains($desc, $kw)) {
$relevant = true;
break;
}
}
if (!$relevant) continue;
// Coba ambil gambar dari enclosure atau media:content
$image = null;
// Coba ambil gambar dari enclosure atau media:content
$image = null;
if (isset($item->enclosure)) {
$image = (string) $item->enclosure->attributes()->url;
}
if (!$image) {
$ns = $item->getNamespaces(true);
if (isset($ns['media'])) {
$media = $item->children($ns['media']);
$image = (string) ($media->content->attributes()->url ?? '');
}
}
// Fallback: ekstrak gambar dari description
if (!$image) {
preg_match('/<img[^>]+src=["\']([^"\']+)["\']/', (string)$item->description, $m);
$image = $m[1] ?? null;
}
$articles[] = [
'title' => (string) $item->title,
'link' => (string) $item->link,
'date' => date('d M Y', strtotime((string) $item->pubDate)),
'source' => $feed['name'],
'logo' => $feed['logo'],
'image' => $image,
'excerpt' => strip_tags(substr((string) $item->description, 0, 120)) . '...',
];
$count++;
}
} catch (\Exception $e) {
continue;
}
}
// Acak urutan artikel
shuffle($articles);
return array_slice($articles, 0, 6);
});
}
}

View File

@ -0,0 +1,129 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Cache;
use App\Models\User;
class ForgotPasswordController extends Controller
{
// STEP 1 — Tampilkan form email
public function showEmailForm()
{
return view('auth.forgot-password');
}
// STEP 1 — Kirim OTP ke email
public function sendOtp(Request $request)
{
$request->validate([
'email' => 'required|email|exists:users,email',
], [
'email.exists' => 'Email tidak ditemukan. Pastikan email sudah terdaftar.',
]);
$otp = rand(100000, 999999);
// Simpan OTP di cache selama 5 menit
Cache::put('otp_' . $request->email, $otp, now()->addMinutes(5));
Cache::put('otp_email', $request->email, now()->addMinutes(5));
// Coba kirim email, kalau gagal tetap lanjut (untuk development)
try {
Mail::raw("Kode OTP kamu: $otp (berlaku 5 menit)", function ($message) use ($request) {
$message->to($request->email)->subject('Kode OTP Reset Password');
});
} catch (\Exception $e) {
// Log error tapi jangan crash — tampilkan OTP langsung untuk testing
\Log::error('Mail gagal: ' . $e->getMessage());
}
// Untuk keperluan development: flash OTP ke session agar bisa dilihat
session()->flash('otp_debug', "OTP kamu: $otp");
return redirect()->route('password.otp.form');
}
// STEP 2 — Tampilkan form OTP
public function showOtpForm()
{
return view('auth.otp');
}
// STEP 2 — Verifikasi OTP
public function verifyOtp(Request $request)
{
$request->validate(['otp' => 'required|digits:6']);
$email = Cache::get('otp_email');
$cachedOtp = Cache::get('otp_' . $email);
if (!$cachedOtp || $request->otp != $cachedOtp) {
return back()->withErrors(['otp' => 'OTP salah atau sudah kadaluarsa.']);
}
// Tandai OTP sudah diverifikasi
Cache::put('otp_verified_' . $email, true, now()->addMinutes(10));
return redirect()->route('password.reset.form');
}
// STEP 2 — Resend OTP
public function resendOtp(Request $request)
{
$email = Cache::get('otp_email');
if (!$email) {
return redirect()->route('password.email')->withErrors(['email' => 'Sesi habis, ulangi dari awal.']);
}
$otp = rand(100000, 999999);
Cache::put('otp_' . $email, $otp, now()->addMinutes(5));
Mail::raw("Kode OTP kamu: $otp (berlaku 5 menit)", function ($message) use ($email) {
$message->to($email)->subject('Kode OTP Reset Password');
});
return back()->with('status', 'OTP baru telah dikirim.');
}
// STEP 3 — Tampilkan form reset password
public function showResetForm()
{
$email = Cache::get('otp_email');
if (!$email || !Cache::get('otp_verified_' . $email)) {
return redirect()->route('password.email')->withErrors(['email' => 'Verifikasi OTP terlebih dahulu.']);
}
return view('auth.reset-password');
}
// STEP 3 — Proses reset password
public function resetPassword(Request $request)
{
$request->validate([
'password' => 'required|min:8|confirmed',
]);
$email = Cache::get('otp_email');
if (!$email || !Cache::get('otp_verified_' . $email)) {
return redirect()->route('password.email')->withErrors(['email' => 'Sesi tidak valid.']);
}
User::where('email', $email)->update([
'password' => Hash::make($request->password),
]);
// Hapus semua cache terkait
Cache::forget('otp_' . $email);
Cache::forget('otp_email');
Cache::forget('otp_verified_' . $email);
return redirect()->route('login')->with('status', 'Password berhasil direset!');
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function showLoginForm()
{
return view('auth.login');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
return back()->withErrors([
'email' => 'Email atau password salah.',
])->onlyInput('email');
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/login');
}
}

View File

@ -0,0 +1,70 @@
<?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 showLogin()
{
return view('auth.login');
}
public function showRegister()
{
return view('auth.register');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return redirect()->intended(url('/dashboard'));
}
return back()->withErrors([
'email' => 'Email atau password salah.',
])->onlyInput('email');
}
public function register(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
'role' => 'required|in:admin,user',
], [
'role.required' => 'Pilih jenis akun terlebih dahulu.',
'role.in' => 'Jenis akun tidak valid.',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
'role' => $validated['role'],
]);
Auth::login($user);
return redirect(url('/dashboard'));
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect(url('/'));
}
}

View File

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

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\Models\Diagnosis;
use Illuminate\Http\Request;
class DashboardController extends Controller
{
public function index()
{
$totalDiagnosis = Diagnosis::where('user_id', auth()->id())->count();
$recentDiagnosis = Diagnosis::where('user_id', auth()->id())
->latest()
->take(5)
->get();
// Data untuk chart bulanan
$monthlyData = Diagnosis::where('user_id', auth()->id())
->whereYear('created_at', date('Y'))
->selectRaw('MONTH(created_at) as month, COUNT(*) as count')
->groupBy('month')
->get();
// Data tanggal diagnosa bulan ini
$diagnosaDates = Diagnosis::where('user_id', auth()->id())
->whereYear('created_at', date('Y'))
->whereMonth('created_at', date('n'))
->selectRaw('DAY(created_at) as day, COUNT(*) as count')
->groupBy('day')
->pluck('count', 'day')
->toArray();
// 🔥 FIX UTAMA: AKURASI RATA-RATA
$avgAccuracy = Diagnosis::where('user_id', auth()->id())
->whereNotNull('confidence')
->avg('confidence');
// biar ga null & tampil bagus
$avgAccuracy = $avgAccuracy ? round($avgAccuracy, 1) : 0;
$monthlyAccuracyRaw = Diagnosis::where('user_id', auth()->id())
->whereYear('created_at', date('Y'))
->whereNotNull('confidence')
->selectRaw('MONTH(created_at) as month, AVG(confidence) as avg_confidence')
->groupBy('month')->get()->keyBy('month');
$monthlyAccuracy = collect(range(1, 12))->map(function ($m) use ($monthlyAccuracyRaw) {
return $monthlyAccuracyRaw->has($m) ? round($monthlyAccuracyRaw[$m]->avg_confidence, 1) : null;
})->values()->toArray();
// Lalu di compact() tambahkan 'monthlyAccuracy'
return view('dashboard', compact(
'totalDiagnosis',
'recentDiagnosis',
'monthlyData',
'diagnosaDates',
'avgAccuracy',
'monthlyAccuracy' // ← tambahkan ini
));
return view('dashboard', compact(
'totalDiagnosis',
'recentDiagnosis',
'monthlyData',
'diagnosaDates',
'avgAccuracy'
));
}
}

View File

@ -0,0 +1,163 @@
<?php
namespace App\Http\Controllers;
use App\Models\Diagnosis;
use App\Models\Disease;
use App\Models\Notification;
use App\Models\Symptom;
use Illuminate\Http\Request;
class DiagnosisController extends Controller
{
public function create()
{
$symptoms = Symptom::orderBy('code')->get();
return view('diagnosis.create', compact('symptoms'));
}
public function store(Request $request)
{
$validated = $request->validate([
'symptoms' => 'required|array|min:4',
]);
$gejalaInput = [];
$gejalaLabels = [];
$namaGejala = Symptom::pluck('name', 'code');
foreach ($validated['symptoms'] as $kode => $cfUser) {
if (isset($cfUser) && $cfUser !== '') {
$cfUser = (float) $cfUser;
$cfUser = min($cfUser, 0.8);
$gejalaInput[$kode] = $cfUser;
$gejalaLabels[$kode] = [
'nama' => $namaGejala[$kode] ?? $kode,
'cf' => $cfUser,
];
}
}
if (count($gejalaInput) < 4) {
return back()->withErrors(['symptoms' => 'Pilih minimal 4 gejala.']);
}
$hasilDiagnosa = $this->diagnosaLengkap($gejalaInput);
$utama = $hasilDiagnosa[0] ?? null;
$diagnosis = Diagnosis::create([
'user_id' => auth()->id(),
'plant_name' => 'Tebu',
'symptoms' => $gejalaLabels,
'disease_name' => $utama ? $utama['nama'] : 'Tidak Terdeteksi',
'treatment' => $utama ? implode('; ', $utama['solusi']) : '-',
'confidence' => $utama ? $utama['persentase'] : 0,
]);
Notification::create([
'user_id' => auth()->id(),
'type' => 'diagnosis',
'title' => 'Diagnosa Selesai',
'message' => 'Diagnosa tanaman tebu selesai. Penyakit terdeteksi: ' . ($utama ? $utama['nama'] . ' (' . $utama['persentase'] . '%)' : 'Tidak Terdeteksi') . '.',
'is_read' => false,
]);
$admins = \App\Models\User::where('role', 'admin')
->where('id', '!=', auth()->id())
->get();
foreach ($admins as $admin) {
Notification::create([
'user_id' => $admin->id,
'type' => 'diagnosis',
'title' => 'Diagnosa Baru dari ' . auth()->user()->name,
'message' => 'User ' . auth()->user()->name . ' baru saja melakukan diagnosa. Penyakit terdeteksi: ' . ($utama ? $utama['nama'] . ' (' . $utama['persentase'] . '%)' : 'Tidak Terdeteksi') . '.',
'is_read' => false,
]);
}
return redirect()->route('diagnosis.result', $diagnosis->id);
}
public function result($id)
{
$diagnosis = Diagnosis::where('user_id', auth()->id())->findOrFail($id);
// DEBUG SEMENTARA — hapus setelah selesai debug
$gejalaInputDebug = collect($diagnosis->symptoms)
->mapWithKeys(fn($v, $k) => [$k => $v['cf']])
->toArray();
$debugInfo = $this->diagnosaLengkap($gejalaInputDebug, debug: true);
return view('diagnosis.result', compact('diagnosis', 'debugInfo'));
}
private function diagnosaLengkap(array $gejalaInput, bool $debug = false): array
{
$hasil = [];
$diseases = Disease::with(['symptoms', 'treatments'])->get();
foreach ($diseases as $disease) {
$cfCombine = 0;
$first = true;
$cocok = false;
$steps = [];
foreach ($disease->symptoms as $symptom) {
$kodeGejala = $symptom->code;
$cfPakar = (float) $symptom->pivot->cf_value;
if (isset($gejalaInput[$kodeGejala])) {
$cocok = true;
$cfUser = $gejalaInput[$kodeGejala];
$cf = $cfUser * $cfPakar;
if ($first) {
$cfCombine = $cf;
$first = false;
} else {
$cfCombine = $cfCombine + ($cf * (1 - $cfCombine));
}
if ($debug) {
$steps[] = [
'gejala' => $kodeGejala,
'cf_pakar' => $cfPakar,
'cf_user' => $cfUser,
'cf_komb' => round($cf, 6),
'cf_akum' => round($cfCombine, 6),
];
}
}
}
if ($cocok) {
$treatments = [];
if ($disease->treatments && $disease->treatments->count()) {
$treatments = $disease->treatments
->sortBy('order')
->pluck('description')
->toArray();
}
$entry = [
'nama' => $disease->name,
'persentase' => round($cfCombine * 100, 2),
'solusi' => count($treatments) ? $treatments : ['-'],
];
if ($debug) {
$entry['steps'] = $steps;
}
$hasil[] = $entry;
}
}
usort($hasil, fn($a, $b) => $b['persentase'] <=> $a['persentase']);
return $hasil;
}
}

View File

@ -0,0 +1,267 @@
<?php
namespace App\Http\Controllers;
use App\Models\Disease;
use App\Models\Notification;
use App\Models\Symptom;
use App\Models\Treatment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\DB;
class DiseaseController extends Controller
{
// Helper untuk simpan foto langsung ke public/storage
private function savePhoto($file, $folder)
{
$filename = uniqid() . '_' . time() . '.' . $file->getClientOriginalExtension();
$file->move(public_path('storage/' . $folder), $filename);
return $folder . '/' . $filename;
}
private function deletePhoto($path)
{
$fullPath = public_path('storage/' . $path);
if (file_exists($fullPath)) {
unlink($fullPath);
}
}
public function index()
{
$diseases = Disease::with(['symptoms','treatments'])->get();
return view('diseases.index', compact('diseases'));
}
public function create()
{
$nextDiseaseCode = 'P' . str_pad(Disease::count() + 1, 2, '0', STR_PAD_LEFT);
$nextSymptomCode = 'G' . str_pad(Symptom::count() + 1, 2, '0', STR_PAD_LEFT);
$nextTreatmentCode = 'S' . str_pad(Treatment::count() + 1, 2, '0', STR_PAD_LEFT);
return view('diseases.create', compact(
'nextDiseaseCode',
'nextSymptomCode',
'nextTreatmentCode'
));
}
public function edit(Disease $disease)
{
$disease->load(['symptoms','treatments']);
return view('diseases.create', [
'disease' => $disease,
'isEdit' => true,
'nextDiseaseCode' => $disease->code,
]);
}
public function update(Request $request, Disease $disease)
{
$request->validate([
'name' => 'required|string|max:255',
'latin_name' => 'nullable|string|max:255',
'description' => 'nullable|string',
]);
DB::transaction(function () use ($request, $disease) {
$disease->update([
'name' => $request->name,
'latin_name' => $request->latin_name,
'description' => $request->description,
]);
// Handle foto penyakit
if ($request->hasFile('photo')) {
if ($disease->photo) {
$this->deletePhoto($disease->photo);
}
$disease->update(['photo' => $this->savePhoto($request->file('photo'), 'diseases')]);
}
// RESET RELASI
$disease->symptoms()->detach();
$disease->treatments()->delete();
// SYMPTOMS
foreach ($request->symptoms as $i => $s) {
if (empty($s['name'])) continue;
$symptomPhoto = null;
if (isset($s['photo']) && $s['photo'] instanceof \Illuminate\Http\UploadedFile) {
$symptomPhoto = $this->savePhoto($s['photo'], 'symptoms');
}
$symptom = Symptom::where('name', $s['name'])->first();
if (!$symptom) {
$symptom = Symptom::create([
'code' => 'G' . str_pad(Symptom::count() + 1, 2, '0', STR_PAD_LEFT),
'name' => $s['name'],
'photo' => $symptomPhoto,
]);
}
$disease->symptoms()->attach($symptom->id, ['cf_value' => $s['cf'] ?? 0.7]);
}
// TREATMENTS
$maxRow = DB::select('SELECT MAX(CAST(SUBSTRING(code, 2) AS UNSIGNED)) as maxnum FROM treatments')[0];
$lastNum = $maxRow->maxnum ?? 0;
foreach ($request->treatments as $i => $t) {
if (!isset($t['description']) || trim($t['description']) === '') continue;
$lastNum++;
DB::table('treatments')->insert([
'code' => 'S' . str_pad($lastNum, 2, '0', STR_PAD_LEFT),
'disease_id' => $disease->id,
'description' => trim($t['description']),
'order' => $i + 1,
'created_at' => now(),
'updated_at' => now(),
]);
}
});
return redirect()->route('diseases.show', $disease->id)
->with('status', 'Penyakit berhasil diupdate!');
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'latin_name' => 'nullable|string|max:255',
'description' => 'nullable|string',
'photo' => 'nullable|image|mimes:jpg,jpeg,png|max:3072',
'symptoms' => 'required|array|min:3',
'symptoms.*.name' => 'required|string',
'treatments' => 'required|array|min:3',
'treatments.*.description' => 'nullable|string',
]);
$disease = DB::transaction(function () use ($request) {
$photoPath = null;
if ($request->hasFile('photo')) {
$photoPath = $this->savePhoto($request->file('photo'), 'diseases');
}
$disease = Disease::create([
'code' => 'P' . str_pad(Disease::count() + 1, 2, '0', STR_PAD_LEFT),
'name' => $request->name,
'latin_name' => $request->latin_name,
'description' => $request->description,
'photo' => $photoPath,
]);
// SYMPTOMS
foreach ($request->symptoms as $s) {
if (empty($s['name'])) continue;
$symptomPhoto = null;
if (isset($s['photo']) && $s['photo'] instanceof \Illuminate\Http\UploadedFile) {
$symptomPhoto = $this->savePhoto($s['photo'], 'symptoms');
}
$symptom = Symptom::where('name', $s['name'])->first();
if (!$symptom) {
$symptom = Symptom::create([
'code' => 'G' . str_pad(Symptom::count() + 1, 2, '0', STR_PAD_LEFT),
'name' => $s['name'],
'photo' => $symptomPhoto,
]);
}
$disease->symptoms()->attach(
$symptom->id,
['cf_value' => $s['cf'] ?? 0.7]
);
}
// TREATMENTS
if ($request->has('treatments')) {
$maxRow = DB::select('SELECT MAX(CAST(SUBSTRING(code, 2) AS UNSIGNED)) as maxnum FROM treatments')[0];
$lastNum = $maxRow->maxnum ?? 0;
foreach ($request->treatments as $i => $t) {
if (!isset($t['description']) || trim($t['description']) === '') continue;
$lastNum++;
DB::table('treatments')->insert([
'code' => 'S' . str_pad($lastNum, 2, '0', STR_PAD_LEFT),
'disease_id' => $disease->id,
'description' => trim($t['description']),
'order' => $i + 1,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
return $disease;
});
Notification::create([
'user_id' => auth()->id(),
'type' => 'system',
'title' => 'Penyakit Baru Ditambahkan',
'message' => "Penyakit \"{$disease->name}\" ({$disease->code}) berhasil ditambahkan dengan "
. $disease->symptoms()->count() . " gejala.",
'is_read' => false,
]);
return redirect()->route('diseases.index')
->with('status', 'Penyakit berhasil ditambahkan!');
}
public function show(Disease $disease)
{
$disease->load(['symptoms','treatments']);
return view('diseases.show', compact('disease'));
}
public function destroy(Disease $disease)
{
$diseaseName = $disease->name;
$diseaseCode = $disease->code;
DB::transaction(function () use ($disease) {
if ($disease->photo) {
$this->deletePhoto($disease->photo);
}
foreach ($disease->symptoms as $symptom) {
$usedByOthers = $symptom->diseases()
->where('diseases.id', '!=', $disease->id)
->exists();
if (!$usedByOthers) {
if ($symptom->photo) {
$this->deletePhoto($symptom->photo);
}
$symptom->delete();
}
}
$disease->symptoms()->detach();
$disease->treatments()->delete();
$disease->delete();
});
Notification::create([
'user_id' => auth()->id(),
'type' => 'system',
'title' => 'Penyakit Dihapus',
'message' => "Penyakit \"{$diseaseName}\" ({$diseaseCode}) telah dihapus.",
'is_read' => false,
]);
return redirect()->route('diseases.index')
->with('status', 'Penyakit berhasil dihapus!');
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers;
use App\Models\Disease;
use App\Models\Symptom;
use Illuminate\Http\Request;
class GuestDiagnosisController extends Controller
{
/**
* Tampilkan data gejala untuk modal di landing page (JSON)
*/
public function symptoms()
{
$symptoms = Symptom::orderBy('code')->get(['code', 'name']);
return response()->json($symptoms);
}
/**
* Proses diagnosis guest hitung CF, kembalikan hasil ringkas (JSON)
* Tidak disimpan ke DB, tidak butuh auth
*/
public function process(Request $request)
{
$validated = $request->validate([
'symptoms' => 'required|array|min:4',
'symptoms.*' => 'numeric|min:0|max:0.8',
]);
$gejalaInput = [];
foreach ($validated['symptoms'] as $kode => $cfUser) {
if ($cfUser !== '' && $cfUser !== null) {
$gejalaInput[$kode] = min((float) $cfUser, 0.8);
}
}
if (count($gejalaInput) < 4) {
return response()->json(['error' => 'Pilih minimal 4 gejala.'], 422);
}
$hasil = $this->hitungCF($gejalaInput);
$utama = $hasil[0] ?? null;
// Kembalikan hasil RINGKAS saja (tanpa penanganan lengkap)
return response()->json([
'disease_name' => $utama ? $utama['nama'] : 'Tidak Terdeteksi',
'confidence' => $utama ? $utama['persentase'] : 0,
'level' => $utama ? $this->level($utama['persentase']) : 'Tidak Terdeteksi',
'top_results' => array_slice(array_map(fn($h) => [
'nama' => $h['nama'],
'persentase' => $h['persentase'],
'level' => $this->level($h['persentase']),
], $hasil), 0, 3),
]);
}
/* ── Helpers ──────────────────────────────────────────────────────── */
private function hitungCF(array $gejalaInput): array
{
$hasil = [];
$diseases = Disease::with('symptoms')->get();
foreach ($diseases as $disease) {
$cfCombine = 0;
$first = true;
$cocok = false;
foreach ($disease->symptoms as $symptom) {
$kode = $symptom->code;
$cfPakar = (float) $symptom->pivot->cf_value;
if (isset($gejalaInput[$kode])) {
$cocok = true;
$cf = $gejalaInput[$kode] * $cfPakar;
if ($first) {
$cfCombine = $cf;
$first = false;
} else {
$cfCombine = $cfCombine + ($cf * (1 - $cfCombine));
}
}
}
if ($cocok) {
$hasil[] = [
'nama' => $disease->name,
'persentase' => round($cfCombine * 100, 2),
];
}
}
usort($hasil, fn($a, $b) => $b['persentase'] <=> $a['persentase']);
return $hasil;
}
private function level(float $persen): string
{
if ($persen <= 20) return 'Sangat Rendah';
if ($persen <= 40) return 'Rendah';
if ($persen <= 60) return 'Sedang';
if ($persen <= 80) return 'Tinggi';
return 'Sangat Tinggi';
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers;
use App\Models\Diagnosis;
use Illuminate\Http\Request;
class HistoryController extends Controller
{
public function index()
{
$diagnoses = Diagnosis::where('user_id', auth()->id())
->latest()
->paginate(15);
return view('history.index', compact('diagnoses'));
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers;
use App\Models\Notification;
use Illuminate\Http\Request;
class NotificationController extends Controller
{
public function index()
{
$notifications = auth()->user()->notifications()
->orderBy('created_at', 'desc')
->get();
// Tandai semua sebagai sudah dibaca saat halaman dibuka
auth()->user()->notifications()->where('is_read', false)->update(['is_read' => true]);
return view('notifications.index', compact('notifications'));
}
public function markRead($id)
{
$notif = Notification::where('id', $id)
->where('user_id', auth()->id())
->firstOrFail();
$notif->update(['is_read' => true]);
return back();
}
public function destroy($id)
{
Notification::where('id', $id)
->where('user_id', auth()->id())
->delete();
return back()->with('status', 'Notifikasi dihapus.');
}
public function destroyAll()
{
auth()->user()->notifications()->delete();
return back()->with('status', 'Semua notifikasi dihapus.');
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class ProfileController extends Controller
{
public function show()
{
return view('profile.index', ['user' => auth()->user()]);
}
public function update(Request $request)
{
$user = auth()->user();
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . $user->id,
'photo' => 'nullable|image|mimes:jpg,jpeg,png|max:2048',
]);
$user->name = $request->name;
$user->email = $request->email;
if ($request->hasFile('photo')) {
// Hapus foto lama
if ($user->photo) {
$oldPath = public_path('storage/' . $user->photo);
if (file_exists($oldPath)) unlink($oldPath);
}
// Simpan foto baru langsung ke public/storage/photos/
$file = $request->file('photo');
$filename = uniqid() . '_' . time() . '.' . $file->getClientOriginalExtension();
$file->move(public_path('storage/photos'), $filename);
$user->photo = 'photos/' . $filename;
}
$user->save();
return back()->with('status', 'Profil berhasil diperbarui!');
}
public function updatePassword(Request $request)
{
$request->validate([
'current_password' => 'required',
'password' => 'required|min:8|confirmed',
]);
$user = auth()->user();
if (!Hash::check($request->current_password, $user->password)) {
return back()->withErrors(['current_password' => 'Password saat ini salah.']);
}
$user->password = Hash::make($request->password);
$user->save();
return back()->with('status_password', 'Password berhasil diubah!');
}
public function destroy(Request $request)
{
$user = auth()->user();
// Hapus foto profil
if ($user->photo) {
$photoPath = public_path('storage/' . $user->photo);
if (file_exists($photoPath)) unlink($photoPath);
}
auth()->logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect(url('/'));
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Notification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class UserController extends Controller
{
// Daftar semua user
public function index(Request $request)
{
$query = User::query();
// Filter by role
if ($request->filled('role')) {
$query->where('role', $request->role);
}
// Search by name/email
if ($request->filled('search')) {
$query->where(function ($q) use ($request) {
$q->where('name', 'like', '%' . $request->search . '%')
->orWhere('email', 'like', '%' . $request->search . '%');
});
}
$users = $query->latest()->paginate(10)->withQueryString();
$totalAdmin = User::where('role', 'admin')->count();
$totalUser = User::where('role', 'user')->count();
return view('users.index', compact('users', 'totalAdmin', 'totalUser'));
}
// Ubah role user
public function updateRole(Request $request, User $user)
{
// Admin tidak bisa ubah role dirinya sendiri
if ($user->id === auth()->id()) {
return back()->withErrors(['error' => 'Kamu tidak bisa mengubah role akunmu sendiri.']);
}
$request->validate([
'role' => 'required|in:admin,user',
]);
$oldRole = $user->role;
$user->update(['role' => $request->role]);
$roleLabel = $request->role === 'admin' ? 'Ahli Tanaman' : 'Petani';
$oldLabel = $oldRole === 'admin' ? 'Ahli Tanaman' : 'Petani';
// Notifikasi
Notification::create([
'user_id' => auth()->id(),
'type' => 'system',
'title' => 'Role User Diubah',
'message' => "Role \"{$user->name}\" berhasil diubah dari {$oldLabel} menjadi {$roleLabel}.",
'is_read' => false,
]);
return back()->with('status', "Role {$user->name} berhasil diubah menjadi {$roleLabel}.");
}
// Hapus user
public function destroy(User $user)
{
// Admin tidak bisa hapus dirinya sendiri
if ($user->id === auth()->id()) {
return back()->withErrors(['error' => 'Kamu tidak bisa menghapus akunmu sendiri.']);
}
$userName = $user->name;
// Hapus data terkait
$user->notifications()->delete();
$user->diagnoses()->delete();
$user->delete();
// Notifikasi
Notification::create([
'user_id' => auth()->id(),
'type' => 'system',
'title' => 'User Dihapus',
'message' => "Akun user \"{$userName}\" berhasil dihapus dari sistem.",
'is_read' => false,
]);
return back()->with('status', "User {$userName} berhasil dihapus.");
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class WelcomeController extends Controller
{
public function index()
{
$articles = Cache::remember('rss_articles', 3600, function () {
$articleController = new ArticleController();
$articles = $articleController->fetchArticles();
return !empty($articles) ? $articles : $this->getDummyArticles();
});
return view('welcome', compact('articles'));
}
private function getDummyArticles()
{
return [
[
'title' => 'Teknologi Pertanian Modern Tingkatkan Produktivitas Tebu',
'link' => 'https://www.pertanian.go.id',
'excerpt' => 'Penerapan teknologi pertanian presisi dan sistem pakar membantu petani meningkatkan hasil panen hingga 30%.',
'date' => date('d M Y'),
'source' => 'Kementerian Pertanian',
'image' => 'https://images.unsplash.com/photo-1625246333195-78d9c38ad449?w=600&q=80',
],
[
'title' => 'Cara Mencegah Penyakit Karat Daun pada Tanaman Tebu',
'link' => 'https://cybex.pertanian.go.id',
'excerpt' => 'Penyakit karat daun menjadi ancaman serius bagi perkebunan tebu. Deteksi dini dan penanganan tepat sangat penting.',
'date' => date('d M Y', strtotime('-2 days')),
'source' => 'CYBEX Pertanian',
'image' => 'https://images.unsplash.com/photo-1574943320219-553eb213f72d?w=600&q=80',
],
[
'title' => 'Inovasi Sistem Pakar untuk Diagnosis Penyakit Tanaman',
'link' => 'https://www.pertanian.go.id',
'excerpt' => 'Sistem pakar berbasis AI menggunakan metode Certainty Factor terbukti efektif dalam mendiagnosis penyakit tanaman.',
'date' => date('d M Y', strtotime('-5 days')),
'source' => 'Litbang Pertanian',
'image' => 'https://images.unsplash.com/photo-1416879595882-3373a0480b5b?w=600&q=80',
],
[
'title' => 'Budidaya Tebu Organik Ramah Lingkungan',
'link' => 'https://www.pertanian.go.id',
'excerpt' => 'Metode budidaya organik tidak hanya ramah lingkungan tetapi juga menghasilkan tebu berkualitas tinggi.',
'date' => date('d M Y', strtotime('-7 days')),
'source' => 'Kementerian Pertanian',
'image' => 'https://images.unsplash.com/photo-1464226184884-fa280b87c399?w=600&q=80',
],
[
'title' => 'Pelatihan Petani dalam Identifikasi Penyakit Tanaman',
'link' => 'https://cybex.pertanian.go.id',
'excerpt' => 'Program pelatihan nasional membekali petani dengan kemampuan mengenali gejala awal penyakit tanaman.',
'date' => date('d M Y', strtotime('-10 days')),
'source' => 'CYBEX Pertanian',
'image' => 'https://images.unsplash.com/photo-1500651230702-0e2d8a49d4ad?w=600&q=80',
],
[
'title' => 'Riset Varietas Tebu Tahan Penyakit',
'link' => 'https://www.pertanian.go.id',
'excerpt' => 'Penelitian terbaru berhasil mengembangkan varietas tebu unggul yang tahan terhadap berbagai jenis penyakit utama.',
'date' => date('d M Y', strtotime('-14 days')),
'source' => 'Litbang Pertanian',
'image' => 'https://images.unsplash.com/photo-1530836369250-ef72a3f5cda8?w=600&q=80',
],
];
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : url('/login');
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect('/dashboard');
}
}
return $next($request);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class RoleMiddleware
{
/**
* Handle an incoming request.
* Penggunaan: Route::middleware('role:admin')
*/
public function handle(Request $request, Closure $next, string $role): Response
{
if (! auth()->check()) {
return redirect()->route('login');
}
if (auth()->user()->role !== $role) {
// Jika user biasa mencoba akses halaman admin
abort(403, 'Akses ditolak. Halaman ini hanya untuk ' . ($role === 'admin' ? 'Ahli Tanaman' : 'Petani') . '.');
}
return $next($request);
}
}

26
app/Models/Diagnosis.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Diagnosis extends Model
{
protected $fillable = [
'user_id',
'plant_name',
'symptoms',
'disease_name',
'treatment',
'confidence',
];
protected $casts = [
'symptoms' => 'array',
];
public function user()
{
return $this->belongsTo(User::class);
}
}

27
app/Models/Disease.php Normal file
View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Disease extends Model
{
protected $fillable = [
'code',
'name',
'latin_name',
'description',
'photo',
];
public function symptoms()
{
return $this->belongsToMany(Symptom::class, 'disease_symptoms')
->withPivot('cf_value');
}
public function treatments()
{
return $this->hasMany(Treatment::class)->orderBy('order');
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Notification extends Model
{
protected $fillable = [
'user_id',
'type',
'title',
'message',
'is_read',
];
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PasswordResetOtp extends Model
{
protected $fillable = [
'email',
'otp',
'expires_at',
'is_used',
];
protected $casts = [
'expires_at' => 'datetime',
'is_used' => 'boolean',
];
/**
* Cek apakah OTP masih valid (belum expired & belum dipakai)
*/
public function isValid(): bool
{
return ! $this->is_used && $this->expires_at->isFuture();
}
}

14
app/Models/Symptom.php Normal file
View File

@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Symptom extends Model
{
protected $fillable = ['code','name','photo'];
public function diseases()
{
return $this->belongsToMany(Disease::class, 'disease_symptoms')
->withPivot('cf_value');
}
}

13
app/Models/Treatment.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Treatment extends Model
{
protected $fillable = ['code','disease_id','description','order'];
public function disease()
{
return $this->belongsTo(Disease::class);
}
}

54
app/Models/User.php Normal file
View File

@ -0,0 +1,54 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
'role',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
// ── Helper role ───────────────────────────────────────────────
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function isUser(): bool
{
return $this->role === 'user';
}
// ── Relasi ───────────────────────────────────────────────────
public function notifications()
{
return $this->hasMany(Notification::class);
}
public function diagnoses()
{
return $this->hasMany(Diagnosis::class);
}
}

View File

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

30
app/mail/otpmail.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class OtpMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public string $otp) {}
public function envelope(): Envelope
{
return new Envelope(
subject: '[PlantCare] Kode Verifikasi Reset Password',
);
}
public function content(): Content
{
return new Content(
view: 'emails.otp',
);
}
}

18
artisan Normal file
View File

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

22
bootstrap/app.php Normal file
View File

@ -0,0 +1,22 @@
<?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) {
$middleware->alias([
'auth' => \App\Http\Middleware\Authenticate::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'role' => \App\Http\Middleware\RoleMiddleware::class, // ← tambahkan ini
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();

2
bootstrap/cache/.gitignore vendored Normal file
View File

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

5
bootstrap/providers.php Normal file
View File

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

88
composer.json Normal file
View File

@ -0,0 +1,88 @@
{
"$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",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"simplepie/simplepie": "^1.9",
"willdurand/negotiation": "^3.1"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"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.3"
},
"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\" \"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
}

126
config/app.php Normal file
View File

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

115
config/auth.php Normal file
View File

@ -0,0 +1,115 @@
<?php
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', App\Models\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),
];

108
config/cache.php Normal file
View File

@ -0,0 +1,108 @@
<?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", "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',
],
],
/*
|--------------------------------------------------------------------------
| 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-'),
];

183
config/database.php Normal file
View File

@ -0,0 +1,183 @@
<?php
use Illuminate\Support\Str;
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([
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([
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' => '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),
],
],
];

80
config/filesystems.php Normal file
View File

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

132
config/logging.php Normal file
View File

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

127
config/mail.php Normal file
View File

@ -0,0 +1,127 @@
<?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' => 'tls', // ← ganti dari env() ke langsung 'tls'
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 587),
'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)),
// Tambahkan ini ↓
'stream' => [
'ssl' => [
'allow_self_signed' => true,
'verify_peer' => false,
'verify_peer_name' => false,
],
],
],
'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', 'Example'),
],
];

120
config/queue.php Normal file
View File

@ -0,0 +1,120 @@
<?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", "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,
],
'failover' => [
'driver' => 'failover',
'connections' => [
'database',
'sync',
],
],
],
/*
|--------------------------------------------------------------------------
| 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',
],
];

38
config/services.php Normal file
View File

@ -0,0 +1,38 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'token' => env('POSTMARK_TOKEN'),
],
'resend' => [
'key' => env('RESEND_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'),
],
],
];

217
config/session.php Normal file
View File

@ -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', 480),
'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 and all 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),
];

1
database/.gitignore vendored Normal file
View File

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

View File

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

View File

@ -0,0 +1,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');
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->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');
}
};

View File

@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};

View File

@ -0,0 +1,33 @@
<?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('diagnoses', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('plant_name')->nullable();
$table->text('symptoms')->nullable();
$table->text('diagnosis')->nullable();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('diagnoses');
}
};

View File

@ -0,0 +1,28 @@
<?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('diagnoses', function (Blueprint $table) {
$table->string('disease_name')->nullable()->after('symptoms');
$table->text('treatment')->nullable()->after('disease_name');
$table->float('confidence')->default(0)->after('treatment');
$table->json('cf_results')->nullable()->after('confidence');
});
}
public function down(): void
{
Schema::table('diagnoses', function (Blueprint $table) {
$table->dropColumn(['disease_name', 'treatment', 'confidence', 'cf_results']);
});
}
};

View File

@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
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('sessions');
}
};

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
{
public function up(): void
{
Schema::create('password_reset_otps', function (Blueprint $table) {
$table->id();
$table->string('email')->index();
$table->string('otp', 6);
$table->timestamp('expires_at');
$table->boolean('is_used')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('password_reset_otps');
}
};

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(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('photo')->nullable()->after('email');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('photo');
});
}
};

View File

@ -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::create('notifications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('type'); // diagnosis, reminder, system
$table->string('title');
$table->text('message');
$table->boolean('is_read')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('notifications');
}
};

View File

@ -0,0 +1,32 @@
<?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('diseases', function (Blueprint $table) {
$table->id();
$table->string('code')->unique(); // P01, P02, dst
$table->string('name');
$table->string('latin_name')->nullable();
$table->text('description')->nullable();
$table->string('photo')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('diseases');
}
};

View File

@ -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::create('symptoms', function (Blueprint $table) {
$table->id();
$table->string('code')->unique(); // G01, G02, dst
$table->string('name');
$table->string('photo')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('symptoms');
}
};

View File

@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('treatments', function (Blueprint $table) {
$table->id();
$table->string('code')->unique(); // S01, S02, dst
$table->foreignId('disease_id')->constrained()->onDelete('cascade');
$table->text('description');
$table->integer('order')->default(1);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('treatments');
}
};

View File

@ -0,0 +1,29 @@
<?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('disease_symptoms', function (Blueprint $table) {
$table->id();
$table->foreignId('disease_id')->constrained()->onDelete('cascade');
$table->foreignId('symptom_id')->constrained()->onDelete('cascade');
$table->float('cf_value')->default(0.5);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('disease_symptoms');
}
};

View File

@ -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) {
// 'admin' = Ahli Tanaman, 'user' = Petani/Umum
$table->enum('role', ['admin', 'user'])->default('user')->after('email');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role');
});
}
};

View File

@ -0,0 +1,15 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
DiseaseSeeder::class,
]);
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace Database\Seeders;
use App\Models\Disease;
use App\Models\Symptom;
use App\Models\Treatment;
use Illuminate\Database\Seeder;
class DiseaseSeeder extends Seeder
{
public function run(): void
{
$data = [
[
'code' => 'P01', 'name' => 'Luka Api (Leaf Scald)',
'latin_name' => 'Xanthomonas albilineans',
'description' => 'Penyakit bakteri yang menyebabkan garis putih memanjang pada daun tebu',
'symptoms' => [
['code'=>'G01','name'=>'Garis putih memanjang pada daun (stripe putih)','cf'=>0.90],
['code'=>'G02','name'=>'Daun layu dan mengering','cf'=>0.70],
['code'=>'G03','name'=>'Tanaman kerdil / tumbuh tidak normal','cf'=>0.60],
['code'=>'G15','name'=>'Daun terlihat kusam / tidak segar','cf'=>0.50],
['code'=>'G18','name'=>'Warna daun pucat kekuningan','cf'=>0.60],
['code'=>'G20','name'=>'Mata tunas mati','cf'=>0.70],
],
'treatments' => [
'Gunakan bibit sehat dan bebas penyakit',
'Sterilisasi alat potong dengan larutan formalin 2%',
'Tanam varietas tahan seperti PS 881, BL',
'Buang dan bakar tanaman yang terinfeksi',
'Rotasi tanaman dengan palawija',
],
],
[
'code' => 'P02', 'name' => 'Pokahbung',
'latin_name' => 'Fusarium moniliforme',
'description' => 'Penyakit jamur yang menyebabkan busuk pada batang tebu dan munculnya tunas abnormal',
'symptoms' => [
['code'=>'G04','name'=>'Batang busuk dari dalam','cf'=>0.90],
['code'=>'G05','name'=>'Bau busuk pada batang','cf'=>0.80],
['code'=>'G06','name'=>'Munculnya tunas-tunas kecil abnormal (pokah)','cf'=>0.90],
['code'=>'G16','name'=>'Tanaman mudah rebah','cf'=>0.70],
['code'=>'G19','name'=>'Luka memanjang pada batang','cf'=>0.60],
['code'=>'G20','name'=>'Mata tunas mati','cf'=>0.70],
],
'treatments' => [
'Gunakan bibit dari bagian pangkal batang',
'Rendam bibit dengan fungisida (Benlate 2g/liter selama 30 menit)',
'Hindari luka mekanis pada batang saat penanaman',
'Perbaiki sistem drainase lahan agar tidak tergenang',
'Tanam varietas tahan seperti PS 862, PS 881',
],
],
[
'code' => 'P03', 'name' => 'Karat Daun (Leaf Rust)',
'latin_name' => 'Puccinia melanocephala',
'description' => 'Penyakit jamur yang menyebabkan bercak karat kemerahan pada permukaan daun',
'symptoms' => [
['code'=>'G07','name'=>'Bercak coklat kemerahan pada daun','cf'=>0.90],
['code'=>'G08','name'=>'Bercak berkembang menjadi pustula karat','cf'=>0.95],
['code'=>'G09','name'=>'Daun mengering dimulai dari ujung','cf'=>0.70],
['code'=>'G15','name'=>'Daun terlihat kusam / tidak segar','cf'=>0.60],
['code'=>'G17','name'=>'Produksi turun drastis','cf'=>0.60],
],
'treatments' => [
'Semprot dengan fungisida berbahan aktif Mancozeb',
'Tanam varietas tahan seperti PS 862, PSJT 941',
'Jaga jarak tanam yang optimal (120130 cm)',
'Bersihkan gulma secara rutin di sekitar tanaman',
'Pangkas dan musnahkan daun yang terinfeksi berat',
],
],
[
'code' => 'P04', 'name' => 'Mozaik',
'latin_name' => 'Sugarcane Mosaic Virus (SCMV)',
'description' => 'Penyakit virus yang menyebabkan pola belang hijau tua dan hijau muda pada daun',
'symptoms' => [
['code'=>'G10','name'=>'Belang hijau tua dan hijau muda pada daun (mozaik)','cf'=>0.95],
['code'=>'G11','name'=>'Pertumbuhan tanaman terhambat','cf'=>0.70],
['code'=>'G15','name'=>'Daun terlihat kusam / tidak segar','cf'=>0.60],
['code'=>'G17','name'=>'Produksi turun drastis','cf'=>0.70],
['code'=>'G18','name'=>'Warna daun pucat kekuningan','cf'=>0.50],
],
'treatments' => [
'Gunakan bibit sehat yang bebas virus',
'Kendalikan vektor kutu daun dengan insektisida sistemik',
'Cabut dan musnahkan tanaman yang terinfeksi virus',
'Tanam varietas tahan terhadap SCMV',
'Rotasi dengan tanaman non-inang virus selama 1 musim',
],
],
[
'code' => 'P05', 'name' => 'Ratoon Stunting Disease (RSD)',
'latin_name' => 'Leifsonia xyli subsp. xyli',
'description' => 'Penyakit bakteri sistemik yang menghambat pertumbuhan dan menurunkan produksi gula',
'symptoms' => [
['code'=>'G03','name'=>'Tanaman kerdil / tumbuh tidak normal','cf'=>0.80],
['code'=>'G11','name'=>'Pertumbuhan tanaman terhambat','cf'=>0.90],
['code'=>'G12','name'=>'Ruas batang memendek','cf'=>0.85],
['code'=>'G13','name'=>'Pembentukan gula terhambat','cf'=>0.80],
['code'=>'G14','name'=>'Pembuluh batang berwarna merah kecoklatan','cf'=>0.90],
['code'=>'G17','name'=>'Produksi turun drastis','cf'=>0.80],
],
'treatments' => [
'Perlakuan air panas pada bibit (suhu 50°C selama 2 jam)',
'Sterilisasi alat potong sebelum dan sesudah digunakan',
'Gunakan bibit dari hasil kultur jaringan yang bersertifikat',
'Tanam varietas toleran RSD yang direkomendasikan',
'Hindari penggunaan bibit dari kebun yang terindikasi terinfeksi',
],
],
];
$treatmentCounter = 1;
foreach ($data as $d) {
$disease = Disease::create([
'code' => $d['code'],
'name' => $d['name'],
'latin_name' => $d['latin_name'],
'description' => $d['description'],
]);
foreach ($d['symptoms'] as $s) {
$symptom = Symptom::firstOrCreate(
['code' => $s['code']],
['name' => $s['name']]
);
$disease->symptoms()->attach($symptom->id, ['cf_value' => $s['cf']]);
}
foreach ($d['treatments'] as $t) {
Treatment::create([
'code' => 'S' . str_pad($treatmentCounter++, 2, '0', STR_PAD_LEFT),
'disease_id' => $disease->id,
'description' => $t,
'order' => array_search($t, $d['treatments']) + 1,
]);
}
}
}
}

19
public/.htaccess Normal file
View File

@ -0,0 +1,19 @@
<IfModule mod_lsapi.c>
AddType application/x-httpd-lsphp .php
</IfModule>
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
RewriteBase /E31232094/public/
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

0
public/favicon.ico Normal file
View File

17
public/index.php Normal file
View File

@ -0,0 +1,17 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
require __DIR__.'/../vendor/autoload.php';
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());

181
public/js/tour.js Normal file
View File

@ -0,0 +1,181 @@
// tour.js — SiPakarTebu Onboarding Tour
const tourSteps = [
{
title: "Selamat datang di SiPakarTebu! 🌱",
desc: "Ini adalah panduan singkat untuk membantu kamu memahami fitur-fitur utama aplikasi.",
target: null,
},
{
title: "Menu Navigasi",
desc: "Gunakan menu di sebelah kiri untuk berpindah antar halaman: Dashboard, Diagnosa, Riwayat, Kamus, dan Profil.",
target: "#sidebar",
pos: "right",
},
{
title: "Statistik Kamu",
desc: "Ringkasan total diagnosa yang sudah kamu lakukan dan akurasi rata-rata model.",
target: ".stat-cards",
pos: "bottom",
},
{
title: "Grafik Diagnosa",
desc: "Grafik ini menunjukkan aktivitas diagnosamu per bulan sepanjang tahun.",
target: ".chart-container",
pos: "top",
},
{
title: "Mulai Diagnosa Baru",
desc: "Klik menu Diagnosa untuk memulai diagnosa penyakit tebu. Unggah foto dan dapatkan hasilnya!",
target: "#nav-diagnosa",
pos: "right",
},
];
let tourCurrent = 0;
function createTourOverlay() {
const overlay = document.createElement("div");
overlay.id = "tour-overlay";
overlay.style.cssText = `
position: fixed; inset: 0; z-index: 9999;
pointer-events: none;
`;
document.body.appendChild(overlay);
return overlay;
}
function createTooltip() {
const el = document.createElement("div");
el.id = "tour-tooltip";
el.style.cssText = `
position: fixed; z-index: 10000;
background: #fff; border-radius: 14px;
padding: 20px 22px; width: 280px;
box-shadow: 0 8px 40px rgba(0,0,0,0.22);
pointer-events: auto;
font-family: inherit;
`;
document.body.appendChild(el);
return el;
}
function createHighlight() {
const el = document.createElement("div");
el.id = "tour-highlight";
el.style.cssText = `
position: fixed; z-index: 9998;
border: 2.5px solid #2e7d52;
border-radius: 10px;
pointer-events: none;
transition: all 0.35s ease;
box-shadow: 0 0 0 9999px rgba(0,0,0,0.50);
`;
document.body.appendChild(el);
return el;
}
function renderTourStep() {
const step = tourSteps[tourCurrent];
const total = tourSteps.length;
const highlight = document.getElementById("tour-highlight");
const tooltip = document.getElementById("tour-tooltip");
// Highlight target element
if (step.target) {
const target = document.querySelector(step.target);
if (target) {
const r = target.getBoundingClientRect();
const pad = 8;
highlight.style.display = "block";
highlight.style.top = (r.top - pad) + "px";
highlight.style.left = (r.left - pad) + "px";
highlight.style.width = (r.width + pad * 2) + "px";
highlight.style.height = (r.height + pad * 2) + "px";
// Position tooltip near target
if (step.pos === "right") {
tooltip.style.top = r.top + "px";
tooltip.style.left = (r.right + 16) + "px";
} else if (step.pos === "bottom") {
tooltip.style.top = (r.bottom + 16) + "px";
tooltip.style.left = Math.max(16, r.left) + "px";
} else if (step.pos === "top") {
tooltip.style.top = (r.top - 200) + "px";
tooltip.style.left = Math.max(16, r.left) + "px";
} else {
tooltip.style.top = "50%";
tooltip.style.left = "50%";
tooltip.style.transform = "translate(-50%, -50%)";
}
}
} else {
highlight.style.display = "none";
tooltip.style.top = "50%";
tooltip.style.left = "50%";
tooltip.style.transform = "translate(-50%, -50%)";
}
const dots = Array.from({ length: total }, (_, i) =>
`<div style="width:${i === tourCurrent ? 16 : 6}px;height:6px;border-radius:3px;
background:${i === tourCurrent ? "#2e7d52" : "#ddd"};
transition:all 0.3s;"></div>`
).join("");
tooltip.innerHTML = `
<div style="font-size:11px;color:#2e7d52;font-weight:600;text-transform:uppercase;
letter-spacing:0.8px;margin-bottom:6px;">
Langkah ${tourCurrent + 1} dari ${total}
</div>
<div style="font-size:15px;font-weight:600;color:#1a2f1a;margin-bottom:6px;">
${step.title}
</div>
<div style="font-size:13px;color:#555;line-height:1.6;margin-bottom:16px;">
${step.desc}
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;gap:5px;align-items:center;">${dots}</div>
<div style="display:flex;gap:10px;align-items:center;">
<button onclick="endTour()" style="background:none;border:none;
font-size:12px;color:#999;cursor:pointer;">Lewati</button>
<button onclick="tourNext()" style="background:#2e7d52;color:#fff;border:none;
padding:8px 16px;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;">
${tourCurrent === total - 1 ? "Selesai ✓" : "Lanjut →"}
</button>
</div>
</div>
`;
}
function tourNext() {
tourCurrent++;
if (tourCurrent >= tourSteps.length) {
endTour();
return;
}
renderTourStep();
}
function endTour() {
["tour-overlay", "tour-tooltip", "tour-highlight"].forEach((id) => {
const el = document.getElementById(id);
if (el) el.remove();
});
// Tandai sudah pernah tour
localStorage.setItem("sipakartebu_tour_done", "1");
}
function startTour() {
tourCurrent = 0;
if (!document.getElementById("tour-overlay")) createTourOverlay();
if (!document.getElementById("tour-tooltip")) createTooltip();
if (!document.getElementById("tour-highlight")) createHighlight();
renderTourStep();
}
// Auto-start untuk pengguna baru
document.addEventListener("DOMContentLoaded", () => {
if (!localStorage.getItem("sipakartebu_tour_done")) {
setTimeout(startTour, 800); // delay sedikit biar halaman load dulu
}
});

2
public/robots.txt Normal file
View File

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

11
resources/css/app.css Normal file
View File

@ -0,0 +1,11 @@
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}

1
resources/js/app.js Normal file
View File

@ -0,0 +1 @@
import './bootstrap';

4
resources/js/bootstrap.js vendored Normal file
View File

@ -0,0 +1,4 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

View File

@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lupa Password - SiPakarTebu</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
background-color: #f5f5f0;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Segoe UI', sans-serif;
}
.card {
background: white;
border-radius: 16px;
overflow: hidden;
width: 100%;
max-width: 460px;
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
}
.card-header {
background: #2d6a4f;
padding: 40px 20px 30px;
text-align: center;
color: white;
}
.card-header h1 { font-size: 28px; font-weight: 700; }
.card-header p { font-size: 14px; opacity: 0.85; margin-top: 4px; }
.card-body { padding: 36px 40px; }
.card-body h2 { font-size: 22px; font-weight: 700; color: #1a1a1a; }
.card-body .subtitle { color: #666; font-size: 14px; margin-top: 4px; margin-bottom: 24px; }
.alert-error {
background: #fef2f2;
border: 1px solid #fca5a5;
color: #dc2626;
border-radius: 8px;
padding: 10px 14px;
font-size: 14px;
margin-bottom: 16px;
}
.alert-success {
background: #f0fdf4;
border: 1px solid #86efac;
color: #16a34a;
border-radius: 8px;
padding: 10px 14px;
font-size: 14px;
margin-bottom: 16px;
}
label {
display: block;
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
letter-spacing: 0.05em;
text-transform: uppercase;
}
input[type="email"] {
width: 100%;
padding: 14px 16px;
border: 1.5px solid #e5e7eb;
border-radius: 10px;
font-size: 15px;
background: #f9fafb;
outline: none;
transition: border 0.2s;
}
input[type="email"]:focus { border-color: #2d6a4f; background: white; }
.btn-submit {
width: 100%;
margin-top: 20px;
padding: 14px;
background: #2d6a4f;
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-submit:hover { background: #1e4d38; }
.back-link {
display: block;
text-align: center;
margin-top: 18px;
font-size: 14px;
color: #2d6a4f;
text-decoration: none;
font-weight: 500;
}
.back-link:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="card">
<div class="card-header">
<h1>SiPakarTebu</h1>
<p>Sistem Diagnosis Penyakit Tanaman Tebu</p>
</div>
<div class="card-body">
<h2>Lupa Password?</h2>
<p class="subtitle">Masukkan email terdaftar untuk menerima kode OTP</p>
@if(session('status'))
<div class="alert-success">{{ session('status') }}</div>
@endif
@if($errors->any())
<div class="alert-error">{{ $errors->first() }}</div>
@endif
<form method="POST" action="{{ route('password.send-otp') }}">
@csrf
<label>Email</label>
<input type="email" name="email" value="{{ old('email') }}" placeholder="contoh@email.com" required>
<button type="submit" class="btn-submit">Kirim OTP</button>
</form>
<a href="{{ route('login') }}" class="back-link"> Kembali ke Login</a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,386 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - SiPakarTebu</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;400i&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<style>
body { font-family: 'DM Sans', sans-serif; background: #f8f4ee; }
input:focus { border-color: #40916c !important; outline: none; box-shadow: 0 0 0 3px rgba(64,145,108,.12) !important; }
</style>
</head>
<body class="bg-[#f8f4ee]">
<div class="min-h-screen flex items-stretch">
<!-- PANEL KIRI Ilustrasi -->
<div class="hidden lg:flex lg:w-1/2 relative overflow-hidden"
style="background: linear-gradient(160deg, #1a3a2a 0%, #2d6a4f 60%, #40916c 100%);">
<!-- Gambar ilustrasi -->
<!-- Ilustrasi SVG kebun tebu -->
<div class="absolute inset-0 flex items-end justify-center overflow-hidden">
<svg viewBox="0 0 600 500" xmlns="http://www.w3.org/2000/svg"
style="width:100%;height:100%;object-fit:cover;opacity:0.35;">
<!-- Langit -->
<rect width="600" height="500" fill="#1a3a2a"/>
<!-- Matahari -->
<circle cx="500" cy="80" r="45" fill="#f4d03f" opacity="0.4"/>
<circle cx="500" cy="80" r="35" fill="#f6e58d" opacity="0.3"/>
<!-- Awan -->
<ellipse cx="120" cy="70" rx="50" ry="20" fill="white" opacity="0.1"/>
<ellipse cx="150" cy="60" rx="40" ry="18" fill="white" opacity="0.1"/>
<ellipse cx="90" cy="65" rx="35" ry="16" fill="white" opacity="0.1"/>
<ellipse cx="380" cy="50" rx="45" ry="18" fill="white" opacity="0.08"/>
<ellipse cx="410" cy="42" rx="35" ry="16" fill="white" opacity="0.08"/>
<!-- Tanah -->
<ellipse cx="300" cy="490" rx="340" ry="60" fill="#2d4a1e" opacity="0.8"/>
<rect x="0" y="440" width="600" height="80" fill="#1e3a12" opacity="0.9"/>
<!-- ===== BATANG TEBU KIRI JAUH ===== -->
<g opacity="0.5">
<rect x="40" y="120" width="8" height="340" rx="4" fill="#4a7c2f"/>
<ellipse cx="44" cy="110" rx="18" ry="6" fill="#52a833" transform="rotate(-20 44 110)"/>
<ellipse cx="44" cy="160" rx="22" ry="6" fill="#4a9a2e" transform="rotate(25 44 160)"/>
<ellipse cx="44" cy="210" rx="20" ry="5" fill="#52a833" transform="rotate(-15 44 210)"/>
<ellipse cx="44" cy="260" rx="19" ry="5" fill="#4a9a2e" transform="rotate(20 44 260)"/>
<rect x="80" y="160" width="7" height="300" rx="3" fill="#4a7c2f"/>
<ellipse cx="83" cy="150" rx="16" ry="5" fill="#52a833" transform="rotate(22 83 150)"/>
<ellipse cx="83" cy="200" rx="20" ry="5" fill="#4a9a2e" transform="rotate(-18 83 200)"/>
<ellipse cx="83" cy="250" rx="18" ry="5" fill="#52a833" transform="rotate(15 83 250)"/>
</g>
<!-- ===== BATANG TEBU KIRI DEKAT ===== -->
<g opacity="0.75">
<rect x="110" y="90" width="10" height="380" rx="5" fill="#5a8f35"/>
<!-- Ruas-ruas batang -->
<line x1="110" y1="160" x2="120" y2="160" stroke="#3d6b22" stroke-width="1.5"/>
<line x1="110" y1="220" x2="120" y2="220" stroke="#3d6b22" stroke-width="1.5"/>
<line x1="110" y1="280" x2="120" y2="280" stroke="#3d6b22" stroke-width="1.5"/>
<line x1="110" y1="340" x2="120" y2="340" stroke="#3d6b22" stroke-width="1.5"/>
<!-- Daun -->
<ellipse cx="115" cy="80" rx="28" ry="7" fill="#68b83f" transform="rotate(-30 115 80)"/>
<ellipse cx="115" cy="130" rx="32" ry="7" fill="#5aa835" transform="rotate(28 115 130)"/>
<ellipse cx="115" cy="185" rx="30" ry="7" fill="#68b83f" transform="rotate(-22 115 185)"/>
<ellipse cx="115" cy="240" rx="28" ry="6" fill="#5aa835" transform="rotate(25 115 240)"/>
<ellipse cx="115" cy="295" rx="26" ry="6" fill="#68b83f" transform="rotate(-18 115 295)"/>
</g>
<!-- ===== BATANG TEBU TENGAH ===== -->
<g opacity="0.8">
<rect x="210" y="70" width="12" height="400" rx="6" fill="#5a8f35"/>
<line x1="210" y1="150" x2="222" y2="150" stroke="#3d6b22" stroke-width="2"/>
<line x1="210" y1="220" x2="222" y2="220" stroke="#3d6b22" stroke-width="2"/>
<line x1="210" y1="290" x2="222" y2="290" stroke="#3d6b22" stroke-width="2"/>
<line x1="210" y1="360" x2="222" y2="360" stroke="#3d6b22" stroke-width="2"/>
<ellipse cx="216" cy="58" rx="34" ry="8" fill="#72c44a" transform="rotate(-35 216 58)"/>
<ellipse cx="216" cy="115" rx="38" ry="8" fill="#65b540" transform="rotate(30 216 115)"/>
<ellipse cx="216" cy="175" rx="36" ry="8" fill="#72c44a" transform="rotate(-25 216 175)"/>
<ellipse cx="216" cy="235" rx="34" ry="7" fill="#65b540" transform="rotate(28 216 235)"/>
<ellipse cx="216" cy="295" rx="32" ry="7" fill="#72c44a" transform="rotate(-20 216 295)"/>
<ellipse cx="216" cy="355" rx="30" ry="6" fill="#65b540" transform="rotate(22 216 355)"/>
</g>
<!-- ===== BATANG TEBU KANAN ===== -->
<g opacity="0.7">
<rect x="470" y="100" width="11" height="380" rx="5" fill="#5a8f35"/>
<line x1="470" y1="170" x2="481" y2="170" stroke="#3d6b22" stroke-width="1.5"/>
<line x1="470" y1="240" x2="481" y2="240" stroke="#3d6b22" stroke-width="1.5"/>
<line x1="470" y1="310" x2="481" y2="310" stroke="#3d6b22" stroke-width="1.5"/>
<ellipse cx="475" cy="90" rx="30" ry="7" fill="#68b83f" transform="rotate(32 475 90)"/>
<ellipse cx="475" cy="145" rx="34" ry="7" fill="#5aa835" transform="rotate(-28 475 145)"/>
<ellipse cx="475" cy="200" rx="32" ry="7" fill="#68b83f" transform="rotate(24 475 200)"/>
<ellipse cx="475" cy="255" rx="30" ry="6" fill="#5aa835" transform="rotate(-20 475 255)"/>
<ellipse cx="475" cy="310" rx="28" ry="6" fill="#68b83f" transform="rotate(18 475 310)"/>
<rect x="520" y="130" width="9" height="350" rx="4" fill="#4a7c2f"/>
<ellipse cx="524" cy="120" rx="26" ry="6" fill="#5aa835" transform="rotate(-25 524 120)"/>
<ellipse cx="524" cy="175" rx="30" ry="6" fill="#4a9a2e" transform="rotate(22 524 175)"/>
<ellipse cx="524" cy="230" rx="28" ry="6" fill="#5aa835" transform="rotate(-18 524 230)"/>
<ellipse cx="524" cy="285" rx="26" ry="5" fill="#4a9a2e" transform="rotate(15 524 285)"/>
</g>
<!-- ===== SOSOK PAKAR ===== -->
<g transform="translate(270, 180)">
<!-- Bayangan -->
<ellipse cx="65" cy="255" rx="45" ry="12" fill="#0d2010" opacity="0.5"/>
<!-- Sepatu -->
<ellipse cx="48" cy="252" rx="18" ry="7" fill="#3d2b1f"/>
<ellipse cx="82" cy="252" rx="16" ry="7" fill="#3d2b1f"/>
<!-- Celana -->
<rect x="38" y="185" width="24" height="70" rx="5" fill="#4a5568"/>
<rect x="68" y="185" width="22" height="70" rx="5" fill="#4a5568"/>
<rect x="36" y="180" width="56" height="15" rx="4" fill="#3d4a5c"/>
<!-- Baju lapangan (coklat khaki) -->
<rect x="30" y="100" width="72" height="85" rx="8" fill="#8b7355"/>
<!-- Saku baju -->
<rect x="35" y="115" width="18" height="14" rx="3" fill="#7a6448" opacity="0.8"/>
<rect x="79" y="115" width="18" height="14" rx="3" fill="#7a6448" opacity="0.8"/>
<!-- Kerah -->
<polygon points="66,100 66,118 58,108" fill="#7a6448"/>
<polygon points="66,100 66,118 74,108" fill="#7a6448"/>
<!-- Lengan kiri (memegang kaca pembesar) -->
<rect x="8" y="105" width="24" height="60" rx="8" fill="#8b7355" transform="rotate(-25 8 105)"/>
<!-- Tangan kiri -->
<circle cx="2" cy="148" r="10" fill="#c8956c"/>
<!-- Kaca pembesar -->
<circle cx="-15" cy="130" r="20" fill="none" stroke="#aaa" stroke-width="4" opacity="0.9"/>
<circle cx="-15" cy="130" r="18" fill="rgba(150,220,255,0.15)"/>
<line x1="-1" y1="144" x2="8" y2="155" stroke="#888" stroke-width="4" stroke-linecap="round"/>
<!-- Lengan kanan (memegang buku catatan) -->
<rect x="96" y="108" width="22" height="55" rx="8" fill="#8b7355" transform="rotate(20 96 108)"/>
<!-- Tangan kanan -->
<circle cx="122" cy="150" r="10" fill="#c8956c"/>
<!-- Buku catatan -->
<rect x="118" y="135" width="28" height="36" rx="4" fill="#f5f5f0" transform="rotate(10 118 135)"/>
<line x1="122" y1="143" x2="143" y2="140" stroke="#ccc" stroke-width="1.5"/>
<line x1="122" y1="150" x2="143" y2="147" stroke="#ccc" stroke-width="1.5"/>
<line x1="122" y1="157" x2="143" y2="154" stroke="#ccc" stroke-width="1.5"/>
<!-- Leher -->
<rect x="58" y="82" width="16" height="22" rx="5" fill="#c8956c"/>
<!-- Kepala -->
<circle cx="66" cy="68" r="32" fill="#c8956c"/>
<!-- Rambut -->
<ellipse cx="66" cy="40" rx="32" ry="16" fill="#2c1810"/>
<ellipse cx="40" cy="58" rx="10" ry="18" fill="#2c1810"/>
<ellipse cx="92" cy="58" rx="10" ry="18" fill="#2c1810"/>
<!-- Wajah -->
<!-- Kacamata -->
<circle cx="56" cy="68" r="10" fill="none" stroke="#555" stroke-width="2.5"/>
<circle cx="76" cy="68" r="10" fill="none" stroke="#555" stroke-width="2.5"/>
<circle cx="56" cy="68" r="9" fill="rgba(200,230,255,0.2)"/>
<circle cx="76" cy="68" r="9" fill="rgba(200,230,255,0.2)"/>
<line x1="66" y1="68" x2="66" y2="68" stroke="#555" stroke-width="2"/>
<line x1="46" y1="66" x2="42" y2="63" stroke="#555" stroke-width="2" stroke-linecap="round"/>
<line x1="86" y1="66" x2="90" y2="63" stroke="#555" stroke-width="2" stroke-linecap="round"/>
<!-- Alis -->
<path d="M48 58 Q56 54 64 57" stroke="#2c1810" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<path d="M68 57 Q76 54 84 58" stroke="#2c1810" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<!-- Senyum -->
<path d="M54 80 Q66 90 78 80" stroke="#8b5e3c" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<!-- Hidung -->
<ellipse cx="66" cy="74" rx="3" ry="4" fill="#b8845c" opacity="0.6"/>
<!-- Name tag -->
<rect x="45" y="128" width="42" height="18" rx="3" fill="white" opacity="0.9"/>
<text x="66" y="140" text-anchor="middle" font-size="6" fill="#2d4a1e" font-family="Arial" font-weight="bold">PAKAR TEBU</text>
</g>
<!-- Rumput / tanaman kecil di depan -->
<g opacity="0.6">
<ellipse cx="30" cy="450" rx="15" ry="5" fill="#4a7c2f" transform="rotate(-20 30 450)"/>
<ellipse cx="50" cy="445" rx="18" ry="5" fill="#52a833" transform="rotate(15 50 445)"/>
<ellipse cx="170" cy="448" rx="14" ry="4" fill="#4a7c2f" transform="rotate(22 170 448)"/>
<ellipse cx="420" cy="452" rx="16" ry="5" fill="#52a833" transform="rotate(-18 420 452)"/>
<ellipse cx="560" cy="447" rx="15" ry="5" fill="#4a7c2f" transform="rotate(20 560 447)"/>
<ellipse cx="580" cy="455" rx="12" ry="4" fill="#52a833" transform="rotate(-12 580 455)"/>
</g>
<!-- Stetoskop di leher pakar -->
<g transform="translate(310, 270)" opacity="0.9">
<path d="M20 0 C20 0 35 0 35 15 C35 30 25 33 25 45" stroke="#c0c0c0" stroke-width="3" stroke-linecap="round" fill="none"/>
<circle cx="25" cy="48" r="6" stroke="#c0c0c0" stroke-width="2.5" fill="rgba(200,200,200,0.3)"/>
<line x1="15" y1="0" x2="26" y2="0" stroke="#c0c0c0" stroke-width="3" stroke-linecap="round"/>
</g>
<!-- Bendera Indonesia kecil di background -->
<g transform="translate(490, 140)" opacity="0.3">
<line x1="0" y1="0" x2="0" y2="60" stroke="#8b7355" stroke-width="2"/>
<rect x="0" y="0" width="30" height="10" fill="#dc143c"/>
<rect x="0" y="10" width="30" height="10" fill="white"/>
</g>
</svg>
</div>
<!-- Overlay gradient bawah -->
<div class="absolute inset-0"
style="background: linear-gradient(to top, rgba(26,58,42,0.92) 0%, rgba(26,58,42,0.4) 50%, rgba(26,58,42,0.15) 100%);">
</div>
<!-- Konten teks di atas gambar -->
<div class="relative z-10 flex flex-col justify-between p-10 w-full">
<!-- Logo -->
<a href="/" class="flex items-center gap-3 text-white no-underline">
<div class="w-10 h-10 rounded-xl flex items-center justify-center"
style="background:rgba(255,255,255,.15);backdrop-filter:blur(4px);">
<svg width="26" height="26" viewBox="0 0 32 32" fill="none">
<ellipse cx="11" cy="15" rx="5" ry="11" fill="rgba(255,255,255,0.25)" stroke="white" stroke-width="1.3" transform="rotate(-15 11 15)"/>
<line x1="11" y1="6" x2="10" y2="24" stroke="white" stroke-width="1" stroke-linecap="round" opacity="0.8" transform="rotate(-15 11 15)"/>
<line x1="10" y1="13" x2="7" y2="17" stroke="white" stroke-width="0.8" stroke-linecap="round" opacity="0.6"/>
<line x1="10" y1="17" x2="7.5" y2="21" stroke="white" stroke-width="0.8" stroke-linecap="round" opacity="0.6"/>
<path d="M19 7 C19 7 24 7 24 12 C24 17 20 18 20 22" stroke="white" stroke-width="1.5" stroke-linecap="round" fill="none"/>
<circle cx="20" cy="24.5" r="3" stroke="white" stroke-width="1.3" fill="rgba(255,255,255,0.2)"/>
<line x1="18" y1="7" x2="21" y2="7" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<span class="text-xl font-bold" style="font-family:'Playfair Display',serif;">SiPakarTebu</span>
</a>
<!-- Quote / Teks menarik -->
<div class="text-white">
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full mb-4"
style="background:rgba(116,198,157,.25);border:1px solid rgba(116,198,157,.4);">
<span style="color:#74c69d;font-size:.75rem;font-weight:600;letter-spacing:.08em;">🌿 SISTEM PAKAR TEBU</span>
</div>
<h2 style="font-family:'Playfair Display',serif;font-size:2rem;line-height:1.25;margin-bottom:1rem;">
Deteksi Dini,<br>
<em style="color:#74c69d;">Panen Lebih Optimal</em>
</h2>
<p style="color:rgba(255,255,255,.7);font-size:.95rem;line-height:1.7;max-width:360px;margin-bottom:2rem;">
Kenali penyakit tanaman tebu lebih cepat dengan teknologi Certainty Factor. Diagnosis akurat, penanganan tepat, hasil panen maksimal.
</p>
<!-- 3 poin keunggulan -->
<div style="display:flex;flex-direction:column;gap:.75rem;">
<div style="display:flex;align-items:center;gap:.75rem;">
<div style="width:32px;height:32px;border-radius:50%;background:rgba(116,198,157,.2);border:1px solid rgba(116,198,157,.4);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="fas fa-microscope" style="color:#74c69d;font-size:.75rem;"></i>
</div>
<span style="color:rgba(255,255,255,.85);font-size:.875rem;">Diagnosis akurat dari 10 jenis penyakit tebu</span>
</div>
<div style="display:flex;align-items:center;gap:.75rem;">
<div style="width:32px;height:32px;border-radius:50%;background:rgba(116,198,157,.2);border:1px solid rgba(116,198,157,.4);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="fas fa-bolt" style="color:#74c69d;font-size:.75rem;"></i>
</div>
<span style="color:rgba(255,255,255,.85);font-size:.875rem;">Hasil diagnosis dalam hitungan menit</span>
</div>
<div style="display:flex;align-items:center;gap:.75rem;">
<div style="width:32px;height:32px;border-radius:50%;background:rgba(116,198,157,.2);border:1px solid rgba(116,198,157,.4);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="fas fa-shield-alt" style="color:#74c69d;font-size:.75rem;"></i>
</div>
<span style="color:rgba(255,255,255,.85);font-size:.875rem;">Rekomendasi penanganan dari pakar kebun</span>
</div>
</div>
</div>
</div>
</div>
<!-- PANEL KANAN Form Login -->
<div class="w-full lg:w-1/2 flex items-center justify-center p-6 lg:p-12">
<div class="w-full max-w-md">
<!-- Logo mobile (hanya muncul di layar kecil) -->
<div class="lg:hidden text-center mb-8">
<div class="w-14 h-14 rounded-2xl flex items-center justify-center mx-auto mb-3"
style="background:linear-gradient(135deg,#2d6a4f,#40916c);">
<svg width="30" height="30" viewBox="0 0 32 32" fill="none">
<ellipse cx="11" cy="15" rx="5" ry="11" fill="rgba(255,255,255,0.25)" stroke="white" stroke-width="1.3" transform="rotate(-15 11 15)"/>
<line x1="11" y1="6" x2="10" y2="24" stroke="white" stroke-width="1" stroke-linecap="round" opacity="0.8" transform="rotate(-15 11 15)"/>
<line x1="10" y1="13" x2="7" y2="17" stroke="white" stroke-width="0.8" stroke-linecap="round" opacity="0.6"/>
<line x1="10" y1="17" x2="7.5" y2="21" stroke="white" stroke-width="0.8" stroke-linecap="round" opacity="0.6"/>
<path d="M19 7 C19 7 24 7 24 12 C24 17 20 18 20 22" stroke="white" stroke-width="1.5" stroke-linecap="round" fill="none"/>
<circle cx="20" cy="24.5" r="3" stroke="white" stroke-width="1.3" fill="rgba(255,255,255,0.2)"/>
<line x1="18" y1="7" x2="21" y2="7" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<h1 class="text-2xl font-bold" style="font-family:'Playfair Display',serif;color:#1a3a2a;">SiPakarTebu</h1>
<p class="text-sm" style="color:#5a7a67;">Sistem Diagnosis Penyakit Tebu</p>
</div>
<!-- Judul form -->
<div class="mb-8">
<h2 class="text-3xl font-bold mb-2" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Selamat Datang</h2>
<p class="text-sm" style="color:#5a7a67;">Masuk untuk melanjutkan ke SiPakarTebu</p>
</div>
@if ($errors->any())
<div class="px-4 py-3 rounded-xl mb-6 text-sm flex items-center gap-2"
style="background:#fff0f0;color:#e63946;border:1px solid #ffd6d6;">
<i class="fas fa-exclamation-circle"></i>
{{ $errors->first() }}
</div>
@endif
<form method="POST" action="{{ url('/login') }}">
@csrf
<div class="mb-5">
<label class="block mb-2" style="color:#1a2e22;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;">Email</label>
<div class="relative">
<i class="fas fa-envelope absolute left-4 top-1/2 -translate-y-1/2" style="color:#a0b4a8;font-size:.85rem;"></i>
<input type="email" name="email" value="{{ old('email') }}"
class="w-full pl-11 pr-4 py-3.5 border rounded-xl transition text-sm"
style="border-color:#e5e7eb;background:#f9fafb;color:#1a2e22;"
placeholder="nama@email.com" required>
</div>
</div>
<div class="mb-7">
<div class="flex items-center justify-between mb-2">
<label style="color:#1a2e22;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;">Password</label>
<a href="{{ route('password.email') }}"
style="font-size:.78rem;color:#2d6a4f;font-weight:600;text-decoration:none;">
Lupa Password?
</a>
</div>
<div class="relative">
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2" style="color:#a0b4a8;font-size:.85rem;"></i>
<input type="password" name="password" id="passwordInput"
class="w-full pl-11 pr-12 py-3.5 border rounded-xl transition text-sm"
style="border-color:#e5e7eb;background:#f9fafb;color:#1a2e22;"
placeholder="masukkan password" required>
<button type="button" onclick="togglePassword()"
class="absolute right-4 top-1/2 -translate-y-1/2"
style="color:#a0b4a8;background:none;border:none;cursor:pointer;">
<i class="fas fa-eye" id="eyeIcon" style="font-size:.85rem;"></i>
</button>
</div>
</div>
<button type="submit"
class="w-full py-3.5 rounded-xl font-semibold text-white text-sm transition"
style="background:linear-gradient(135deg,#1a3a2a,#2d6a4f);box-shadow:0 6px 20px rgba(26,58,42,.3);"
onmouseover="this.style.opacity='.9'" onmouseout="this.style.opacity='1'">
<i class="fas fa-sign-in-alt mr-2"></i>Masuk ke SiPakarTebu
</button>
</form>
<div class="mt-8 pt-6 text-center" style="border-top:1px solid #ede8df;">
<p class="text-sm" style="color:#8fa89a;">
Belum punya akun?
<a href="{{ route('register') }}" style="color:#2d6a4f;font-weight:600;text-decoration:none;">
Daftar Gratis
</a>
</p>
</div>
</div>
</div>
</div>
<script>
function togglePassword() {
const input = document.getElementById('passwordInput');
const icon = document.getElementById('eyeIcon');
if (input.type === 'password') {
input.type = 'text';
icon.classList.replace('fa-eye', 'fa-eye-slash');
} else {
input.type = 'password';
icon.classList.replace('fa-eye-slash', 'fa-eye');
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,158 @@
{{-- resources/views/auth/otp-verify.blade.php --}}
@extends('layouts.auth')
@section('title', 'Verifikasi OTP')
@section('content')
{{-- Steps indicator --}}
<div class="steps">
<div class="step done"><div class="step-dot"></div> Email</div>
<div class="step-line done"></div>
<div class="step active"><div class="step-dot">2</div> Kode OTP</div>
<div class="step-line"></div>
<div class="step"><div class="step-dot">3</div> Password Baru</div>
</div>
<div class="page-title">Masukkan Kode OTP</div>
<p class="page-sub">
Kode 6 digit telah dikirim ke email kamu. Berlaku selama
<strong id="timerDisplay">05:00</strong>.
</p>
{{-- Alert success --}}
@if (session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
{{-- Alert error --}}
@if ($errors->any())
<div class="alert alert-error">{{ $errors->first() }}</div>
@endif
<div class="info-box">
<span>📧</span>
<span>Cek folder <strong>Spam</strong> jika kode tidak muncul dalam 1 menit.</span>
</div>
{{-- Form verifikasi OTP --}}
<form method="POST" action="{{ route('password.otp.verify') }}" id="otpForm">
@csrf
<div class="form-group">
<label>Kode Verifikasi</label>
<div class="otp-group" id="otpGroup">
@for ($i = 0; $i < 6; $i++)
<input
type="text"
maxlength="1"
class="otp-input"
inputmode="numeric"
pattern="[0-9]"
autocomplete="off"
/>
@endfor
</div>
{{-- Hidden input yang akan dikirim --}}
<input type="hidden" name="otp" id="otpHidden" />
@error('otp')
<span class="invalid-feedback" style="display:block; margin-top:8px;">{{ $message }}</span>
@enderror
</div>
<button type="submit" class="btn btn-primary" id="verifyBtn" disabled>
Verifikasi Kode
</button>
</form>
{{-- Kirim ulang OTP --}}
<div class="resend">
Tidak menerima kode?
<form method="POST" action="{{ route('password.otp.resend') }}" style="display:inline;">
@csrf
<button type="submit" id="resendBtn" disabled>
Kirim Ulang (<span id="resendTimer">60</span>s)
</button>
</form>
</div>
<a href="{{ route('password.email') }}" class="btn btn-ghost" style="display:block; text-align:center; margin-top:12px; text-decoration:none;">
Ganti Email
</a>
@endsection
@push('scripts')
<script>
// ── OTP Timer (5 menit) ──
let timerInterval;
(function startOTPTimer() {
let seconds = 300;
const display = document.getElementById('timerDisplay');
const tick = () => {
const m = String(Math.floor(seconds / 60)).padStart(2, '0');
const s = String(seconds % 60).padStart(2, '0');
display.textContent = m + ':' + s;
if (seconds-- <= 0) {
clearInterval(timerInterval);
display.textContent = 'Kedaluwarsa';
display.style.color = '#e74c3c';
}
};
tick();
timerInterval = setInterval(tick, 1000);
})();
// ── Resend countdown (60 detik) ──
(function startResendTimer() {
const btn = document.getElementById('resendBtn');
const span = document.getElementById('resendTimer');
btn.disabled = true;
let t = 60;
const interval = setInterval(() => {
span.textContent = --t;
if (t <= 0) {
clearInterval(interval);
btn.disabled = false;
btn.innerHTML = 'Kirim Ulang';
}
}, 1000);
})();
// ── OTP Input UX ──
const inputs = document.querySelectorAll('.otp-input');
const hidden = document.getElementById('otpHidden');
const verBtn = document.getElementById('verifyBtn');
function syncHidden() {
hidden.value = [...inputs].map(i => i.value).join('');
verBtn.disabled = hidden.value.length !== 6;
}
inputs.forEach((inp, idx) => {
inp.addEventListener('input', () => {
inp.value = inp.value.replace(/\D/g, '').slice(-1);
if (inp.value && idx < inputs.length - 1) inputs[idx + 1].focus();
syncHidden();
});
inp.addEventListener('keydown', e => {
if (e.key === 'Backspace' && !inp.value && idx > 0) {
inputs[idx - 1].focus();
}
});
inp.addEventListener('paste', e => {
e.preventDefault();
const pasted = (e.clipboardData || window.clipboardData)
.getData('text').replace(/\D/g, '').slice(0, 6);
pasted.split('').forEach((ch, i) => { if (inputs[i]) inputs[i].value = ch; });
syncHidden();
inputs[Math.min(pasted.length, inputs.length - 1)].focus();
});
});
// Auto-focus input pertama
inputs[0].focus();
</script>
@endpush

View File

@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verifikasi OTP - SiPakarTebu</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
background-color: #f5f5f0;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Segoe UI', sans-serif;
}
.card {
background: white;
border-radius: 16px;
overflow: hidden;
width: 100%;
max-width: 460px;
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
}
.card-header {
background: #2d6a4f;
padding: 40px 20px 30px;
text-align: center;
color: white;
}
.card-header h1 { font-size: 28px; font-weight: 700; }
.card-header p { font-size: 14px; opacity: 0.85; margin-top: 4px; }
.card-body { padding: 36px 40px; }
.card-body h2 { font-size: 22px; font-weight: 700; color: #1a1a1a; }
.card-body .subtitle { color: #666; font-size: 14px; margin-top: 4px; margin-bottom: 24px; }
.alert-error {
background: #fef2f2;
border: 1px solid #fca5a5;
color: #dc2626;
border-radius: 8px;
padding: 10px 14px;
font-size: 14px;
margin-bottom: 16px;
}
.alert-success {
background: #f0fdf4;
border: 1px solid #86efac;
color: #16a34a;
border-radius: 8px;
padding: 10px 14px;
font-size: 14px;
margin-bottom: 16px;
}
label {
display: block;
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
letter-spacing: 0.05em;
text-transform: uppercase;
}
input[type="text"] {
width: 100%;
padding: 14px 16px;
border: 1.5px solid #e5e7eb;
border-radius: 10px;
font-size: 22px;
letter-spacing: 0.3em;
text-align: center;
background: #f9fafb;
outline: none;
transition: border 0.2s;
}
input[type="text"]:focus { border-color: #2d6a4f; background: white; }
.btn-submit {
width: 100%;
margin-top: 20px;
padding: 14px;
background: #2d6a4f;
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-submit:hover { background: #1e4d38; }
.btn-resend {
width: 100%;
margin-top: 10px;
padding: 12px;
background: transparent;
color: #2d6a4f;
border: 1.5px solid #2d6a4f;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-resend:hover { background: #f0fdf4; }
.back-link {
display: block;
text-align: center;
margin-top: 18px;
font-size: 14px;
color: #2d6a4f;
text-decoration: none;
font-weight: 500;
}
.back-link:hover { text-decoration: underline; }
</style>
@if(session('otp_debug'))
<div class="mb-4 px-4 py-3 rounded-xl text-sm font-semibold text-center"
style="background:#fff9c4;color:#92400e;border:1px solid #fcd34d;">
<i class="fas fa-key mr-2"></i>{{ session('otp_debug') }}
</div>
@endif
</head>
<body>
<div class="card">
<div class="card-header">
<h1>CaneDoc</h1>
<p>Sistem Diagnosis Penyakit Tanaman Tebu</p>
</div>
<div class="card-body">
<h2>Verifikasi OTP</h2>
<p class="subtitle">Masukkan kode 6 digit yang dikirim ke email kamu</p>
@if(session('otp_debug'))
<div class="alert-success">{{ session('otp_debug') }}</div>
@endif
@if(session('status'))
<div class="alert-success">{{ session('status') }}</div>
@endif
@if($errors->any())
<div class="alert-error">{{ $errors->first() }}</div>
@endif
<form method="POST" action="{{ route('password.otp.verify') }}">
@csrf
<label>Kode OTP</label>
<input type="text" name="otp" maxlength="6" placeholder="······" required autofocus>
<button type="submit" class="btn-submit">Verifikasi</button>
</form>
<form method="POST" action="{{ route('password.otp.resend') }}" style="margin-top:10px">
@csrf
<button type="submit" class="btn-resend">Kirim Ulang OTP</button>
</form>
<a href="{{ route('password.email') }}" class="back-link"> Kembali</a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,448 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - SiPakarTebu</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;400i&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<style>
body { font-family: 'DM Sans', sans-serif; background: #f8f4ee; }
input:focus { border-color: #40916c !important; outline: none; box-shadow: 0 0 0 3px rgba(64,145,108,.12) !important; }
.role-card { cursor: pointer; transition: all .2s; border: 2px solid #e5e7eb; border-radius: 12px; padding: 12px 14px; display: flex; align-items: center; gap: 10px; background: #f9fafb; }
.role-card:hover { border-color: #40916c; background: #f0fdf4; }
.role-card input[type="radio"] { display: none; }
.role-card.selected { border-color: #2d6a4f; background: #f0fdf4; }
.role-card .radio-dot { width: 17px; height: 17px; border-radius: 50%; border: 2px solid #c8d8cc; flex-shrink: 0; display: flex; align-items: center; justify-content: center; transition: all .2s; }
.role-card.selected .radio-dot { border-color: #2d6a4f; background: #2d6a4f; }
.role-card.selected .radio-dot::after { content: ''; width: 5px; height: 5px; background: white; border-radius: 50%; display: block; }
</style>
</head>
<body class="bg-[#f8f4ee]">
<div class="min-h-screen flex items-stretch">
<!-- PANEL KIRI Ilustrasi -->
<div class="hidden lg:flex lg:w-1/2 relative overflow-hidden sticky top-0 h-screen"
style="background: linear-gradient(160deg, #1a3a2a 0%, #2d6a4f 60%, #40916c 100%);">
<!-- Ilustrasi SVG kebun tebu -->
<div class="absolute inset-0 flex items-end justify-center overflow-hidden">
<svg viewBox="0 0 600 500" xmlns="http://www.w3.org/2000/svg"
style="width:100%;height:100%;object-fit:cover;opacity:0.35;">
<!-- Langit -->
<rect width="600" height="500" fill="#1a3a2a"/>
<!-- Matahari -->
<circle cx="500" cy="80" r="45" fill="#f4d03f" opacity="0.4"/>
<circle cx="500" cy="80" r="35" fill="#f6e58d" opacity="0.3"/>
<!-- Awan -->
<ellipse cx="120" cy="70" rx="50" ry="20" fill="white" opacity="0.1"/>
<ellipse cx="150" cy="60" rx="40" ry="18" fill="white" opacity="0.1"/>
<ellipse cx="90" cy="65" rx="35" ry="16" fill="white" opacity="0.1"/>
<ellipse cx="380" cy="50" rx="45" ry="18" fill="white" opacity="0.08"/>
<ellipse cx="410" cy="42" rx="35" ry="16" fill="white" opacity="0.08"/>
<!-- Tanah -->
<ellipse cx="300" cy="490" rx="340" ry="60" fill="#2d4a1e" opacity="0.8"/>
<rect x="0" y="440" width="600" height="80" fill="#1e3a12" opacity="0.9"/>
<!-- ===== BATANG TEBU KIRI JAUH ===== -->
<g opacity="0.5">
<rect x="40" y="120" width="8" height="340" rx="4" fill="#4a7c2f"/>
<ellipse cx="44" cy="110" rx="18" ry="6" fill="#52a833" transform="rotate(-20 44 110)"/>
<ellipse cx="44" cy="160" rx="22" ry="6" fill="#4a9a2e" transform="rotate(25 44 160)"/>
<ellipse cx="44" cy="210" rx="20" ry="5" fill="#52a833" transform="rotate(-15 44 210)"/>
<ellipse cx="44" cy="260" rx="19" ry="5" fill="#4a9a2e" transform="rotate(20 44 260)"/>
<rect x="80" y="160" width="7" height="300" rx="3" fill="#4a7c2f"/>
<ellipse cx="83" cy="150" rx="16" ry="5" fill="#52a833" transform="rotate(22 83 150)"/>
<ellipse cx="83" cy="200" rx="20" ry="5" fill="#4a9a2e" transform="rotate(-18 83 200)"/>
<ellipse cx="83" cy="250" rx="18" ry="5" fill="#52a833" transform="rotate(15 83 250)"/>
</g>
<!-- ===== BATANG TEBU KIRI DEKAT ===== -->
<g opacity="0.75">
<rect x="110" y="90" width="10" height="380" rx="5" fill="#5a8f35"/>
<!-- Ruas-ruas batang -->
<line x1="110" y1="160" x2="120" y2="160" stroke="#3d6b22" stroke-width="1.5"/>
<line x1="110" y1="220" x2="120" y2="220" stroke="#3d6b22" stroke-width="1.5"/>
<line x1="110" y1="280" x2="120" y2="280" stroke="#3d6b22" stroke-width="1.5"/>
<line x1="110" y1="340" x2="120" y2="340" stroke="#3d6b22" stroke-width="1.5"/>
<!-- Daun -->
<ellipse cx="115" cy="80" rx="28" ry="7" fill="#68b83f" transform="rotate(-30 115 80)"/>
<ellipse cx="115" cy="130" rx="32" ry="7" fill="#5aa835" transform="rotate(28 115 130)"/>
<ellipse cx="115" cy="185" rx="30" ry="7" fill="#68b83f" transform="rotate(-22 115 185)"/>
<ellipse cx="115" cy="240" rx="28" ry="6" fill="#5aa835" transform="rotate(25 115 240)"/>
<ellipse cx="115" cy="295" rx="26" ry="6" fill="#68b83f" transform="rotate(-18 115 295)"/>
</g>
<!-- ===== BATANG TEBU TENGAH ===== -->
<g opacity="0.8">
<rect x="210" y="70" width="12" height="400" rx="6" fill="#5a8f35"/>
<line x1="210" y1="150" x2="222" y2="150" stroke="#3d6b22" stroke-width="2"/>
<line x1="210" y1="220" x2="222" y2="220" stroke="#3d6b22" stroke-width="2"/>
<line x1="210" y1="290" x2="222" y2="290" stroke="#3d6b22" stroke-width="2"/>
<line x1="210" y1="360" x2="222" y2="360" stroke="#3d6b22" stroke-width="2"/>
<ellipse cx="216" cy="58" rx="34" ry="8" fill="#72c44a" transform="rotate(-35 216 58)"/>
<ellipse cx="216" cy="115" rx="38" ry="8" fill="#65b540" transform="rotate(30 216 115)"/>
<ellipse cx="216" cy="175" rx="36" ry="8" fill="#72c44a" transform="rotate(-25 216 175)"/>
<ellipse cx="216" cy="235" rx="34" ry="7" fill="#65b540" transform="rotate(28 216 235)"/>
<ellipse cx="216" cy="295" rx="32" ry="7" fill="#72c44a" transform="rotate(-20 216 295)"/>
<ellipse cx="216" cy="355" rx="30" ry="6" fill="#65b540" transform="rotate(22 216 355)"/>
</g>
<!-- ===== BATANG TEBU KANAN ===== -->
<g opacity="0.7">
<rect x="470" y="100" width="11" height="380" rx="5" fill="#5a8f35"/>
<line x1="470" y1="170" x2="481" y2="170" stroke="#3d6b22" stroke-width="1.5"/>
<line x1="470" y1="240" x2="481" y2="240" stroke="#3d6b22" stroke-width="1.5"/>
<line x1="470" y1="310" x2="481" y2="310" stroke="#3d6b22" stroke-width="1.5"/>
<ellipse cx="475" cy="90" rx="30" ry="7" fill="#68b83f" transform="rotate(32 475 90)"/>
<ellipse cx="475" cy="145" rx="34" ry="7" fill="#5aa835" transform="rotate(-28 475 145)"/>
<ellipse cx="475" cy="200" rx="32" ry="7" fill="#68b83f" transform="rotate(24 475 200)"/>
<ellipse cx="475" cy="255" rx="30" ry="6" fill="#5aa835" transform="rotate(-20 475 255)"/>
<ellipse cx="475" cy="310" rx="28" ry="6" fill="#68b83f" transform="rotate(18 475 310)"/>
<rect x="520" y="130" width="9" height="350" rx="4" fill="#4a7c2f"/>
<ellipse cx="524" cy="120" rx="26" ry="6" fill="#5aa835" transform="rotate(-25 524 120)"/>
<ellipse cx="524" cy="175" rx="30" ry="6" fill="#4a9a2e" transform="rotate(22 524 175)"/>
<ellipse cx="524" cy="230" rx="28" ry="6" fill="#5aa835" transform="rotate(-18 524 230)"/>
<ellipse cx="524" cy="285" rx="26" ry="5" fill="#4a9a2e" transform="rotate(15 524 285)"/>
</g>
<!-- ===== SOSOK PAKAR ===== -->
<g transform="translate(270, 180)">
<!-- Bayangan -->
<ellipse cx="65" cy="255" rx="45" ry="12" fill="#0d2010" opacity="0.5"/>
<!-- Sepatu -->
<ellipse cx="48" cy="252" rx="18" ry="7" fill="#3d2b1f"/>
<ellipse cx="82" cy="252" rx="16" ry="7" fill="#3d2b1f"/>
<!-- Celana -->
<rect x="38" y="185" width="24" height="70" rx="5" fill="#4a5568"/>
<rect x="68" y="185" width="22" height="70" rx="5" fill="#4a5568"/>
<rect x="36" y="180" width="56" height="15" rx="4" fill="#3d4a5c"/>
<!-- Baju lapangan (coklat khaki) -->
<rect x="30" y="100" width="72" height="85" rx="8" fill="#8b7355"/>
<!-- Saku baju -->
<rect x="35" y="115" width="18" height="14" rx="3" fill="#7a6448" opacity="0.8"/>
<rect x="79" y="115" width="18" height="14" rx="3" fill="#7a6448" opacity="0.8"/>
<!-- Kerah -->
<polygon points="66,100 66,118 58,108" fill="#7a6448"/>
<polygon points="66,100 66,118 74,108" fill="#7a6448"/>
<!-- Lengan kiri (memegang kaca pembesar) -->
<rect x="8" y="105" width="24" height="60" rx="8" fill="#8b7355" transform="rotate(-25 8 105)"/>
<!-- Tangan kiri -->
<circle cx="2" cy="148" r="10" fill="#c8956c"/>
<!-- Kaca pembesar -->
<circle cx="-15" cy="130" r="20" fill="none" stroke="#aaa" stroke-width="4" opacity="0.9"/>
<circle cx="-15" cy="130" r="18" fill="rgba(150,220,255,0.15)"/>
<line x1="-1" y1="144" x2="8" y2="155" stroke="#888" stroke-width="4" stroke-linecap="round"/>
<!-- Lengan kanan (memegang buku catatan) -->
<rect x="96" y="108" width="22" height="55" rx="8" fill="#8b7355" transform="rotate(20 96 108)"/>
<!-- Tangan kanan -->
<circle cx="122" cy="150" r="10" fill="#c8956c"/>
<!-- Buku catatan -->
<rect x="118" y="135" width="28" height="36" rx="4" fill="#f5f5f0" transform="rotate(10 118 135)"/>
<line x1="122" y1="143" x2="143" y2="140" stroke="#ccc" stroke-width="1.5"/>
<line x1="122" y1="150" x2="143" y2="147" stroke="#ccc" stroke-width="1.5"/>
<line x1="122" y1="157" x2="143" y2="154" stroke="#ccc" stroke-width="1.5"/>
<!-- Leher -->
<rect x="58" y="82" width="16" height="22" rx="5" fill="#c8956c"/>
<!-- Kepala -->
<circle cx="66" cy="68" r="32" fill="#c8956c"/>
<!-- Rambut -->
<ellipse cx="66" cy="40" rx="32" ry="16" fill="#2c1810"/>
<ellipse cx="40" cy="58" rx="10" ry="18" fill="#2c1810"/>
<ellipse cx="92" cy="58" rx="10" ry="18" fill="#2c1810"/>
<!-- Wajah -->
<!-- Kacamata -->
<circle cx="56" cy="68" r="10" fill="none" stroke="#555" stroke-width="2.5"/>
<circle cx="76" cy="68" r="10" fill="none" stroke="#555" stroke-width="2.5"/>
<circle cx="56" cy="68" r="9" fill="rgba(200,230,255,0.2)"/>
<circle cx="76" cy="68" r="9" fill="rgba(200,230,255,0.2)"/>
<line x1="66" y1="68" x2="66" y2="68" stroke="#555" stroke-width="2"/>
<line x1="46" y1="66" x2="42" y2="63" stroke="#555" stroke-width="2" stroke-linecap="round"/>
<line x1="86" y1="66" x2="90" y2="63" stroke="#555" stroke-width="2" stroke-linecap="round"/>
<!-- Alis -->
<path d="M48 58 Q56 54 64 57" stroke="#2c1810" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<path d="M68 57 Q76 54 84 58" stroke="#2c1810" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<!-- Senyum -->
<path d="M54 80 Q66 90 78 80" stroke="#8b5e3c" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<!-- Hidung -->
<ellipse cx="66" cy="74" rx="3" ry="4" fill="#b8845c" opacity="0.6"/>
<!-- Name tag -->
<rect x="45" y="128" width="42" height="18" rx="3" fill="white" opacity="0.9"/>
<text x="66" y="140" text-anchor="middle" font-size="6" fill="#2d4a1e" font-family="Arial" font-weight="bold">PAKAR TEBU</text>
</g>
<!-- Rumput / tanaman kecil di depan -->
<g opacity="0.6">
<ellipse cx="30" cy="450" rx="15" ry="5" fill="#4a7c2f" transform="rotate(-20 30 450)"/>
<ellipse cx="50" cy="445" rx="18" ry="5" fill="#52a833" transform="rotate(15 50 445)"/>
<ellipse cx="170" cy="448" rx="14" ry="4" fill="#4a7c2f" transform="rotate(22 170 448)"/>
<ellipse cx="420" cy="452" rx="16" ry="5" fill="#52a833" transform="rotate(-18 420 452)"/>
<ellipse cx="560" cy="447" rx="15" ry="5" fill="#4a7c2f" transform="rotate(20 560 447)"/>
<ellipse cx="580" cy="455" rx="12" ry="4" fill="#52a833" transform="rotate(-12 580 455)"/>
</g>
<!-- Stetoskop di leher pakar -->
<g transform="translate(310, 270)" opacity="0.9">
<path d="M20 0 C20 0 35 0 35 15 C35 30 25 33 25 45" stroke="#c0c0c0" stroke-width="3" stroke-linecap="round" fill="none"/>
<circle cx="25" cy="48" r="6" stroke="#c0c0c0" stroke-width="2.5" fill="rgba(200,200,200,0.3)"/>
<line x1="15" y1="0" x2="26" y2="0" stroke="#c0c0c0" stroke-width="3" stroke-linecap="round"/>
</g>
<!-- Bendera Indonesia kecil di background -->
<g transform="translate(490, 140)" opacity="0.3">
<line x1="0" y1="0" x2="0" y2="60" stroke="#8b7355" stroke-width="2"/>
<rect x="0" y="0" width="30" height="10" fill="#dc143c"/>
<rect x="0" y="10" width="30" height="10" fill="white"/>
</g>
</svg>
</div>
<div class="absolute inset-0"
style="background: linear-gradient(to top, rgba(26,58,42,0.92) 0%, rgba(26,58,42,0.4) 50%, rgba(26,58,42,0.15) 100%);">
</div>
<div class="relative z-10 flex flex-col justify-between p-10 w-full">
<a href="/" class="flex items-center gap-3 text-white no-underline">
<div class="w-10 h-10 rounded-xl flex items-center justify-center"
style="background:rgba(255,255,255,.15);backdrop-filter:blur(4px);">
<svg width="26" height="26" viewBox="0 0 32 32" fill="none">
<ellipse cx="11" cy="15" rx="5" ry="11" fill="rgba(255,255,255,0.25)" stroke="white" stroke-width="1.3" transform="rotate(-15 11 15)"/>
<line x1="11" y1="6" x2="10" y2="24" stroke="white" stroke-width="1" stroke-linecap="round" opacity="0.8" transform="rotate(-15 11 15)"/>
<line x1="10" y1="13" x2="7" y2="17" stroke="white" stroke-width="0.8" stroke-linecap="round" opacity="0.6"/>
<line x1="10" y1="17" x2="7.5" y2="21" stroke="white" stroke-width="0.8" stroke-linecap="round" opacity="0.6"/>
<path d="M19 7 C19 7 24 7 24 12 C24 17 20 18 20 22" stroke="white" stroke-width="1.5" stroke-linecap="round" fill="none"/>
<circle cx="20" cy="24.5" r="3" stroke="white" stroke-width="1.3" fill="rgba(255,255,255,0.2)"/>
<line x1="18" y1="7" x2="21" y2="7" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<span class="text-xl font-bold" style="font-family:'Playfair Display',serif;">SiPakarTebu</span>
</a>
<div class="text-white">
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full mb-4"
style="background:rgba(116,198,157,.25);border:1px solid rgba(116,198,157,.4);">
<span style="color:#74c69d;font-size:.75rem;font-weight:600;letter-spacing:.08em;">🌿 SISTEM PAKAR TEBU</span>
</div>
<h2 style="font-family:'Playfair Display',serif;font-size:2rem;line-height:1.25;margin-bottom:1rem;">
Bergabung &<br>
<em style="color:#74c69d;">Mulai Diagnosa Sekarang</em>
</h2>
<p style="color:rgba(255,255,255,.7);font-size:.95rem;line-height:1.7;max-width:360px;margin-bottom:2rem;">
Daftar gratis dan dapatkan akses penuh ke sistem diagnosis penyakit tebu berbasis kecerdasan buatan.
</p>
<div style="display:flex;flex-direction:column;gap:.75rem;">
<div style="display:flex;align-items:center;gap:.75rem;">
<div style="width:32px;height:32px;border-radius:50%;background:rgba(116,198,157,.2);border:1px solid rgba(116,198,157,.4);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="fas fa-user-check" style="color:#74c69d;font-size:.75rem;"></i>
</div>
<span style="color:rgba(255,255,255,.85);font-size:.875rem;">Daftar gratis, tanpa biaya apapun</span>
</div>
<div style="display:flex;align-items:center;gap:.75rem;">
<div style="width:32px;height:32px;border-radius:50%;background:rgba(116,198,157,.2);border:1px solid rgba(116,198,157,.4);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="fas fa-history" style="color:#74c69d;font-size:.75rem;"></i>
</div>
<span style="color:rgba(255,255,255,.85);font-size:.875rem;">Riwayat diagnosis tersimpan otomatis</span>
</div>
<div style="display:flex;align-items:center;gap:.75rem;">
<div style="width:32px;height:32px;border-radius:50%;background:rgba(116,198,157,.2);border:1px solid rgba(116,198,157,.4);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="fas fa-book-medical" style="color:#74c69d;font-size:.75rem;"></i>
</div>
<span style="color:rgba(255,255,255,.85);font-size:.875rem;">Akses kamus 10 penyakit tebu lengkap</span>
</div>
</div>
</div>
</div>
</div>
<!-- PANEL KANAN Form Register -->
<div class="w-full lg:w-1/2 flex items-center justify-center p-6 lg:p-12 overflow-y-auto">
<div class="w-full max-w-md">
<!-- Logo mobile -->
<div class="lg:hidden text-center mb-8">
<div class="w-14 h-14 rounded-2xl flex items-center justify-center mx-auto mb-3"
style="background:linear-gradient(135deg,#2d6a4f,#40916c);">
<svg width="30" height="30" viewBox="0 0 32 32" fill="none">
<ellipse cx="11" cy="15" rx="5" ry="11" fill="rgba(255,255,255,0.25)" stroke="white" stroke-width="1.3" transform="rotate(-15 11 15)"/>
<line x1="11" y1="6" x2="10" y2="24" stroke="white" stroke-width="1" stroke-linecap="round" opacity="0.8" transform="rotate(-15 11 15)"/>
<line x1="10" y1="13" x2="7" y2="17" stroke="white" stroke-width="0.8" stroke-linecap="round" opacity="0.6"/>
<line x1="10" y1="17" x2="7.5" y2="21" stroke="white" stroke-width="0.8" stroke-linecap="round" opacity="0.6"/>
<path d="M19 7 C19 7 24 7 24 12 C24 17 20 18 20 22" stroke="white" stroke-width="1.5" stroke-linecap="round" fill="none"/>
<circle cx="20" cy="24.5" r="3" stroke="white" stroke-width="1.3" fill="rgba(255,255,255,0.2)"/>
<line x1="18" y1="7" x2="21" y2="7" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<h1 class="text-2xl font-bold" style="font-family:'Playfair Display',serif;color:#1a3a2a;">SiPakarTebu</h1>
</div>
<div class="mb-7">
<h2 class="text-3xl font-bold mb-2" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Buat Akun Baru</h2>
<p class="text-sm" style="color:#5a7a67;">Isi data di bawah untuk mulai menggunakan SiPakarTebu</p>
</div>
@if ($errors->any())
<div class="px-4 py-3 rounded-xl mb-5 text-sm" style="background:#fff0f0;border:1px solid #ffd6d6;">
<ul style="color:#e63946;list-style:disc;padding-left:1rem;">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('register') }}">
@csrf
<div class="mb-4">
<label class="block mb-2" style="color:#1a2e22;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;">Nama Lengkap</label>
<div class="relative">
<i class="fas fa-user absolute left-4 top-1/2 -translate-y-1/2" style="color:#a0b4a8;font-size:.85rem;"></i>
<input type="text" name="name" value="{{ old('name') }}"
class="w-full pl-11 pr-4 py-3.5 border rounded-xl transition text-sm"
style="border-color:#e5e7eb;background:#f9fafb;color:#1a2e22;"
placeholder="masukkan nama lengkap" required>
</div>
</div>
<div class="mb-4">
<label class="block mb-2" style="color:#1a2e22;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;">Email</label>
<div class="relative">
<i class="fas fa-envelope absolute left-4 top-1/2 -translate-y-1/2" style="color:#a0b4a8;font-size:.85rem;"></i>
<input type="email" name="email" value="{{ old('email') }}"
class="w-full pl-11 pr-4 py-3.5 border rounded-xl transition text-sm"
style="border-color:#e5e7eb;background:#f9fafb;color:#1a2e22;"
placeholder="nama@email.com" required>
</div>
</div>
<div class="mb-4">
<label class="block mb-2" style="color:#1a2e22;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;">Password</label>
<div class="relative">
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2" style="color:#a0b4a8;font-size:.85rem;"></i>
<input type="password" name="password" id="passInput"
class="w-full pl-11 pr-12 py-3.5 border rounded-xl transition text-sm"
style="border-color:#e5e7eb;background:#f9fafb;color:#1a2e22;"
placeholder="minimal 8 karakter" required>
<button type="button" onclick="togglePass('passInput','eyePass')"
class="absolute right-4 top-1/2 -translate-y-1/2"
style="color:#a0b4a8;background:none;border:none;cursor:pointer;">
<i class="fas fa-eye" id="eyePass" style="font-size:.85rem;"></i>
</button>
</div>
</div>
<div class="mb-5">
<label class="block mb-2" style="color:#1a2e22;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;">Konfirmasi Password</label>
<div class="relative">
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2" style="color:#a0b4a8;font-size:.85rem;"></i>
<input type="password" name="password_confirmation" id="passConfInput"
class="w-full pl-11 pr-12 py-3.5 border rounded-xl transition text-sm"
style="border-color:#e5e7eb;background:#f9fafb;color:#1a2e22;"
placeholder="ulangi password" required>
<button type="button" onclick="togglePass('passConfInput','eyeConf')"
class="absolute right-4 top-1/2 -translate-y-1/2"
style="color:#a0b4a8;background:none;border:none;cursor:pointer;">
<i class="fas fa-eye" id="eyeConf" style="font-size:.85rem;"></i>
</button>
</div>
</div>
<div class="mb-6">
<label class="block mb-2" style="color:#1a2e22;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;">Daftar Sebagai</label>
<div class="grid grid-cols-2 gap-3">
<label class="role-card {{ old('role') === 'user' ? 'selected' : '' }}" id="card-user" onclick="selectRole('user')">
<input type="radio" name="role" value="user" {{ old('role') === 'user' ? 'checked' : '' }}>
<div class="radio-dot" id="dot-user"></div>
<div>
<div class="flex items-center gap-1 mb-0.5">
<i class="fas fa-seedling text-xs" style="color:#2d6a4f;"></i>
<span class="text-sm font-semibold" style="color:#1a3a2a;">Petani / Umum</span>
</div>
<p class="text-xs" style="color:#8fa89a;">Diagnosa & lihat kamus</p>
</div>
</label>
<label class="role-card {{ old('role') === 'admin' ? 'selected' : '' }}" id="card-admin" onclick="selectRole('admin')">
<input type="radio" name="role" value="admin" {{ old('role') === 'admin' ? 'checked' : '' }}>
<div class="radio-dot" id="dot-admin"></div>
<div>
<div class="flex items-center gap-1 mb-0.5">
<i class="fas fa-user-tie text-xs" style="color:#2d6a4f;"></i>
<span class="text-sm font-semibold" style="color:#1a3a2a;">Ahli Tanaman</span>
</div>
<p class="text-xs" style="color:#8fa89a;">Kelola kamus penyakit</p>
</div>
</label>
</div>
@error('role')
<p class="text-xs mt-2" style="color:#e63946;">{{ $message }}</p>
@enderror
</div>
<button type="submit"
class="w-full py-3.5 rounded-xl font-semibold text-white text-sm transition"
style="background:linear-gradient(135deg,#1a3a2a,#2d6a4f);box-shadow:0 6px 20px rgba(26,58,42,.3);"
onmouseover="this.style.opacity='.9'" onmouseout="this.style.opacity='1'">
<i class="fas fa-user-plus mr-2"></i>Daftar Sekarang
</button>
</form>
<div class="mt-6 pt-6 text-center" style="border-top:1px solid #ede8df;">
<p class="text-sm" style="color:#8fa89a;">
Sudah punya akun?
<a href="{{ route('login') }}" style="color:#2d6a4f;font-weight:600;text-decoration:none;">Masuk di sini</a>
</p>
</div>
</div>
</div>
</div>
<script>
function selectRole(role) {
['user', 'admin'].forEach(r => {
document.getElementById('card-' + r).classList.remove('selected');
});
document.getElementById('card-' + role).classList.add('selected');
document.querySelector('input[value="' + role + '"]').checked = true;
}
document.addEventListener('DOMContentLoaded', function () {
const checked = document.querySelector('input[name="role"]:checked');
if (!checked) selectRole('user');
});
function togglePass(inputId, iconId) {
const input = document.getElementById(inputId);
const icon = document.getElementById(iconId);
if (input.type === 'password') {
input.type = 'text';
icon.classList.replace('fa-eye', 'fa-eye-slash');
} else {
input.type = 'password';
icon.classList.replace('fa-eye-slash', 'fa-eye');
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,291 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reset Password - SiPakarTebu</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f0f4f0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.card {
background: #ffffff;
border-radius: 12px;
overflow: hidden;
width: 100%;
max-width: 390px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
.card-header {
background-color: #2d6a4f;
padding: 32px 24px;
text-align: center;
color: white;
}
.card-header h1 {
font-size: 26px;
font-weight: 700;
letter-spacing: 0.5px;
}
.card-header p {
font-size: 13px;
margin-top: 4px;
opacity: 0.85;
}
.card-body {
padding: 28px 28px 32px;
}
.card-body h2 {
font-size: 20px;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 6px;
}
.card-body .subtitle {
font-size: 13px;
color: #666;
margin-bottom: 22px;
}
.alert-error {
background-color: #fde8e8;
border: 1px solid #f5c6c6;
border-radius: 8px;
padding: 10px 14px;
margin-bottom: 16px;
color: #c0392b;
font-size: 13px;
}
.alert-success {
background-color: #e8f5e9;
border: 1px solid #c8e6c9;
border-radius: 8px;
padding: 10px 14px;
margin-bottom: 16px;
color: #2d6a4f;
font-size: 13px;
}
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.8px;
color: #444;
margin-bottom: 8px;
text-transform: uppercase;
}
.form-group input {
width: 100%;
padding: 14px 16px;
border: 1.5px solid #ddd;
border-radius: 8px;
font-size: 15px;
color: #333;
outline: none;
transition: border-color 0.2s;
background-color: #fff;
}
.form-group input:focus {
border-color: #2d6a4f;
box-shadow: 0 0 0 3px rgba(45, 106, 79, 0.1);
}
.form-group input.is-invalid {
border-color: #e74c3c;
}
.invalid-feedback {
color: #e74c3c;
font-size: 12px;
margin-top: 5px;
}
.password-wrapper {
position: relative;
}
.password-wrapper input {
padding-right: 44px;
}
.toggle-password {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #888;
background: none;
border: none;
font-size: 18px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.btn-primary {
width: 100%;
padding: 15px;
background-color: #2d6a4f;
color: white;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
margin-top: 6px;
}
.btn-primary:hover {
background-color: #245a42;
}
.btn-primary:active {
background-color: #1e4d38;
}
.back-link {
display: block;
text-align: center;
margin-top: 18px;
color: #555;
font-size: 13px;
text-decoration: none;
transition: color 0.2s;
}
.back-link:hover {
color: #2d6a4f;
}
</style>
</head>
<body>
<div class="card">
{{-- Header --}}
<div class="card-header">
<h1>SiPakarTebu</h1>
<p>Sistem Diagnosis Penyakit Tanaman Tebu</p>
</div>
{{-- Body --}}
<div class="card-body">
<h2>Reset Password</h2>
<p class="subtitle">Masukkan password baru kamu</p>
{{-- Error messages --}}
@if ($errors->any())
<div class="alert-error">
@foreach ($errors->all() as $error)
<div>{{ $error }}</div>
@endforeach
</div>
@endif
{{-- Success message --}}
@if (session('status'))
<div class="alert-success">
{{ session('status') }}
</div>
@endif
<form method="POST" action="{{ route('password.reset') }}">
@csrf
{{-- Hidden fields --}}
<input type="hidden" name="token" value="{{ $token ?? request()->route('token') }}">
<input type="hidden" name="email" value="{{ $email ?? request('email') }}">
{{-- Password Baru --}}
<div class="form-group">
<label for="password">PASSWORD BARU</label>
<div class="password-wrapper">
<input
type="password"
id="password"
name="password"
placeholder="Masukkan password baru"
required
autocomplete="new-password"
class="{{ $errors->has('password') ? 'is-invalid' : '' }}"
>
<button type="button" class="toggle-password" onclick="togglePassword('password', this)" title="Tampilkan password">
<svg id="icon-password" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
@error('password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
{{-- Konfirmasi Password --}}
<div class="form-group">
<label for="password_confirmation">KONFIRMASI PASSWORD</label>
<div class="password-wrapper">
<input
type="password"
id="password_confirmation"
name="password_confirmation"
placeholder="Ulangi password baru"
required
autocomplete="new-password"
>
<button type="button" class="toggle-password" onclick="togglePassword('password_confirmation', this)" title="Tampilkan password">
<svg id="icon-password_confirmation" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
</div>
<button type="submit" class="btn-primary">Simpan Password</button>
</form>
<a href="{{ route('login') }}" class="back-link"> Kembali ke Login</a>
</div>
</div>
<script>
const eyeIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`;
const eyeSlashIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>`;
function togglePassword(fieldId, btn) {
const input = document.getElementById(fieldId);
if (input.type === 'password') {
input.type = 'text';
btn.innerHTML = eyeSlashIcon;
} else {
input.type = 'password';
btn.innerHTML = eyeIcon;
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,579 @@
@extends('layouts.app')
@section('title', 'Dashboard')
@section('page-title', 'Dashboard')
@section('content')
{{-- Sapaan --}}
<div class="mb-6">
<h2 class="text-2xl font-bold" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
Halo, {{ auth()->user()->name }}! 👋
</h2>
<p class="text-sm mt-1" style="color:#5a7a67;">
Selamat datang kembali. Berikut ringkasan aktivitas diagnosa kamu hari ini.
</p>
</div>
<div id="tour-stat-cards" class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
<!-- Card 1: Total Diagnosa -->
<div class="bg-white p-4 rounded-2xl border border-[#ede8df] cursor-pointer transition hover:shadow-lg"
style="box-shadow:0 4px 16px rgba(26,58,42,.08);"
onclick="openTotalModal()"
title="Klik untuk lihat statistik diagnosa">
<div class="flex items-center justify-between">
<div>
<p class="flex items-center gap-1" style="color:#5a7a67;text-transform:uppercase;letter-spacing:.06em;font-size:.7rem;">
Total Diagnosa
<i class="fas fa-search text-xs" style="color:#2d6a4f;"></i>
</p>
<h3 class="text-2xl font-bold mt-1" style="font-family:'Playfair Display',serif;color:#1a3a2a;">{{ $totalDiagnosis }}</h3>
</div>
<div class="p-3 rounded-xl" style="background:#d8f3dc;">
<i class="fas fa-clipboard-list text-xl" style="color:#2d6a4f;"></i>
</div>
</div>
</div>
<!-- Card 2: Bulan Ini -->
<div class="bg-white p-4 rounded-2xl border border-[#ede8df] cursor-pointer transition hover:shadow-lg"
style="box-shadow:0 4px 16px rgba(26,58,42,.08);"
onclick="openCalendar()"
title="Klik untuk lihat kalender diagnosa">
<div class="flex items-center justify-between">
<div>
<p class="flex items-center gap-1" style="color:#5a7a67;text-transform:uppercase;letter-spacing:.06em;font-size:.7rem;">
Bulan Ini
<i class="fas fa-search text-xs" style="color:#2563eb;"></i>
</p>
<h3 class="text-2xl font-bold mt-1" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
{{ $monthlyData->where('month', (int) date('n'))->first()->count ?? 0 }}
</h3>
</div>
<div class="p-3 rounded-xl" style="background:#e0f0ff;">
<i class="fas fa-calendar text-xl" style="color:#2563eb;"></i>
</div>
</div>
</div>
<!-- Card 3: Akurasi Rata-rata -->
<div class="bg-white p-4 rounded-2xl border border-[#ede8df] cursor-pointer transition hover:shadow-lg"
style="box-shadow:0 4px 16px rgba(26,58,42,.08);"
onclick="openAccuracyModal()"
title="Klik untuk lihat grafik akurasi">
<div class="flex items-center justify-between">
<div>
<p class="flex items-center gap-1" style="color:#5a7a67;text-transform:uppercase;letter-spacing:.06em;font-size:.7rem;">
Akurasi Rata-rata
<i class="fas fa-search text-xs" style="color:#7c3aed;"></i>
</p>
<h3 class="text-2xl font-bold mt-1" style="font-family:'Playfair Display',serif;color:#1a3a2a;">{{ $avgAccuracy }}%</h3>
</div>
<div class="p-3 rounded-xl" style="background:#f0e8ff;">
<i class="fas fa-chart-line text-xl" style="color:#7c3aed;"></i>
</div>
</div>
</div>
</div>
<!-- Chart -->
<div id="tour-chart" class="bg-white p-4 rounded-2xl mb-6 border border-[#ede8df]" style="box-shadow:0 4px 16px rgba(26,58,42,.08);">
<h3 class="text-lg font-bold mb-4" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Statistik Diagnosa</h3>
<div class="relative" style="height:200px;">
<canvas id="diagnosisChart"></canvas>
</div>
</div>
<!-- Recent Diagnosis -->
<div class="bg-white p-4 rounded-2xl border border-[#ede8df]" style="box-shadow:0 4px 16px rgba(26,58,42,.08);">
<h3 class="text-lg font-bold mb-4" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Diagnosa Terbaru</h3>
{{-- Tampilan mobile: card --}}
<div class="block sm:hidden space-y-3">
@forelse($recentDiagnosis as $diagnosis)
<div class="p-3 rounded-xl border border-[#ede8df]">
<div class="flex justify-between items-start mb-1">
<span class="text-xs" style="color:#5a7a67;">{{ $diagnosis->created_at->format('d/m/Y') }}</span>
<span class="px-2 py-0.5 rounded-full text-xs font-semibold" style="background:#d8f3dc;color:#2d6a4f;">
{{ $diagnosis->confidence }}%
</span>
</div>
<p class="font-medium text-sm" style="color:#1a3a2a;">{{ $diagnosis->plant_name }}</p>
<p class="text-sm" style="color:#5a7a67;">{{ $diagnosis->disease_name }}</p>
</div>
@empty
<p class="text-center py-4 text-sm" style="color:#8fa89a;">Belum ada diagnosa</p>
@endforelse
</div>
{{-- Tampilan desktop: tabel --}}
<div class="hidden sm:block overflow-x-auto">
<table class="w-full">
<thead>
<tr style="border-bottom:2px solid #ede8df;">
<th class="text-left py-3 px-4" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Tanggal</th>
<th class="text-left py-3 px-4" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Nama Tanaman</th>
<th class="text-left py-3 px-4" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Penyakit</th>
<th class="text-left py-3 px-4" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Akurasi</th>
</tr>
</thead>
<tbody>
@forelse($recentDiagnosis as $diagnosis)
<tr style="border-bottom:1px solid rgba(237,232,223,.6);" class="hover:bg-[#d8f3dc]/20 transition">
<td class="py-3 px-4 text-sm" style="color:#5a7a67;">{{ $diagnosis->created_at->format('d/m/Y') }}</td>
<td class="py-3 px-4 text-sm font-medium" style="color:#1a3a2a;">{{ $diagnosis->plant_name }}</td>
<td class="py-3 px-4 text-sm" style="color:#1a2e22;">{{ $diagnosis->disease_name }}</td>
<td class="py-3 px-4">
<span class="px-3 py-1 rounded-full text-sm font-semibold" style="background:#d8f3dc;color:#2d6a4f;">
{{ $diagnosis->confidence }}%
</span>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="text-center py-8" style="color:#8fa89a;">Belum ada diagnosa</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<!-- ─── Modal Total Diagnosa ─── -->
<div id="totalModal" class="fixed inset-0 z-50 hidden flex items-center justify-center"
style="background:rgba(0,0,0,0.4);">
<div class="bg-white rounded-2xl p-6 w-full max-w-lg mx-4 shadow-2xl"
style="animation: popIn .25s cubic-bezier(0.22,1,0.36,1);">
<div class="flex items-center justify-between mb-5">
<div>
<h3 class="text-lg font-bold" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Statistik Total Diagnosa</h3>
<p class="text-sm" style="color:#5a7a67;">Per bulan tahun {{ date('Y') }}</p>
</div>
<button onclick="closeTotalModal()"
class="w-8 h-8 rounded-full flex items-center justify-center transition"
style="background:#f0fdf4;color:#2d6a4f;"
onmouseover="this.style.background='#d8f3dc'" onmouseout="this.style.background='#f0fdf4'">
<i class="fas fa-times text-sm"></i>
</button>
</div>
<canvas id="totalModalChart" height="160"></canvas>
<div class="mt-4 p-3 rounded-xl text-sm text-center font-semibold" style="background:#d8f3dc;color:#2d6a4f;">
Total keseluruhan: {{ $totalDiagnosis }} diagnosa
</div>
<button onclick="closeTotalModal()"
class="w-full mt-4 py-2.5 rounded-xl text-sm font-semibold"
style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;">
Tutup
</button>
</div>
</div>
<!-- ─── Modal Kalender ─── -->
<div id="calendarModal" class="fixed inset-0 z-50 hidden flex items-center justify-center"
style="background:rgba(0,0,0,0.4);">
<div class="bg-white rounded-2xl p-6 w-full max-w-sm mx-4 shadow-2xl"
style="animation: popIn .25s cubic-bezier(0.22,1,0.36,1);">
<div class="flex items-center justify-between mb-5">
<div>
<h3 class="text-lg font-bold" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Kalender Diagnosa</h3>
<p class="text-sm" style="color:#5a7a67;" id="calendarMonthYear"></p>
</div>
<button onclick="closeCalendar()"
class="w-8 h-8 rounded-full flex items-center justify-center transition"
style="background:#f0fdf4;color:#2d6a4f;"
onmouseover="this.style.background='#d8f3dc'" onmouseout="this.style.background='#f0fdf4'">
<i class="fas fa-times text-sm"></i>
</button>
</div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center gap-1.5">
<div class="w-4 h-4 rounded-full" style="background:#2d6a4f;"></div>
<span class="text-xs" style="color:#5a7a67;">Ada diagnosa</span>
</div>
<div class="flex items-center gap-1.5">
<div class="w-4 h-4 rounded-full" style="background:#f0f0f0;"></div>
<span class="text-xs" style="color:#5a7a67;">Tidak ada diagnosa</span>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
@foreach(['Min','Sen','Sel','Rab','Kam','Jum','Sab'] as $day)
<div class="text-center text-xs font-semibold py-1" style="color:#8fa89a;">{{ $day }}</div>
@endforeach
</div>
<div class="grid grid-cols-7 gap-1" id="calendarGrid"></div>
<div id="selectedDayInfo" class="mt-4 hidden p-3 rounded-xl text-sm" style="background:#f0fdf4;color:#2d6a4f;border:1px solid #b7ddc4;"></div>
<button onclick="closeCalendar()"
class="w-full mt-4 py-2.5 rounded-xl text-sm font-semibold"
style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;">
Tutup
</button>
</div>
</div>
<!-- ─── Modal Akurasi ─── -->
<div id="accuracyModal" class="fixed inset-0 z-50 hidden flex items-center justify-center"
style="background:rgba(0,0,0,0.4);">
<div class="bg-white rounded-2xl p-6 w-full max-w-lg mx-4 shadow-2xl"
style="animation: popIn .25s cubic-bezier(0.22,1,0.36,1);">
<div class="flex items-center justify-between mb-5">
<div>
<h3 class="text-lg font-bold" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Grafik Akurasi</h3>
<p class="text-sm" style="color:#5a7a67;">Rata-rata akurasi per bulan tahun {{ date('Y') }}</p>
</div>
<button onclick="closeAccuracyModal()"
class="w-8 h-8 rounded-full flex items-center justify-center transition"
style="background:#f0e8ff;color:#7c3aed;"
onmouseover="this.style.background='#e9d5ff'" onmouseout="this.style.background='#f0e8ff'">
<i class="fas fa-times text-sm"></i>
</button>
</div>
<canvas id="accuracyModalChart" height="160"></canvas>
<div class="mt-4 p-3 rounded-xl text-sm text-center font-semibold" style="background:#f0e8ff;color:#7c3aed;">
Akurasi rata-rata keseluruhan: {{ $avgAccuracy }}%
</div>
<button onclick="closeAccuracyModal()"
class="w-full mt-4 py-2.5 rounded-xl text-sm font-semibold"
style="background:#f0e8ff;color:#7c3aed;border:1.5px solid #c4b5fd;">
Tutup
</button>
</div>
</div>
@endsection
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const monthlyRaw = [
@foreach(range(1, 12) as $month)
{{ $monthlyData->where('month', $month)->first()?->count ?? 0 }},
@endforeach
];
const monthlyAccuracy = @json($monthlyAccuracy);
const monthLabels = ['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des'];
const ctx = document.getElementById('diagnosisChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: monthLabels,
datasets: [{
label: 'Jumlah Diagnosa',
data: monthlyRaw,
backgroundColor: 'rgba(64,145,108,0.25)',
borderColor: 'rgba(45,106,79,1)',
borderWidth: 2,
borderRadius: 6,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { beginAtZero: true, ticks: { stepSize: 1, color: '#5a7a67' }, grid: { color: 'rgba(237,232,223,.6)' } },
x: { ticks: { color: '#5a7a67', font: { size: 10 } }, grid: { display: false } }
},
plugins: { legend: { labels: { color: '#1a3a2a', font: { size: 11 } } } }
}
});
let totalChartInstance = null;
function openTotalModal() {
document.getElementById('totalModal').classList.remove('hidden');
if (totalChartInstance) totalChartInstance.destroy();
const ctx2 = document.getElementById('totalModalChart').getContext('2d');
totalChartInstance = new Chart(ctx2, {
type: 'bar',
data: {
labels: monthLabels,
datasets: [{
label: 'Jumlah Diagnosa',
data: monthlyRaw,
backgroundColor: 'rgba(64,145,108,0.25)',
borderColor: 'rgba(45,106,79,1)',
borderWidth: 2,
borderRadius: 6,
}]
},
options: {
responsive: true,
scales: {
y: { beginAtZero: true, ticks: { stepSize: 1, color: '#5a7a67' }, grid: { color: 'rgba(237,232,223,.6)' } },
x: { ticks: { color: '#5a7a67' }, grid: { display: false } }
},
plugins: { legend: { labels: { color: '#1a3a2a' } } }
}
});
}
function closeTotalModal() { document.getElementById('totalModal').classList.add('hidden'); }
document.getElementById('totalModal').addEventListener('click', function(e) { if (e.target === this) closeTotalModal(); });
const diagnosaDates = @json($diagnosaDates);
const today = {{ date('j') }};
const currentMonth = {{ date('n') }};
const currentYear = {{ date('Y') }};
const bulanNama = ['Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember'];
function openCalendar() {
document.getElementById('calendarMonthYear').textContent = bulanNama[currentMonth - 1] + ' ' + currentYear;
buildCalendar();
document.getElementById('calendarModal').classList.remove('hidden');
document.getElementById('selectedDayInfo').classList.add('hidden');
}
function closeCalendar() { document.getElementById('calendarModal').classList.add('hidden'); }
document.getElementById('calendarModal').addEventListener('click', function(e) { if (e.target === this) closeCalendar(); });
function buildCalendar() {
const grid = document.getElementById('calendarGrid');
grid.innerHTML = '';
const firstDay = new Date(currentYear, currentMonth - 1, 1).getDay();
const daysInMonth = new Date(currentYear, currentMonth, 0).getDate();
for (let i = 0; i < firstDay; i++) grid.innerHTML += `<div></div>`;
for (let d = 1; d <= daysInMonth; d++) {
const hasDiagnosa = diagnosaDates[d] !== undefined;
const isToday = d === today;
const count = diagnosaDates[d] || 0;
let bg = 'transparent', color = '#1a3a2a', fw = '400', ring = '';
if (hasDiagnosa) { bg = '#2d6a4f'; color = '#fff'; fw = '700'; }
if (isToday && !hasDiagnosa) { ring = 'border:2px solid #2563eb;'; color = '#2563eb'; fw = '600'; }
if (isToday && hasDiagnosa) { ring = 'border:2px solid #1a3a2a;'; }
grid.innerHTML += `
<div onclick="showDayInfo(${d}, ${count})"
class="relative flex items-center justify-center rounded-full text-xs transition cursor-pointer hover:opacity-80"
style="width:32px;height:32px;background:${bg};color:${color};font-weight:${fw};${ring}margin:auto;">
${d}
${hasDiagnosa ? `<span class="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full" style="background:#74c69d;border:1px solid white;"></span>` : ''}
</div>`;
}
}
function showDayInfo(day, count) {
const info = document.getElementById('selectedDayInfo');
if (count > 0) {
info.innerHTML = `<i class="fas fa-check-circle mr-2"></i><strong>${day} ${bulanNama[currentMonth-1]} ${currentYear}</strong>: ${count} diagnosa dilakukan`;
info.style.cssText = 'background:#f0fdf4;color:#2d6a4f;border:1px solid #b7ddc4;padding:12px;border-radius:12px;';
} else {
info.innerHTML = `<i class="fas fa-info-circle mr-2"></i><strong>${day} ${bulanNama[currentMonth-1]} ${currentYear}</strong>: Tidak ada diagnosa`;
info.style.cssText = 'background:#f8f4ee;color:#8fa89a;border:1px solid #ede8df;padding:12px;border-radius:12px;';
}
info.classList.remove('hidden');
}
let accuracyChartInstance = null;
function openAccuracyModal() {
document.getElementById('accuracyModal').classList.remove('hidden');
if (accuracyChartInstance) accuracyChartInstance.destroy();
const ctx3 = document.getElementById('accuracyModalChart').getContext('2d');
accuracyChartInstance = new Chart(ctx3, {
type: 'line',
data: {
labels: monthLabels,
datasets: [{
label: 'Akurasi (%)',
data: monthlyAccuracy,
borderColor: 'rgba(124,58,237,1)',
backgroundColor: 'rgba(124,58,237,0.1)',
borderWidth: 2,
pointBackgroundColor: 'rgba(124,58,237,1)',
pointRadius: 4,
tension: 0.4,
fill: true,
}]
},
options: {
responsive: true,
scales: {
y: { beginAtZero: false, min: 0, max: 100, ticks: { color: '#5a7a67', callback: v => v + '%' }, grid: { color: 'rgba(237,232,223,.6)' } },
x: { ticks: { color: '#5a7a67' }, grid: { display: false } }
},
plugins: { legend: { labels: { color: '#1a3a2a' } } }
}
});
}
function closeAccuracyModal() { document.getElementById('accuracyModal').classList.add('hidden'); }
document.getElementById('accuracyModal').addEventListener('click', function(e) { if (e.target === this) closeAccuracyModal(); });
/* ============================================================
GUIDED TOUR SiPakarTebu
Otomatis muncul untuk pengguna baru (belum pernah lihat tour)
============================================================ */
const tourSteps = [
{
title: 'Selamat datang di SiPakarTebu! 🌱',
desc: 'Ini panduan singkat supaya kamu langsung paham cara pakai aplikasi ini. Klik <b>Lanjut</b> untuk mulai!',
target: null,
},
{
title: 'Menu Navigasi',
desc: 'Di sini ada semua menu: <b>Dashboard</b>, <b>Diagnosa</b>, <b>Riwayat</b>, <b>Kamus</b>, dan <b>Profil</b>. Klik menu untuk berpindah halaman.',
targetSel: 'nav#sidebar, aside, [class*="sidebar"], nav',
pos: 'right',
},
{
title: 'Kartu Statistik',
desc: 'Di sini kamu bisa lihat <b>total diagnosa</b>, <b>diagnosa bulan ini</b>, dan <b>akurasi rata-rata</b>. Klik tiap kartu untuk detail lebih lanjut!',
targetSel: '#tour-stat-cards',
pos: 'bottom',
},
{
title: 'Grafik Diagnosa',
desc: 'Grafik batang ini menampilkan jumlah diagnosa yang kamu lakukan setiap bulan sepanjang tahun.',
targetSel: '#tour-chart',
pos: 'top',
},
{
title: 'Mulai Diagnosa Baru',
desc: 'Klik menu <b>Diagnosa</b> di sidebar untuk mengunggah foto tanaman tebu dan mendapatkan hasil diagnosa penyakitnya!',
targetSel: 'a[href*="diagnosa"], a[href*="diagnosis"]',
pos: 'right',
},
];
let _tourStep = 0;
function _tourEl(id) { return document.getElementById(id); }
function _tourFindTarget(step) {
if (!step.targetSel) return null;
const sels = step.targetSel.split(',');
for (const s of sels) {
const el = document.querySelector(s.trim());
if (el) return el;
}
return null;
}
function _tourStart() {
_tourStep = 0;
// Buat elemen tour kalau belum ada
if (!_tourEl('_tour_hl')) { const d = document.createElement('div'); d.id = '_tour_hl'; document.body.appendChild(d); }
if (!_tourEl('_tour_tip')) { const d = document.createElement('div'); d.id = '_tour_tip'; document.body.appendChild(d); }
if (!_tourEl('_tour_dim')) { const d = document.createElement('div'); d.id = '_tour_dim'; document.body.appendChild(d); }
_tourRender();
}
function _tourRender() {
const step = tourSteps[_tourStep];
const total = tourSteps.length;
const hl = _tourEl('_tour_hl');
const tip = _tourEl('_tour_tip');
const dim = _tourEl('_tour_dim');
const target = _tourFindTarget(step);
// === Dim overlay ===
dim.style.cssText = `
position:fixed;inset:0;z-index:99990;
background:rgba(0,0,0,0.52);pointer-events:auto;`;
// === Highlight box ===
if (target) {
const r = target.getBoundingClientRect();
const pad = 8;
hl.style.cssText = `
position:fixed;z-index:99992;pointer-events:none;
top:${r.top - pad}px;left:${r.left - pad}px;
width:${r.width + pad*2}px;height:${r.height + pad*2}px;
border:2.5px solid #2d6a4f;border-radius:12px;
box-shadow:0 0 0 9999px rgba(0,0,0,0.52);
transition:all .35s ease;`;
dim.style.background = 'transparent';
} else {
hl.style.cssText = 'display:none;';
}
// === Dots ===
const dots = Array.from({length: total}, (_, i) =>
`<div style="width:${i===_tourStep?16:6}px;height:6px;border-radius:3px;
background:${i===_tourStep?'#2d6a4f':'#d1d5db'};
transition:all .3s;display:inline-block;margin-right:4px;"></div>`
).join('');
// === Tooltip posisi ===
let tipPos = 'top:50%;left:50%;transform:translate(-50%,-50%);';
if (target) {
const r = target.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
if (step.pos === 'right' && r.right + 310 < vw)
tipPos = `top:${Math.min(r.top, vh-260)}px;left:${r.right+16}px;transform:none;`;
else if (step.pos === 'bottom' && r.bottom + 220 < vh)
tipPos = `top:${r.bottom+16}px;left:${Math.max(16,Math.min(r.left, vw-310))}px;transform:none;`;
else if (step.pos === 'top' && r.top - 220 > 0)
tipPos = `top:${r.top-220}px;left:${Math.max(16,Math.min(r.left, vw-310))}px;transform:none;`;
else
tipPos = `top:50%;left:50%;transform:translate(-50%,-50%);`;
}
tip.style.cssText = `
position:fixed;${tipPos}
z-index:99999;background:#fff;border-radius:16px;
padding:22px 24px;width:290px;
box-shadow:0 12px 40px rgba(0,0,0,0.18);
font-family:inherit;pointer-events:auto;`;
tip.innerHTML = `
<div style="font-size:11px;color:#2d6a4f;font-weight:700;text-transform:uppercase;
letter-spacing:.8px;margin-bottom:6px;">
Langkah ${_tourStep+1} dari ${total}
</div>
<div style="font-size:15px;font-weight:700;color:#1a3a2a;margin-bottom:8px;
font-family:'Playfair Display',serif;">
${step.title}
</div>
<div style="font-size:13px;color:#5a7a67;line-height:1.65;margin-bottom:18px;">
${step.desc}
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<div>${dots}</div>
<div style="display:flex;gap:10px;align-items:center;">
<button onclick="_tourEnd()"
style="background:none;border:none;font-size:12px;color:#9ca3af;cursor:pointer;padding:4px;">
Lewati
</button>
<button onclick="_tourNext()"
style="background:#2d6a4f;color:#fff;border:none;
padding:9px 18px;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;">
${_tourStep === total-1 ? 'Selesai ✓' : 'Lanjut →'}
</button>
</div>
</div>`;
}
function _tourNext() {
_tourStep++;
if (_tourStep >= tourSteps.length) { _tourEnd(); return; }
_tourRender();
}
// Key khusus per user
const TOUR_KEY = 'sipakartebu_tour_done_{{ auth()->id() }}';
function _tourEnd() {
['_tour_hl','_tour_tip','_tour_dim'].forEach(id => {
const el = _tourEl(id);
if (el) el.remove();
});
localStorage.setItem(TOUR_KEY, '1');
}
// Auto-mulai untuk pengguna baru
window.addEventListener('load', function () {
if (!localStorage.getItem(TOUR_KEY)) {
setTimeout(_tourStart, 900);
}
});
// Fungsi ulangi tour
function ulangiTour() {
localStorage.removeItem(TOUR_KEY);
_tourStart();
}
</script>
<style>
@keyframes popIn {
from { transform: scale(0.85); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
</style>
@endpush

View File

@ -0,0 +1,485 @@
@extends('layouts.app')
@section('title', 'Diagnosa Penyakit')
@section('page-title', 'Diagnosa Penyakit')
@section('content')
<div class="max-w-2xl mx-auto">
<div id="tour-form-card" class="bg-white p-6 rounded-2xl border border-[#ede8df]" style="box-shadow:0 4px 16px rgba(26,58,42,.08);">
{{-- Progress bar --}}
<div id="tour-progress" class="mb-6">
<div class="flex justify-between text-xs font-semibold mb-2" style="color:#5a7a67;">
<span>Step <span id="step-number">1</span> dari 3</span>
<span id="step-label">Pilih Gejala</span>
</div>
<div class="w-full h-2 rounded-full" style="background:#ede8df;">
<div id="progress-bar" class="h-2 rounded-full transition-all duration-300" style="background:linear-gradient(90deg,#40916c,#74c69d);width:33%;"></div>
</div>
</div>
<form method="POST" action="{{ route('diagnosis.store') }}">
@csrf
{{-- STEP 1: Pilih Gejala --}}
<div id="step1">
<h3 class="text-lg font-bold mb-1" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Pilih Gejala</h3>
<p class="text-xs mb-4" style="color:#8fa89a;">Pilih minimal 4 gejala yang sesuai dengan kondisi tanaman</p>
<input type="text" id="search" placeholder="Cari gejala..."
class="w-full mb-4 px-4 py-2.5 border rounded-xl text-sm"
style="border-color:#ede8df;outline:none;color:#1a3a2a;">
<div id="tour-symptom-list" class="space-y-2 overflow-y-auto pr-1" style="max-height:380px;">
@foreach($symptoms as $symptom)
<label class="symptom-item flex items-center gap-3 p-3 rounded-xl cursor-pointer border transition"
style="border-color:#ede8df;background:white;">
<input type="checkbox"
class="symptom-checkbox w-4 h-4 accent-green-700"
data-code="{{ $symptom->code }}"
onchange="updateCount(); updateCheckStyle(this)">
<span>
<span class="text-xs font-bold px-1.5 py-0.5 rounded mr-1" style="background:#d8f3dc;color:#2d6a4f;">{{ $symptom->code }}</span>
<span class="text-sm" style="color:#1a3a2a;">{{ $symptom->name }}</span>
</span>
</label>
@endforeach
</div>
<div class="mt-4 flex items-center justify-between">
<span class="text-sm" style="color:#5a7a67;">
Dipilih: <strong id="count" style="color:#2d6a4f;">0</strong> gejala
<span id="count-warning" class="text-xs ml-1" style="color:#dc2626;display:none;">(minimal 4)</span>
</span>
</div>
<button type="button" onclick="nextStep()" id="tour-next-btn"
class="mt-4 w-full py-3 rounded-xl text-sm font-semibold text-white transition"
style="background:#1a3a2a;"
onmouseover="this.style.background='#2d6a4f'" onmouseout="this.style.background='#1a3a2a'">
Lanjut
</button>
</div>
{{-- STEP 2: Tingkat Keyakinan --}}
<div id="step2" class="hidden">
<h3 class="text-lg font-bold mb-1" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Tingkat Keyakinan</h3>
<p class="text-xs mb-5" style="color:#8fa89a;">Seberapa yakin kamu melihat gejala ini pada tanaman?</p>
<div id="cf-container" class="space-y-4"></div>
<div class="flex gap-3 mt-6">
<button type="button" onclick="prevStep()"
class="px-5 py-3 rounded-xl text-sm font-semibold border transition"
style="border-color:#ede8df;color:#5a7a67;background:white;"
onmouseover="this.style.background='#f8f4ee'" onmouseout="this.style.background='white'">
Kembali
</button>
<button type="button" onclick="nextStep()"
class="flex-1 py-3 rounded-xl text-sm font-semibold text-white transition"
style="background:#1a3a2a;"
onmouseover="this.style.background='#2d6a4f'" onmouseout="this.style.background='#1a3a2a'">
Lanjut
</button>
</div>
</div>
{{-- STEP 3: Konfirmasi --}}
<div id="step3" class="hidden">
<h3 class="text-lg font-bold mb-1" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Konfirmasi</h3>
<p class="text-xs mb-5" style="color:#8fa89a;">Periksa kembali sebelum diagnosa dimulai</p>
<div id="summary" class="space-y-2 mb-6"></div>
<div class="flex gap-3">
<button type="button" onclick="prevStep()"
class="px-5 py-3 rounded-xl text-sm font-semibold border transition"
style="border-color:#ede8df;color:#5a7a67;background:white;"
onmouseover="this.style.background='#f8f4ee'" onmouseout="this.style.background='white'">
Kembali
</button>
<button type="submit"
class="flex-1 py-3 rounded-xl text-sm font-semibold text-white transition"
style="background:#1a3a2a;"
onmouseover="this.style.background='#2d6a4f'" onmouseout="this.style.background='#1a3a2a'">
Diagnosa Sekarang
</button>
</div>
</div>
</form>
</div>
</div>
<script>
let step = 1;
const stepLabels = ['', 'Pilih Gejala', 'Tingkat Keyakinan', 'Konfirmasi'];
const progressWidth = ['', '33%', '66%', '100%'];
function updateCount() {
const checked = document.querySelectorAll('.symptom-checkbox:checked').length;
document.getElementById('count').innerText = checked;
document.getElementById('count-warning').style.display = checked < 4 ? 'inline' : 'none';
}
function updateCheckStyle(cb) {
const label = cb.closest('label');
if (cb.checked) {
label.style.background = '#f0fdf4';
label.style.borderColor = '#b7ddc4';
} else {
label.style.background = 'white';
label.style.borderColor = '#ede8df';
}
}
function nextStep() {
if (step === 1) {
const count = document.querySelectorAll('.symptom-checkbox:checked').length;
if (count < 4) {
document.getElementById('count-warning').style.display = 'inline';
return;
}
buildCF();
}
// ── VALIDASI: blokir kalau semua CF = 0.0 ──
if (step === 2) {
const checked = document.querySelectorAll('.symptom-checkbox:checked');
let semuaTidakYakin = true;
checked.forEach(cb => {
const kode = cb.getAttribute('data-code');
const val = document.getElementById('cf-val-' + kode)?.value;
if (val && parseFloat(val) > 0) semuaTidakYakin = false;
});
if (semuaTidakYakin) {
showCFWarning();
return;
}
buildSummary();
}
document.getElementById('step' + step).classList.add('hidden');
step++;
document.getElementById('step' + step).classList.remove('hidden');
document.getElementById('step-number').innerText = step;
document.getElementById('step-label').innerText = stepLabels[step];
document.getElementById('progress-bar').style.width = progressWidth[step];
}
function prevStep() {
document.getElementById('step' + step).classList.add('hidden');
step--;
document.getElementById('step' + step).classList.remove('hidden');
document.getElementById('step-number').innerText = step;
document.getElementById('step-label').innerText = stepLabels[step];
document.getElementById('progress-bar').style.width = progressWidth[step];
}
const cfOptions = [
{ val: '0.8', label: 'Sangat Yakin' },
{ val: '0.6', label: 'Yakin' },
{ val: '0.4', label: 'Cukup Yakin' },
{ val: '0.2', label: 'Kurang Yakin' },
{ val: '0.0', label: 'Tidak Yakin' },
];
function buildCF() {
const container = document.getElementById('cf-container');
container.innerHTML = '';
document.querySelectorAll('.symptom-checkbox:checked').forEach(cb => {
const label = cb.closest('label').innerText.trim();
const kode = cb.getAttribute('data-code');
const buttons = cfOptions.map(opt => `
<button type="button"
onclick="selectCF(this, '${kode}', '${opt.val}')"
data-val="${opt.val}"
class="cf-btn-${kode} py-2 px-1 rounded-xl border-2 text-xs font-semibold transition text-center"
style="border-color:#ede8df;color:#5a7a67;background:white;">
${opt.label}
</button>
`).join('');
container.innerHTML += `
<div class="p-4 rounded-xl border border-[#ede8df]" style="background:#fafaf8;">
<p class="text-sm font-semibold mb-3" style="color:#1a3a2a;">${label}</p>
<input type="hidden" name="symptoms[${kode}]" id="cf-val-${kode}" value="0.8">
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;">${buttons}</div>
</div>
`;
});
// Set default "Sangat Yakin" terpilih
document.querySelectorAll('.symptom-checkbox:checked').forEach(cb => {
const kode = cb.getAttribute('data-code');
const defaultBtn = document.querySelector(`.cf-btn-${kode}[data-val="0.8"]`);
if (defaultBtn) {
setSelectedStyle(defaultBtn);
document.getElementById(`cf-val-${kode}`).value = '0.8';
}
});
}
function selectCF(btn, kode, val) {
document.querySelectorAll(`.cf-btn-${kode}`).forEach(b => {
b.style.borderColor = '#ede8df';
b.style.background = 'white';
b.style.color = '#5a7a67';
});
setSelectedStyle(btn);
document.getElementById(`cf-val-${kode}`).value = val;
}
function setSelectedStyle(btn) {
btn.style.borderColor = '#2d6a4f';
btn.style.background = '#d8f3dc';
btn.style.color = '#1a3a2a';
}
function buildSummary() {
const summary = document.getElementById('summary');
summary.innerHTML = '';
const cfMap = { '0.8':'Sangat Yakin','0.6':'Yakin','0.4':'Cukup Yakin','0.2':'Kurang Yakin','0.0':'Tidak Yakin' };
document.querySelectorAll('.symptom-checkbox:checked').forEach(cb => {
const label = cb.closest('label').innerText.trim();
const kode = cb.getAttribute('data-code');
const val = document.getElementById(`cf-val-${kode}`).value;
summary.innerHTML += `
<div class="flex items-center justify-between p-3 rounded-xl" style="background:#f0fdf4;border:1px solid #b7ddc4;">
<span class="text-sm" style="color:#1a3a2a;">${label}</span>
<span class="text-xs font-semibold px-2 py-1 rounded-full ml-2 flex-shrink-0" style="background:#d8f3dc;color:#2d6a4f;">${cfMap[val] ?? val}</span>
</div>
`;
});
}
document.getElementById('search').addEventListener('input', function () {
const keyword = this.value.toLowerCase();
document.querySelectorAll('.symptom-item').forEach(item => {
item.style.display = item.innerText.toLowerCase().includes(keyword) ? '' : 'none';
});
});
/* ── Popup warning CF semua 0.0 ── */
function showCFWarning() {
if (document.getElementById('cf-warn-overlay')) return;
const overlay = document.createElement('div');
overlay.id = 'cf-warn-overlay';
overlay.style.cssText = `
position:fixed;inset:0;z-index:99999;
background:rgba(15,30,20,.55);backdrop-filter:blur(4px);
display:flex;align-items:center;justify-content:center;padding:1rem;
`;
overlay.innerHTML = `
<div style="
background:#fff;border-radius:20px;padding:2rem 1.75rem;
max-width:360px;width:100%;text-align:center;
box-shadow:0 24px 60px rgba(10,25,15,.3);
animation:warnPop .3s cubic-bezier(.22,.97,.44,1) both;
">
<div style="font-size:2.5rem;margin-bottom:.75rem;">⚠️</div>
<h3 style="
font-family:'Playfair Display',serif;
font-size:1.1rem;color:#1a3a2a;margin-bottom:.5rem;
">Tingkat Keyakinan Terlalu Rendah</h3>
<p style="font-size:.875rem;color:#5a7a67;line-height:1.6;margin-bottom:1.5rem;">
Kamu memilih <strong>Tidak Yakin</strong> untuk semua gejala.<br>
Minimal satu gejala harus memiliki tingkat keyakinan di atas <em>Tidak Yakin</em>
agar diagnosa dapat diproses.
</p>
<button onclick="closeCFWarning()"
style="
width:100%;padding:.8rem;border-radius:12px;
background:#1a3a2a;color:white;border:none;
font-family:'DM Sans',sans-serif;font-size:.9rem;
font-weight:600;cursor:pointer;
"
onmouseover="this.style.background='#2d6a4f'"
onmouseout="this.style.background='#1a3a2a'">
Oke, Saya Ubah
</button>
</div>
<style>
@keyframes warnPop {
from { opacity:0; transform:scale(.92) translateY(16px); }
to { opacity:1; transform:none; }
}
</style>
`;
document.body.appendChild(overlay);
overlay.addEventListener('click', function(e) {
if (e.target === overlay) closeCFWarning();
});
}
function closeCFWarning() {
const el = document.getElementById('cf-warn-overlay');
if (el) el.remove();
}
/* ============================================================
GUIDED TOUR Halaman Diagnosa
============================================================ */
const diagTourSteps = [
{
title: 'Halaman Diagnosa 🌿',
desc: 'Di sini kamu bisa mendiagnosa penyakit tanaman tebumu. Ada <b>3 langkah</b> yang perlu kamu ikuti.',
target: null,
},
{
title: 'Progress Bar',
desc: 'Bar ini menunjukkan kamu sedang di langkah berapa. Total ada <b>3 langkah</b>: Pilih Gejala → Tingkat Keyakinan → Konfirmasi.',
targetSel: '#tour-progress',
pos: 'bottom',
},
{
title: 'Daftar Gejala',
desc: '<b>Centang minimal 4 gejala</b> yang kamu temukan pada tanaman tebu. Bisa juga cari gejala pakai kolom pencarian di atas.',
targetSel: '#tour-symptom-list',
pos: 'top',
},
{
title: 'Tombol Lanjut',
desc: 'Setelah memilih minimal 4 gejala, klik tombol ini untuk ke langkah berikutnya yaitu mengisi <b>tingkat keyakinan</b> tiap gejala.',
targetSel: '#tour-next-btn',
pos: 'top',
offsetY: -120
},
];
let _dTourStep = 0;
function _dTourEl(id) { return document.getElementById(id); }
function _dTourFindTarget(step) {
if (!step.targetSel) return null;
return document.querySelector(step.targetSel);
}
function _dTourStart() {
_dTourStep = 0;
if (!_dTourEl('_dtour_hl')) { const d = document.createElement('div'); d.id = '_dtour_hl'; document.body.appendChild(d); }
if (!_dTourEl('_dtour_tip')) { const d = document.createElement('div'); d.id = '_dtour_tip'; document.body.appendChild(d); }
if (!_dTourEl('_dtour_dim')) { const d = document.createElement('div'); d.id = '_dtour_dim'; document.body.appendChild(d); }
_dTourRender();
}
function _dTourRender() {
const step = diagTourSteps[_dTourStep];
const total = diagTourSteps.length;
const hl = _dTourEl('_dtour_hl');
const tip = _dTourEl('_dtour_tip');
const dim = _dTourEl('_dtour_dim');
const target = _dTourFindTarget(step);
dim.style.cssText = `position:fixed;inset:0;z-index:99990;background:rgba(0,0,0,0.52);pointer-events:auto;`;
if (target) {
const r = target.getBoundingClientRect();
const pad = 8;
hl.style.cssText = `
position:fixed;z-index:99992;pointer-events:none;
top:${r.top-pad}px;left:${r.left-pad}px;
width:${r.width+pad*2}px;height:${r.height+pad*2}px;
border:2.5px solid #2d6a4f;border-radius:12px;
box-shadow:0 0 0 9999px rgba(0,0,0,0.52);
transition:all .35s ease;`;
dim.style.background = 'transparent';
} else {
hl.style.cssText = 'display:none;';
}
const dots = Array.from({length:total},(_,i)=>
`<div style="width:${i===_dTourStep?16:6}px;height:6px;border-radius:3px;
background:${i===_dTourStep?'#2d6a4f':'#d1d5db'};
transition:all .3s;display:inline-block;margin-right:4px;"></div>`
).join('');
let tipPos = 'top:50%;left:50%;transform:translate(-50%,-50%);';
if (target) {
const r = target.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
const tipH = 340;
const tipW = 310;
if (step.pos === 'bottom' && r.bottom + tipH < vh) {
tipPos = `top:${r.bottom + 16}px;left:${Math.max(16, Math.min(r.left, vw - tipW))}px;transform:none;`;
} else if (step.pos === 'top') {
let topPos = r.top - tipH - 20 + (step.offsetY || 0);
if (topPos < 20) topPos = 20;
tipPos = `top:${topPos}px;left:${Math.max(16, Math.min(r.left, vw - tipW))}px;transform:none;`;
} else if (step.pos === 'right' && r.right + tipW < vw) {
tipPos = `top:${Math.min(r.top, vh - tipH - 20)}px;left:${r.right + 16}px;transform:none;`;
} else {
tipPos = `top:50%;left:50%;transform:translate(-50%,-50%);`;
}
}
tip.style.cssText = `
position:fixed;${tipPos}
z-index:99999;background:#fff;border-radius:16px;
padding:22px 24px;width:290px;
box-shadow:0 12px 40px rgba(0,0,0,0.18);
font-family:inherit;pointer-events:auto;`;
tip.innerHTML = `
<div style="font-size:11px;color:#2d6a4f;font-weight:700;text-transform:uppercase;
letter-spacing:.8px;margin-bottom:6px;">
Langkah ${_dTourStep+1} dari ${total}
</div>
<div style="font-size:15px;font-weight:700;color:#1a3a2a;margin-bottom:8px;
font-family:'Playfair Display',serif;">
${step.title}
</div>
<div style="font-size:13px;color:#5a7a67;line-height:1.65;margin-bottom:18px;">
${step.desc}
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<div>${dots}</div>
<div style="display:flex;gap:10px;align-items:center;">
<button onclick="_dTourEnd()"
style="background:none;border:none;font-size:12px;color:#9ca3af;cursor:pointer;padding:4px;">
Lewati
</button>
<button onclick="_dTourNext()"
style="background:#2d6a4f;color:#fff;border:none;
padding:9px 18px;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;">
${_dTourStep === total-1 ? 'Siap! ✓' : 'Lanjut →'}
</button>
</div>
</div>`;
}
function _dTourNext() {
_dTourStep++;
if (_dTourStep >= diagTourSteps.length) { _dTourEnd(); return; }
_dTourRender();
}
function _dTourEnd() {
['_dtour_hl','_dtour_tip','_dtour_dim'].forEach(id => {
const el = _dTourEl(id);
if (el) el.remove();
});
localStorage.setItem('sipakartebu_diag_tour_done', '1');
}
window.addEventListener('load', function () {
if (!localStorage.getItem('sipakartebu_diag_tour_done')) {
setTimeout(_dTourStart, 800);
}
});
function ulangiTourDiagnosa() {
localStorage.removeItem('sipakartebu_diag_tour_done');
_dTourStart();
}
</script>
@endsection

View File

@ -0,0 +1,71 @@
@extends('layouts.app')
@section('title', 'Riwayat Diagnosa')
@section('page-title', 'Riwayat Diagnosa')
@section('content')
<div class="bg-white rounded-2xl border border-[#ede8df]" style="box-shadow:0 4px 16px rgba(26,58,42,.08);">
<!-- Header -->
<div class="flex items-center justify-between p-6" style="border-bottom:1px solid #ede8df;">
<h3 class="text-lg font-bold" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
Semua Riwayat Diagnosa
</h3>
<a href="{{ route('diagnosis.create') }}"
class="px-4 py-2 rounded-xl text-sm font-semibold text-white transition"
style="background:#2d6a4f;">
<i class="fas fa-plus mr-1"></i> Diagnosa Baru
</a>
</div>
<!-- Table -->
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr style="border-bottom:2px solid #ede8df;">
<th class="text-left py-3 px-6" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Tanggal</th>
<th class="text-left py-3 px-6" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Nama Tanaman</th>
<th class="text-left py-3 px-6" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Penyakit</th>
<th class="text-left py-3 px-6" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Akurasi</th>
<th class="text-left py-3 px-6" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($diagnoses as $diagnosis)
<tr style="border-bottom:1px solid rgba(237,232,223,.6);" class="hover:bg-[#d8f3dc]/20 transition">
<td class="py-4 px-6 text-sm" style="color:#5a7a67;">{{ $diagnosis->created_at->format('d/m/Y') }}</td>
<td class="py-4 px-6 text-sm font-medium" style="color:#1a3a2a;">{{ $diagnosis->plant_name }}</td>
<td class="py-4 px-6 text-sm" style="color:#1a3a2a;">{{ $diagnosis->disease_name }}</td>
<td class="py-4 px-6">
<span class="px-3 py-1 rounded-full text-sm font-semibold" style="background:#d8f3dc;color:#2d6a4f;">
{{ $diagnosis->confidence }}%
</span>
</td>
<td class="py-4 px-6">
<a href="{{ route('diagnosis.result', $diagnosis->id) }}"
class="text-sm font-medium px-3 py-1 rounded-lg transition"
style="color:#2d6a4f;background:#f0fdf4;">
<i class="fas fa-eye mr-1"></i> Detail
</a>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="text-center py-16" style="color:#8fa89a;">
<i class="fas fa-clipboard text-4xl mb-3 block" style="color:#c8d8cc;"></i>
Belum ada riwayat diagnosa
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
@if($diagnoses->hasPages())
<div class="p-6" style="border-top:1px solid #ede8df;">
{{ $diagnoses->links() }}
</div>
@endif
</div>
@endsection

View File

@ -0,0 +1,319 @@
@extends('layouts.app')
@section('title', 'Hasil Diagnosa')
@section('page-title', 'Hasil Diagnosa')
@section('content')
<div class="max-w-4xl mx-auto space-y-6">
<!-- HEADER -->
<div id="tour-result-header" class="bg-white p-6 rounded-2xl border text-center">
<h2 class="text-2xl font-bold text-green-800">
Diagnosa Selesai
</h2>
<p class="text-sm text-gray-500">
{{ now()->format('d F Y, H:i') }} WIB
</p>
</div>
<!-- INFO TANAMAN -->
<div class="bg-white p-6 rounded-2xl border">
<h3 class="font-bold mb-3">Informasi Tanaman</h3>
<p><strong>Nama:</strong> {{ $diagnosis->plant_name }}</p>
</div>
<!-- GEJALA -->
<div id="tour-result-gejala" class="bg-white p-6 rounded-2xl border">
<h3 class="font-bold mb-3">
Gejala yang Dilaporkan ({{ count($diagnosis->symptoms ?? []) }} gejala)
</h3>
<ul class="space-y-2">
@foreach($diagnosis->symptoms ?? [] as $kode => $data)
@php
$cfVal = $data['cf'];
if ($cfVal == 0.0) {
$cfColor = '#6b7280'; $cfBg = '#f3f4f6';
} elseif ($cfVal <= 0.2) {
$cfColor = '#991b1b'; $cfBg = '#fee2e2';
} elseif ($cfVal <= 0.4) {
$cfColor = '#92400e'; $cfBg = '#fef3c7';
} elseif ($cfVal <= 0.6) {
$cfColor = '#1d4ed8'; $cfBg = '#dbeafe';
} else {
$cfColor = '#2d6a4f'; $cfBg = '#d8f3dc';
}
@endphp
<li>
{{ $kode }} - {{ $data['nama'] }}
<span style="display:inline-block;margin-left:6px;padding:1px 8px;border-radius:999px;
font-size:11px;font-weight:600;background:{{ $cfBg }};color:{{ $cfColor }};">
{{ number_format($cfVal, 1) }}
</span>
</li>
@endforeach
</ul>
</div>
<!-- HASIL -->
<div id="tour-result-penyakit" class="bg-white p-6 rounded-2xl border">
<h3 class="font-bold mb-3">Hasil Diagnosa</h3>
<p><strong>Penyakit:</strong> {{ $diagnosis->disease_name }}</p>
@php
$conf = $diagnosis->confidence;
$confVal = $conf / 100;
if ($confVal <= 0.20) {
$label = 'Tidak Terdeteksi'; $labelColor = '#6b7280'; $labelBg = '#f3f4f6';
} elseif ($confVal <= 0.40) {
$label = 'Rendah'; $labelColor = '#991b1b'; $labelBg = '#fee2e2';
} elseif ($confVal <= 0.60) {
$label = 'Sedang'; $labelColor = '#92400e'; $labelBg = '#fef3c7';
} elseif ($confVal <= 0.80) {
$label = 'Tinggi'; $labelColor = '#1d4ed8'; $labelBg = '#dbeafe';
} else {
$label = 'Sangat Tinggi'; $labelColor = '#2d6a4f'; $labelBg = '#d8f3dc';
}
@endphp
<p>
<strong>Kepercayaan:</strong> {{ $conf }}%
<span style="display:inline-block;margin-left:8px;padding:2px 10px;border-radius:999px;
font-size:12px;font-weight:600;background:{{ $labelBg }};color:{{ $labelColor }};">
{{ $label }}
</span>
</p>
<div class="mt-4">
<h4 class="font-bold mb-2">Penanganan:</h4>
@php $treatments = explode(';', $diagnosis->treatment); @endphp
<ol class="list-decimal ml-5 space-y-1">
@foreach($treatments as $item)
@if(trim($item) != '')
<li>{{ trim($item) }}</li>
@endif
@endforeach
</ol>
<div id="tour-result-catatan" class="mt-4 p-3 rounded-lg bg-yellow-50 border text-sm text-yellow-800">
<strong>Catatan:</strong><br>
Hasil diagnosa ini bersifat awal. Untuk penanganan yang lebih tepat dan akurat,
disarankan untuk berkonsultasi langsung dengan ahli tanaman atau penyuluh pertanian.
</div>
</div>
</div>
<!-- DEBUG SEMENTARA hapus setelah selesai debug
@isset($debugInfo)
<div class="bg-gray-900 text-green-300 p-4 rounded-2xl text-xs font-mono overflow-x-auto">
<p class="text-yellow-400 font-bold mb-3">🔍 gambaran perhitungan manual Langkah Perhitungan CF (hapus setelah selesai)</p>
@foreach($debugInfo as $item)
<div class="mb-4 border-b border-gray-700 pb-3">
<p class="text-white font-bold">{{ $item['nama'] }} {{ $item['persentase'] }}%</p>
@if(isset($item['steps']))
<table class="mt-1 w-full text-xs">
<tr class="text-gray-400">
<th class="text-left pr-4">Gejala</th>
<th class="text-left pr-4">CF Pakar</th>
<th class="text-left pr-4">CF User</th>
<th class="text-left pr-4">CF Komb</th>
<th class="text-left">CF Akumulasi</th>
</tr>
@foreach($item['steps'] as $step)
<tr>
<td class="pr-4">{{ $step['gejala'] }}</td>
<td class="pr-4">{{ $step['cf_pakar'] }}</td>
<td class="pr-4">{{ $step['cf_user'] }}</td>
<td class="pr-4">{{ $step['cf_komb'] }}</td>
<td>{{ $step['cf_akum'] }}</td>
</tr>
@endforeach
</table>
@endif
</div>
@endforeach
</div>
@endisset -->
<!-- ACTION -->
<div id="tour-result-action" class="flex gap-4">
<a href="{{ route('diagnosis.create') }}"
class="flex-1 text-center py-3 rounded-full text-white bg-green-800">
+ Diagnosa Baru
</a>
<a href="{{ route('history') }}"
class="flex-1 text-center py-3 rounded-full border">
Lihat Riwayat
</a>
</div>
</div>
<script>
const resultTourSteps = [
{
title: 'Hasil Diagnosa Kamu! 🎉',
desc: 'Ini adalah hasil analisa dari gejala yang kamu masukkan. Mari kita lihat satu per satu.',
targetSel: null,
},
{
title: 'Gejala yang Dilaporkan',
desc: 'Di sini tampil semua gejala yang kamu pilih tadi beserta nilai <b>CF (Certainty Factor)</b> — yaitu tingkat keyakinanmu.',
targetSel: '#tour-result-gejala',
},
{
title: 'Nama Penyakit & Penanganan',
desc: 'Ini hasil diagnosa utama: nama penyakit, tingkat kepercayaan model, dan <b>langkah penanganannya</b>.',
targetSel: '#tour-result-penyakit',
},
{
title: 'Catatan Penting',
desc: 'Hasil ini bersifat <b>awal</b>. Tetap konsultasikan dengan ahli pertanian untuk penanganan yang lebih tepat ya!',
targetSel: '#tour-result-catatan',
},
{
title: 'Langkah Selanjutnya',
desc: 'Kamu bisa langsung <b>Diagnosa Baru</b> atau melihat semua riwayat diagnosamu.',
targetSel: '#tour-result-action',
},
];
let _rStep = 0;
const _rGet = id => document.getElementById(id);
const _rTarget = step => step.targetSel ? document.querySelector(step.targetSel) : null;
function _rStart() {
_rStep = 0;
['_rt_hl','_rt_tip','_rt_dim'].forEach(id => {
if (!_rGet(id)) {
const d = document.createElement('div');
d.id = id;
document.body.appendChild(d);
}
});
_rRender();
}
function _rRender() {
const step = resultTourSteps[_rStep];
const total = resultTourSteps.length;
const hl = _rGet('_rt_hl');
const tip = _rGet('_rt_tip');
const dim = _rGet('_rt_dim');
const target = _rTarget(step);
dim.style.cssText = 'position:fixed;inset:0;z-index:99990;background:rgba(0,0,0,0.52);pointer-events:auto;';
tip.style.opacity = '0';
hl.style.opacity = '0';
hl.style.display = 'block';
const dots = Array.from({length: total}, (_, i) =>
`<span style="display:inline-block;width:${i===_rStep?16:6}px;height:6px;
border-radius:3px;background:${i===_rStep?'#2d6a4f':'#d1d5db'};
transition:all .3s;margin-right:4px;"></span>`
).join('');
tip.innerHTML = `
<div style="font-size:11px;color:#2d6a4f;font-weight:700;text-transform:uppercase;
letter-spacing:.8px;margin-bottom:6px;">Langkah ${_rStep+1} dari ${total}</div>
<div style="font-size:15px;font-weight:700;color:#1a3a2a;margin-bottom:8px;
font-family:'Playfair Display',serif;">${step.title}</div>
<div style="font-size:13px;color:#5a7a67;line-height:1.65;margin-bottom:18px;">${step.desc}</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<div>${dots}</div>
<div style="display:flex;gap:10px;align-items:center;">
<button onclick="_rEnd()"
style="background:none;border:none;font-size:12px;color:#9ca3af;cursor:pointer;padding:4px;">
Lewati
</button>
<button onclick="_rNext()"
style="background:#2d6a4f;color:#fff;border:none;padding:9px 18px;
border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;">
${_rStep === total-1 ? 'Mengerti! ✓' : 'Lanjut →'}
</button>
</div>
</div>`;
tip.style.cssText = `
position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
z-index:99999;background:#fff;border-radius:16px;
padding:22px 24px;width:290px;
box-shadow:0 12px 40px rgba(0,0,0,0.18);
font-family:inherit;pointer-events:auto;opacity:0;`;
function _place() {
if (!target) {
hl.style.display = 'none';
dim.style.background = 'rgba(0,0,0,0.52)';
tip.style.opacity = '1';
return;
}
const r = target.getBoundingClientRect();
const pad = 8;
const vw = window.innerWidth;
const vh = window.innerHeight;
const TW = 310;
const TH = 230;
hl.style.cssText = `
position:fixed;z-index:99992;pointer-events:none;
top:${r.top - pad}px;left:${r.left - pad}px;
width:${r.width + pad*2}px;height:${r.height + pad*2}px;
border:2.5px solid #2d6a4f;border-radius:12px;
box-shadow:0 0 0 9999px rgba(0,0,0,0.52);
transition:all .3s ease;opacity:1;display:block;`;
dim.style.background = 'transparent';
let top, left;
left = Math.max(16, Math.min(r.left, vw - TW - 16));
if (r.top - TH - 16 >= 0) {
top = r.top - TH - 12;
} else if (r.bottom + TH + 16 <= vh) {
top = r.bottom + 12;
} else {
top = Math.max(16, (vh - TH) / 2);
left = Math.max(16, (vw - TW) / 2);
}
tip.style.top = top + 'px';
tip.style.left = left + 'px';
tip.style.transform = 'none';
tip.style.opacity = '1';
}
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(_place, 450);
} else {
_place();
}
}
function _rNext() {
_rStep++;
if (_rStep >= resultTourSteps.length) { _rEnd(); return; }
_rRender();
}
function _rEnd() {
['_rt_hl','_rt_tip','_rt_dim'].forEach(id => {
const el = _rGet(id);
if (el) el.remove();
});
localStorage.setItem('sipakartebu_result_tour_done', '1');
}
window.addEventListener('load', function () {
if (!localStorage.getItem('sipakartebu_result_tour_done')) {
setTimeout(_rStart, 800);
}
});
function ulangiTourHasil() {
localStorage.removeItem('sipakartebu_result_tour_done');
_rStart();
}
</script>
@endsection

View File

@ -0,0 +1,279 @@
@extends('layouts.app')
@section('title', isset($isEdit) ? 'Edit Penyakit' : 'Tambah Penyakit')
@section('page-title', isset($isEdit) ? 'Edit Penyakit' : 'Tambah Penyakit Baru')
@section('content')
<a href="{{ route('diseases.index') }}" class="inline-flex items-center gap-2 mb-6 text-sm font-medium" style="color:#2d6a4f;">
<i class="fas fa-arrow-left"></i> Kembali
</a>
<form method="POST"
action="{{ isset($isEdit) ? route('diseases.update', $disease->id) : route('diseases.store') }}"
enctype="multipart/form-data">
@csrf
@if(isset($isEdit))
@method('PUT')
@endif
<!-- INFO PENYAKIT -->
<div class="bg-white rounded-2xl border border-[#ede8df] p-6 mb-6" style="box-shadow:0 2px 12px rgba(26,58,42,.06);">
<h3 class="text-lg font-bold mb-5" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
<i class="fas fa-info-circle mr-2" style="color:#2d6a4f;"></i>Informasi Penyakit
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;letter-spacing:.06em;">Kode Penyakit</label>
<input type="text" value="{{ $nextDiseaseCode }}" disabled
class="w-full px-4 py-3 rounded-xl text-sm" style="background:#f0fdf4;border:1.5px solid #b7ddc4;color:#2d6a4f;font-weight:700;">
</div>
<div>
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;letter-spacing:.06em;">Nama Penyakit <span style="color:red">*</span></label>
<input type="text" name="name" value="{{ old('name', isset($disease) ? $disease->name : '') }}" required placeholder="Contoh: Luka Api"
class="w-full px-4 py-3 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:#f9fafb;outline:none;">
@error('name')<p class="text-xs mt-1" style="color:red;">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;letter-spacing:.06em;">Nama Latin</label>
<input type="text" name="latin_name" value="{{ old('latin_name', isset($disease) ? $disease->latin_name : '') }}" placeholder="Contoh: Xanthomonas albilineans"
class="w-full px-4 py-3 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:#f9fafb;outline:none;">
</div>
<div>
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;letter-spacing:.06em;">Foto Penyakit</label>
@if(isset($disease) && $disease->photo)
<div class="mb-2">
<img src="{{ asset('storage/'.$disease->photo) }}" class="h-16 rounded-lg object-cover">
<p class="text-xs mt-1" style="color:#8fa89a;">Unggah foto baru untuk mengganti</p>
</div>
@endif
<input type="file" name="photo" accept="image/*"
class="w-full px-4 py-3 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:#f9fafb;">
</div>
<div class="md:col-span-2">
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;letter-spacing:.06em;">Deskripsi</label>
<textarea name="description" rows="3" placeholder="Deskripsi singkat tentang penyakit ini..."
class="w-full px-4 py-3 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:#f9fafb;outline:none;resize:none;">{{ old('description', isset($disease) ? $disease->description : '') }}</textarea>
</div>
</div>
</div>
<!-- GEJALA -->
<div class="bg-white rounded-2xl border border-[#ede8df] p-6 mb-6" style="box-shadow:0 2px 12px rgba(26,58,42,.06);">
<div class="flex items-center justify-between mb-5">
<h3 class="text-lg font-bold" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
<i class="fas fa-search mr-2" style="color:#2d6a4f;"></i>Gejala <span class="text-sm font-normal" style="color:#8fa89a;">(minimal 3)</span>
</h3>
<button type="button" onclick="tambahGejala()"
class="px-4 py-2 rounded-xl text-sm font-semibold" style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;">
<i class="fas fa-plus mr-1"></i> Tambah Gejala
</button>
</div>
@error('symptoms')<p class="text-xs mb-3" style="color:red;">{{ $message }}</p>@enderror
<div id="gejala-container" class="space-y-4">
@if(isset($isEdit) && $disease->symptoms->count() > 0)
@foreach($disease->symptoms as $i => $symptom)
<div class="gejala-item p-4 rounded-xl" style="background:#f8f4ee;border:1px solid #ede8df;">
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-bold" style="color:#2d6a4f;">Gejala {{ $i+1 }}</span>
@if($i >= 3)
<button type="button" onclick="hapusGejala(this)" class="text-xs px-2 py-1 rounded-lg" style="color:#dc2626;background:#fef2f2;">
<i class="fas fa-times"></i> Hapus
</button>
@endif
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<div class="md:col-span-2">
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;">Nama Gejala *</label>
<input type="text" name="symptoms[{{ $i }}][name]" required
value="{{ $symptom->name }}"
placeholder="Contoh: Daun berwarna kuning"
class="w-full px-3 py-2.5 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:white;outline:none;">
</div>
<div>
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;">Nilai CF (0.1 - 1.0)</label>
<input type="number" name="symptoms[{{ $i }}][cf]"
value="{{ $symptom->pivot->cf_value }}"
min="0.1" max="1.0" step="0.05"
class="w-full px-3 py-2.5 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:white;outline:none;">
</div>
<div class="md:col-span-3">
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;">Foto Gejala</label>
@if($symptom->photo)
<div class="mb-2">
<img src="{{ asset('storage/'.$symptom->photo) }}" class="h-12 rounded-lg object-cover">
<p class="text-xs mt-1" style="color:#8fa89a;">Unggah foto baru untuk mengganti</p>
</div>
@endif
<input type="file" name="symptoms[{{ $i }}][photo]" accept="image/*"
class="w-full px-3 py-2.5 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:white;">
</div>
</div>
</div>
@endforeach
@else
@for($i = 0; $i < 3; $i++)
<div class="gejala-item p-4 rounded-xl" style="background:#f8f4ee;border:1px solid #ede8df;">
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-bold" style="color:#2d6a4f;">Gejala {{ $i+1 }}</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<div class="md:col-span-2">
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;">Nama Gejala *</label>
<input type="text" name="symptoms[{{ $i }}][name]" required placeholder="Contoh: Daun berwarna kuning"
class="w-full px-3 py-2.5 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:white;outline:none;">
</div>
<div>
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;">Nilai CF (0.1 - 1.0)</label>
<input type="number" name="symptoms[{{ $i }}][cf]" value="0.7" min="0.1" max="1.0" step="0.05"
class="w-full px-3 py-2.5 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:white;outline:none;">
</div>
<div class="md:col-span-3">
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;">Foto Gejala</label>
<input type="file" name="symptoms[{{ $i }}][photo]" accept="image/*"
class="w-full px-3 py-2.5 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:white;">
</div>
</div>
</div>
@endfor
@endif
</div>
</div>
<!-- PENANGANAN -->
<div class="bg-white rounded-2xl border border-[#ede8df] p-6 mb-6" style="box-shadow:0 2px 12px rgba(26,58,42,.06);">
<div class="flex items-center justify-between mb-5">
<h3 class="text-lg font-bold" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
<i class="fas fa-medkit mr-2" style="color:#2d6a4f;"></i>Cara Penanganan <span class="text-sm font-normal" style="color:#8fa89a;">(minimal 3)</span>
</h3>
<button type="button" onclick="tambahPenanganan()"
class="px-4 py-2 rounded-xl text-sm font-semibold" style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;">
<i class="fas fa-plus mr-1"></i> Tambah Penanganan
</button>
</div>
@error('treatments')<p class="text-xs mb-3" style="color:red;">{{ $message }}</p>@enderror
<div id="penanganan-container" class="space-y-3">
@if(isset($isEdit) && $disease->treatments->count() > 0)
@foreach($disease->treatments as $i => $t)
<div class="penanganan-item flex gap-3 items-start">
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-bold flex-shrink-0 mt-1"
style="background:#2d6a4f;">{{ $i+1 }}</div>
<div class="flex-1">
<input type="text" name="treatments[{{ $i }}][description]" required
value="{{ $t->description }}"
placeholder="Contoh: Gunakan bibit sehat dan bebas penyakit"
class="w-full px-4 py-3 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:#f9fafb;outline:none;">
</div>
@if($i >= 3)
<button type="button" onclick="hapusPenanganan(this)" class="mt-1 text-xs px-2 py-2 rounded-lg flex-shrink-0" style="color:#dc2626;background:#fef2f2;">
<i class="fas fa-times"></i>
</button>
@endif
</div>
@endforeach
@else
@for($i = 0; $i < 3; $i++)
<div class="penanganan-item flex gap-3 items-start">
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-bold flex-shrink-0 mt-1"
style="background:#2d6a4f;">{{ $i+1 }}</div>
<div class="flex-1">
<input type="text" name="treatments[{{ $i }}][description]" required
placeholder="Contoh: Gunakan bibit sehat dan bebas penyakit"
class="w-full px-4 py-3 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:#f9fafb;outline:none;">
</div>
</div>
@endfor
@endif
</div>
</div>
<!-- SUBMIT -->
<div class="flex gap-3">
<button type="submit" class="px-8 py-3 rounded-xl text-sm font-semibold text-white" style="background:#2d6a4f;">
<i class="fas fa-save mr-2"></i>{{ isset($isEdit) ? 'Update Penyakit' : 'Simpan Penyakit' }}
</button>
<a href="{{ isset($isEdit) ? route('diseases.show', $disease->id) : route('diseases.index') }}"
class="px-8 py-3 rounded-xl text-sm font-semibold" style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;">
Batal
</a>
</div>
</form>
@endsection
@push('scripts')
<script>
let gejalaCount = {{ isset($isEdit) ? $disease->symptoms->count() : 3 }};
let penangananCount = {{ isset($isEdit) ? $disease->treatments->count() : 3 }};
function tambahGejala() {
gejalaCount++;
const container = document.getElementById('gejala-container');
const div = document.createElement('div');
div.className = 'gejala-item p-4 rounded-xl';
div.style.cssText = 'background:#f8f4ee;border:1px solid #ede8df;';
div.innerHTML = `
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-bold" style="color:#2d6a4f;">Gejala ${gejalaCount}</span>
<button type="button" onclick="hapusGejala(this)" class="text-xs px-2 py-1 rounded-lg" style="color:#dc2626;background:#fef2f2;">
<i class="fas fa-times"></i> Hapus
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<div class="md:col-span-2">
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;">Nama Gejala *</label>
<input type="text" name="symptoms[${gejalaCount-1}][name]" required placeholder="Contoh: Daun berwarna kuning"
class="w-full px-3 py-2.5 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:white;outline:none;">
</div>
<div>
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;">Nilai CF (0.1 - 1.0)</label>
<input type="number" name="symptoms[${gejalaCount-1}][cf]" value="0.7" min="0.1" max="1.0" step="0.05"
class="w-full px-3 py-2.5 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:white;outline:none;">
</div>
<div class="md:col-span-3">
<label class="block text-xs font-semibold uppercase mb-1" style="color:#5a7a67;">Foto Gejala</label>
<input type="file" name="symptoms[${gejalaCount-1}][photo]" accept="image/*"
class="w-full px-3 py-2.5 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:white;">
</div>
</div>`;
container.appendChild(div);
}
function hapusGejala(btn) {
const items = document.querySelectorAll('.gejala-item');
if (items.length <= 3) { alert('Minimal 3 gejala!'); return; }
btn.closest('.gejala-item').remove();
document.querySelectorAll('.gejala-item').forEach((el, i) => {
el.querySelector('span').textContent = 'Gejala ' + (i+1);
});
}
function tambahPenanganan() {
penangananCount++;
const container = document.getElementById('penanganan-container');
const div = document.createElement('div');
div.className = 'penanganan-item flex gap-3 items-start';
div.innerHTML = `
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-bold flex-shrink-0 mt-1"
style="background:#2d6a4f;">${penangananCount}</div>
<div class="flex-1">
<input type="text" name="treatments[${penangananCount-1}][description]" required
placeholder="Contoh: Gunakan bibit sehat dan bebas penyakit"
class="w-full px-4 py-3 rounded-xl text-sm" style="border:1.5px solid #e5e7eb;background:#f9fafb;outline:none;">
</div>
<button type="button" onclick="hapusPenanganan(this)" class="mt-1 text-xs px-2 py-2 rounded-lg flex-shrink-0" style="color:#dc2626;background:#fef2f2;">
<i class="fas fa-times"></i>
</button>`;
container.appendChild(div);
}
function hapusPenanganan(btn) {
const items = document.querySelectorAll('.penanganan-item');
if (items.length <= 3) { alert('Minimal 3 cara penanganan!'); return; }
btn.closest('.penanganan-item').remove();
document.querySelectorAll('.penanganan-item').forEach((el, i) => {
el.querySelector('div.w-8').textContent = i+1;
});
}
</script>
@endpush

View File

@ -0,0 +1,165 @@
@extends('layouts.app')
@section('title', 'Kamus Penyakit')
@section('page-title', 'Kamus Penyakit')
@section('content')
@if(session('status'))
<div class="mb-4 px-4 py-3 rounded-xl text-sm font-medium" style="background:#f0fdf4;color:#16a34a;border:1px solid #86efac;">
<i class="fas fa-check-circle mr-2"></i>{{ session('status') }}
</div>
@endif
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<p style="color:#5a7a67;">Total {{ $diseases->count() }} penyakit terdaftar</p>
@if(auth()->user()->isAdmin())
<a href="{{ route('diseases.create') }}"
class="px-5 py-2.5 rounded-xl text-sm font-semibold text-white"
style="background:#2d6a4f;">
<i class="fas fa-plus mr-2"></i>Tambah Penyakit
</a>
@endif
</div>
<!-- Grid Kartu Penyakit -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
@forelse($diseases as $disease)
<div class="bg-white rounded-2xl border border-[#ede8df] overflow-hidden hover:shadow-lg transition"
style="box-shadow:0 2px 12px rgba(26,58,42,.06);">
<!-- Foto -->
<div class="h-44 flex items-center justify-center overflow-hidden" style="background:#f0fdf4;">
@if($disease->photo)
<img src="{{ url('storage/'.$disease->photo) }}" class="w-full h-full object-cover">
@else
<div class="text-center">
<i class="fas fa-leaf text-5xl mb-2" style="color:#b7ddc4;"></i>
<p class="text-xs" style="color:#a0b4a8;">Belum ada foto</p>
</div>
@endif
</div>
<!-- Info -->
<div class="p-5">
<div class="flex items-start justify-between mb-2">
<span class="text-xs font-bold px-2 py-1 rounded-lg" style="background:#d8f3dc;color:#2d6a4f;">
{{ $disease->code }}
</span>
<span class="text-xs" style="color:#a0b4a8;">{{ $disease->symptoms->count() }} gejala</span>
</div>
<h3 class="font-bold text-base mb-1" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
{{ $disease->name }}
</h3>
<p class="text-xs italic mb-3" style="color:#8fa89a;">{{ $disease->latin_name }}</p>
<p class="text-sm mb-4 line-clamp-2" style="color:#5a7a67;">{{ $disease->description }}</p>
<!-- Gejala tags -->
<div class="flex flex-wrap gap-1 mb-4">
@foreach($disease->symptoms->take(3) as $s)
<span class="text-xs px-2 py-1 rounded-full" style="background:#f0fdf4;color:#2d6a4f;border:1px solid #b7ddc4;">
{{ $s->code }}
</span>
@endforeach
@if($disease->symptoms->count() > 3)
<span class="text-xs px-2 py-1 rounded-full" style="background:#f0f0f0;color:#888;">
+{{ $disease->symptoms->count() - 3 }} lainnya
</span>
@endif
</div>
<!-- Tombol aksi -->
<div class="flex gap-2">
<a href="{{ route('diseases.show', $disease) }}"
class="block text-center py-2.5 rounded-xl text-sm font-semibold transition {{ auth()->user()->isAdmin() ? 'flex-1' : 'w-full' }}"
style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;">
<i class="fas fa-eye mr-1"></i> Lihat Detail
</a>
@if(auth()->user()->isAdmin())
<button
onclick="confirmDelete({{ $disease->id }}, '{{ addslashes($disease->name) }}')"
class="px-4 py-2.5 rounded-xl text-sm font-semibold transition"
style="background:#fff0f0;color:#dc2626;border:1.5px solid #fca5a5;"
onmouseover="this.style.background='#fee2e2'" onmouseout="this.style.background='#fff0f0'">
<i class="fas fa-trash"></i>
</button>
@endif
</div>
</div>
</div>
@empty
<div class="col-span-3 text-center py-20">
<i class="fas fa-book-medical text-5xl mb-4" style="color:#c8d8cc;"></i>
<p style="color:#8fa89a;">Belum ada data penyakit</p>
</div>
@endforelse
</div>
<!-- ─── Modal Konfirmasi Hapus ─── -->
<div id="deleteModal" class="fixed inset-0 z-50 hidden flex items-center justify-center"
style="background:rgba(0,0,0,0.4);">
<div class="bg-white rounded-2xl p-6 w-full max-w-sm mx-4 shadow-2xl"
style="animation: popIn .25s cubic-bezier(0.22,1,0.36,1);">
<div class="text-center mb-4">
<div class="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-3"
style="background:#fff0f0;">
<i class="fas fa-trash text-2xl" style="color:#dc2626;"></i>
</div>
<h3 class="text-lg font-bold mb-1" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
Hapus Penyakit?
</h3>
<p class="text-sm" style="color:#5a7a67;">
Kamu akan menghapus penyakit <br>
<strong id="deleteName" style="color:#1a3a2a;"></strong>
</p>
<p class="text-xs mt-2" style="color:#dc2626;">
⚠️ Data gejala & penanganan ikut terhapus dan tidak bisa dikembalikan.
</p>
</div>
<div class="flex gap-3 mt-4">
<button onclick="closeModal()"
class="flex-1 py-2.5 rounded-xl text-sm font-semibold"
style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;">
Batal
</button>
<form id="deleteForm" method="POST" class="flex-1">
@csrf
@method('DELETE')
<button type="submit"
class="w-full py-2.5 rounded-xl text-sm font-semibold text-white"
style="background:#dc2626;">
Ya, Hapus
</button>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
function confirmDelete(id, name) {
document.getElementById('deleteName').textContent = name;
document.getElementById('deleteForm').action = '{{ url("/kamus") }}/' + id;
document.getElementById('deleteModal').classList.remove('hidden');
}
function closeModal() {
document.getElementById('deleteModal').classList.add('hidden');
}
document.getElementById('deleteModal').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
</script>
<style>
@keyframes popIn {
from { transform: scale(0.85); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
</style>
@endpush

View File

@ -0,0 +1,165 @@
@extends('layouts.app')
@section('title', $disease->name)
@section('page-title', 'Detail Penyakit')
@section('content')
<!-- Navigasi atas -->
<div class="flex items-center justify-between mb-6">
<a href="{{ route('diseases.index') }}" class="inline-flex items-center gap-2 text-sm font-medium" style="color:#2d6a4f;">
<i class="fas fa-arrow-left"></i> Kembali ke Kamus
</a>
<!-- Tombol Hapus di halaman detail -->
@if(auth()->user()->isAdmin())
<a href="{{ route('diseases.edit', $disease->id) }}"
class="px-4 py-2 rounded-lg text-sm font-semibold"
style="background:#fef3c7;color:#92400e;border:1px solid #fcd34d;">
Edit Penyakit
</a>
<button onclick="confirmDelete({{ $disease->id }}, '{{ addslashes($disease->name) }}')"
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-semibold transition"
style="background:#fff0f0;color:#dc2626;border:1.5px solid #fca5a5;"
onmouseover="this.style.background='#fee2e2'" onmouseout="this.style.background='#fff0f0'">
<i class="fas fa-trash"></i> Hapus Penyakit
</button>
@endif
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Kiri: Info Penyakit -->
<div class="lg:col-span-1">
<div class="bg-white rounded-2xl border border-[#ede8df] overflow-hidden" style="box-shadow:0 2px 12px rgba(26,58,42,.06);">
<div class="h-56 flex items-center justify-center overflow-hidden" style="background:#f0fdf4;">
@if($disease->photo)
<img src="{{ asset('storage/'.$disease->photo) }}" class="w-full h-full object-cover">
@else
<i class="fas fa-leaf text-6xl" style="color:#b7ddc4;"></i>
@endif
</div>
<div class="p-5">
<span class="text-xs font-bold px-2 py-1 rounded-lg" style="background:#d8f3dc;color:#2d6a4f;">{{ $disease->code }}</span>
<h2 class="text-xl font-bold mt-3 mb-1" style="font-family:'Playfair Display',serif;color:#1a3a2a;">{{ $disease->name }}</h2>
<p class="text-sm italic mb-3" style="color:#8fa89a;">{{ $disease->latin_name }}</p>
<p class="text-sm" style="color:#5a7a67;">{{ $disease->description }}</p>
</div>
</div>
</div>
<!-- Kanan: Gejala & Penanganan -->
<div class="lg:col-span-2 space-y-6">
<!-- Gejala -->
<div class="bg-white rounded-2xl border border-[#ede8df] p-6" style="box-shadow:0 2px 12px rgba(26,58,42,.06);">
<h3 class="text-lg font-bold mb-4" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
<i class="fas fa-search mr-2" style="color:#2d6a4f;"></i>Gejala-Gejala
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@foreach($disease->symptoms as $symptom)
<div class="rounded-xl overflow-hidden border" style="border-color:#e8f5ec;">
@if($symptom->photo)
<img src="{{ asset('storage/'.$symptom->photo) }}" class="w-full h-32 object-cover">
@else
<div class="w-full h-24 flex items-center justify-center" style="background:#f0fdf4;">
<i class="fas fa-image text-2xl" style="color:#b7ddc4;"></i>
</div>
@endif
<div class="p-3">
<span class="text-xs font-bold" style="color:#2d6a4f;">{{ $symptom->code }}</span>
<p class="text-sm mt-1" style="color:#1a3a2a;">{{ $symptom->name }}</p>
<p class="text-xs mt-1" style="color:#a0b4a8;">CF: {{ $symptom->pivot->cf_value }}</p>
</div>
</div>
@endforeach
</div>
</div>
<!-- Penanganan -->
<div class="bg-white rounded-2xl border border-[#ede8df] p-6" style="box-shadow:0 2px 12px rgba(26,58,42,.06);">
<h3 class="text-lg font-bold mb-4" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
<i class="fas fa-medkit mr-2" style="color:#2d6a4f;"></i>Cara Penanganan
</h3>
<div class="space-y-3">
@foreach($disease->treatments as $i => $t)
<div class="flex gap-3 p-3 rounded-xl" style="background:#f8f4ee;">
<div class="w-7 h-7 rounded-full flex items-center justify-center flex-shrink-0 text-white text-sm font-bold"
style="background:#2d6a4f;">{{ $i+1 }}</div>
<div>
<span class="text-xs font-bold" style="color:#8fa89a;">{{ $t->code }}</span>
<p class="text-sm mt-0.5" style="color:#1a3a2a;">{{ $t->description }}</p>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
<!-- ─── Modal Konfirmasi Hapus ─── -->
<div id="deleteModal" class="fixed inset-0 z-50 hidden flex items-center justify-center"
style="background:rgba(0,0,0,0.4);">
<div class="bg-white rounded-2xl p-6 w-full max-w-sm mx-4 shadow-2xl"
style="animation: popIn .25s cubic-bezier(0.22,1,0.36,1);">
<div class="text-center mb-4">
<div class="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-3"
style="background:#fff0f0;">
<i class="fas fa-trash text-2xl" style="color:#dc2626;"></i>
</div>
<h3 class="text-lg font-bold mb-1" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
Hapus Penyakit?
</h3>
<p class="text-sm" style="color:#5a7a67;">
Kamu akan menghapus penyakit <br>
<strong id="deleteName" style="color:#1a3a2a;"></strong>
</p>
<p class="text-xs mt-2" style="color:#dc2626;">
⚠️ Data gejala & penanganan ikut terhapus dan tidak bisa dikembalikan.
</p>
</div>
<div class="flex gap-3 mt-4">
<button onclick="closeModal()"
class="flex-1 py-2.5 rounded-xl text-sm font-semibold"
style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;">
Batal
</button>
<form id="deleteForm" method="POST" class="flex-1">
@csrf
@method('DELETE')
<button type="submit"
class="w-full py-2.5 rounded-xl text-sm font-semibold text-white"
style="background:#dc2626;">
Ya, Hapus
</button>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
function confirmDelete(id, name) {
document.getElementById('deleteName').textContent = name;
document.getElementById('deleteForm').action = '/kamus/' + id;
document.getElementById('deleteModal').classList.remove('hidden');
}
function closeModal() {
document.getElementById('deleteModal').classList.add('hidden');
}
document.getElementById('deleteModal').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
</script>
<style>
@keyframes popIn {
from { transform: scale(0.85); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
</style>
@endpush

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Akses Ditolak - PlantCare</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=DM+Sans:wght@400;500&display=swap" rel="stylesheet"/>
<style>body { font-family: 'DM Sans', sans-serif; background: #f8f4ee; }</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
<div class="text-center max-w-sm">
<div class="w-24 h-24 rounded-full flex items-center justify-center mx-auto mb-6"
style="background:#fff0f0;">
<i class="fas fa-lock text-4xl" style="color:#dc2626;"></i>
</div>
<h1 class="text-3xl font-bold mb-2" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
Akses Ditolak
</h1>
<p class="text-sm mb-2" style="color:#5a7a67;">
Halaman ini hanya dapat diakses oleh <strong>Ahli Tanaman</strong>.
</p>
<p class="text-sm mb-8" style="color:#8fa89a;">
Akun kamu terdaftar sebagai <strong>Petani</strong>.
</p>
<a href="{{ url('/dashboard') }}"
class="inline-block px-6 py-3 rounded-full text-white font-semibold text-sm"
style="background:#1a3a2a;box-shadow:0 4px 14px rgba(26,58,42,.25);">
<i class="fas fa-arrow-left mr-2"></i>Kembali ke Dashboard
</a>
</div>
</body>
</html>

View File

@ -0,0 +1,255 @@
@extends('layouts.app')
@section('title', 'Riwayat Diagnosa')
@section('page-title', 'Riwayat Diagnosa')
@section('content')
<div id="tour-history-card" class="bg-white p-4 rounded-2xl border border-[#ede8df]" style="box-shadow:0 4px 16px rgba(26,58,42,.08);">
<div class="flex justify-between items-center mb-5">
<h3 class="text-lg font-bold" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Daftar Riwayat</h3>
<a href="{{ route('diagnosis.create') }}" id="tour-history-btn"
class="px-4 py-2 rounded-full text-white font-semibold text-sm flex items-center gap-2 transition"
style="background:#1a3a2a;box-shadow:0 4px 14px rgba(26,58,42,.25);"
onmouseover="this.style.background='#2d6a4f'" onmouseout="this.style.background='#1a3a2a'">
<i class="fas fa-plus"></i>
<span class="hidden sm:inline">Diagnosa Baru</span>
</a>
</div>
{{-- Mobile: card --}}
<div class="block sm:hidden space-y-3">
@forelse($diagnoses as $index => $diagnosis)
<div class="p-4 rounded-xl border border-[#ede8df]">
<div class="flex justify-between items-start mb-2">
<div>
<p class="font-semibold text-sm" style="color:#1a3a2a;">{{ $diagnosis->plant_name }}</p>
<p class="text-xs mt-0.5" style="color:#5a7a67;">{{ $diagnosis->disease_name }}</p>
</div>
<span class="px-2 py-1 rounded-full text-xs font-semibold flex-shrink-0" style="background:#d8f3dc;color:#2d6a4f;">
{{ $diagnosis->confidence }}%
</span>
</div>
<div class="flex justify-between items-center mt-3">
<p class="text-xs" style="color:#8fa89a;">{{ $diagnosis->created_at->format('d/m/Y H:i') }}</p>
<a href="{{ route('diagnosis.result', $diagnosis->id) }}"
class="text-xs font-semibold px-3 py-1.5 rounded-lg"
style="background:#f0fdf4;color:#2d6a4f;border:1px solid #b7ddc4;">
<i class="fas fa-eye mr-1"></i> Detail
</a>
</div>
</div>
@empty
<div class="text-center py-10" style="color:#8fa89a;">
<i class="fas fa-inbox text-4xl mb-3 block" style="color:#b7e4c7;"></i>
<p class="mb-2">Belum ada riwayat diagnosa</p>
<a href="{{ route('diagnosis.create') }}" style="color:#40916c;" class="text-sm font-medium">
Mulai diagnosa pertama Anda
</a>
</div>
@endforelse
</div>
{{-- Desktop: tabel --}}
<div id="tour-history-table" class="hidden sm:block overflow-x-auto">
<table class="w-full">
<thead>
<tr style="background:#f8f4ee;border-bottom:2px solid #ede8df;">
<th class="text-left py-3 px-4" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">No</th>
<th class="text-left py-3 px-4" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Tanggal</th>
<th class="text-left py-3 px-4" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Nama Tanaman</th>
<th class="text-left py-3 px-4" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Penyakit</th>
<th class="text-left py-3 px-4" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Akurasi</th>
<th class="text-left py-3 px-4" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($diagnoses as $index => $diagnosis)
<tr style="border-bottom:1px solid rgba(237,232,223,.6);" class="transition" onmouseover="this.style.background='rgba(216,243,220,.25)'" onmouseout="this.style.background=''">
<td class="py-3 px-4 text-sm" style="color:#8fa89a;">{{ $diagnoses->firstItem() + $index }}</td>
<td class="py-3 px-4 text-sm" style="color:#5a7a67;">{{ $diagnosis->created_at->format('d/m/Y H:i') }}</td>
<td class="py-3 px-4 text-sm font-medium" style="color:#1a3a2a;">{{ $diagnosis->plant_name }}</td>
<td class="py-3 px-4 text-sm" style="color:#1a2e22;">{{ $diagnosis->disease_name }}</td>
<td class="py-3 px-4">
<span class="px-3 py-1 rounded-full text-sm font-semibold" style="background:#d8f3dc;color:#2d6a4f;">
{{ $diagnosis->confidence }}%
</span>
</td>
<td class="py-3 px-4">
<a href="{{ route('diagnosis.result', $diagnosis->id) }}"
class="text-sm font-medium flex items-center gap-1 transition"
style="color:#40916c;"
onmouseover="this.style.color='#1a3a2a'" onmouseout="this.style.color='#40916c'">
<i class="fas fa-eye"></i> Detail
</a>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center py-10" style="color:#8fa89a;">
<i class="fas fa-inbox text-4xl mb-3 block" style="color:#b7e4c7;"></i>
<p class="mb-2">Belum ada riwayat diagnosa</p>
<a href="{{ route('diagnosis.create') }}" style="color:#40916c;" class="hover:underline text-sm font-medium">
Mulai diagnosa pertama Anda
</a>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($diagnoses->hasPages())
<div class="mt-6">
{{ $diagnoses->links() }}
</div>
@endif
</div>
<script>
/* ============================================================
GUIDED TOUR Halaman Riwayat
============================================================ */
const histTourSteps = [
{
title: 'Halaman Riwayat 📋',
desc: 'Di sini tersimpan semua diagnosa yang pernah kamu lakukan. Yuk kita lihat apa saja yang ada!',
target: null,
},
{
title: 'Tabel Riwayat',
desc: 'Tabel ini menampilkan <b>tanggal</b>, <b>nama tanaman</b>, <b>penyakit</b> yang terdeteksi, dan <b>tingkat akurasi</b>. Klik <b>Detail</b> untuk melihat hasil lengkapnya.',
targetSel: '#tour-history-table',
pos: 'bottom',
},
{
title: 'Diagnosa Baru',
desc: 'Mau diagnosa tanaman lagi? Klik tombol ini dan kamu langsung diarahkan ke halaman diagnosa.',
targetSel: '#tour-history-btn',
pos: 'bottom',
},
];
let _hTourStep = 0;
function _hTourEl(id) { return document.getElementById(id); }
function _hTourFindTarget(step) {
if (!step.targetSel) return null;
return document.querySelector(step.targetSel);
}
function _hTourStart() {
_hTourStep = 0;
if (!_hTourEl('_htour_hl')) { const d = document.createElement('div'); d.id = '_htour_hl'; document.body.appendChild(d); }
if (!_hTourEl('_htour_tip')) { const d = document.createElement('div'); d.id = '_htour_tip'; document.body.appendChild(d); }
if (!_hTourEl('_htour_dim')) { const d = document.createElement('div'); d.id = '_htour_dim'; document.body.appendChild(d); }
_hTourRender();
}
function _hTourRender() {
const step = histTourSteps[_hTourStep];
const total = histTourSteps.length;
const hl = _hTourEl('_htour_hl');
const tip = _hTourEl('_htour_tip');
const dim = _hTourEl('_htour_dim');
const target = _hTourFindTarget(step);
dim.style.cssText = 'position:fixed;inset:0;z-index:99990;background:rgba(0,0,0,0.52);pointer-events:auto;';
if (target) {
const r = target.getBoundingClientRect();
const pad = 8;
hl.style.cssText = `
position:fixed;z-index:99992;pointer-events:none;
top:${r.top-pad}px;left:${r.left-pad}px;
width:${r.width+pad*2}px;height:${r.height+pad*2}px;
border:2.5px solid #2d6a4f;border-radius:12px;
box-shadow:0 0 0 9999px rgba(0,0,0,0.52);
transition:all .35s ease;`;
dim.style.background = 'transparent';
} else {
hl.style.cssText = 'display:none;';
}
const dots = Array.from({length:total}, (_, i) =>
`<div style="width:${i===_hTourStep?16:6}px;height:6px;border-radius:3px;
background:${i===_hTourStep?'#2d6a4f':'#d1d5db'};
transition:all .3s;display:inline-block;margin-right:4px;"></div>`
).join('');
let tipPos = 'top:50%;left:50%;transform:translate(-50%,-50%);';
if (target) {
const r = target.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
if (step.pos === 'bottom' && r.bottom + 230 < vh)
tipPos = `top:${r.bottom+16}px;left:${Math.max(16,Math.min(r.left,vw-310))}px;transform:none;`;
else if (step.pos === 'top' && r.top - 230 > 0)
tipPos = `top:${r.top-230}px;left:${Math.max(16,Math.min(r.left,vw-310))}px;transform:none;`;
else if (step.pos === 'right' && r.right + 310 < vw)
tipPos = `top:${Math.min(r.top,vh-260)}px;left:${r.right+16}px;transform:none;`;
else
tipPos = 'top:50%;left:50%;transform:translate(-50%,-50%);';
}
tip.style.cssText = `
position:fixed;${tipPos}
z-index:99999;background:#fff;border-radius:16px;
padding:22px 24px;width:290px;
box-shadow:0 12px 40px rgba(0,0,0,0.18);
font-family:inherit;pointer-events:auto;`;
tip.innerHTML = `
<div style="font-size:11px;color:#2d6a4f;font-weight:700;text-transform:uppercase;
letter-spacing:.8px;margin-bottom:6px;">
Langkah ${_hTourStep+1} dari ${total}
</div>
<div style="font-size:15px;font-weight:700;color:#1a3a2a;margin-bottom:8px;
font-family:'Playfair Display',serif;">
${step.title}
</div>
<div style="font-size:13px;color:#5a7a67;line-height:1.65;margin-bottom:18px;">
${step.desc}
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<div>${dots}</div>
<div style="display:flex;gap:10px;align-items:center;">
<button onclick="_hTourEnd()"
style="background:none;border:none;font-size:12px;color:#9ca3af;cursor:pointer;padding:4px;">
Lewati
</button>
<button onclick="_hTourNext()"
style="background:#2d6a4f;color:#fff;border:none;
padding:9px 18px;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;">
${_hTourStep === total-1 ? 'Paham! ✓' : 'Lanjut →'}
</button>
</div>
</div>`;
}
function _hTourNext() {
_hTourStep++;
if (_hTourStep >= histTourSteps.length) { _hTourEnd(); return; }
_hTourRender();
}
function _hTourEnd() {
['_htour_hl','_htour_tip','_htour_dim'].forEach(id => {
const el = _hTourEl(id);
if (el) el.remove();
});
localStorage.setItem('sipakartebu_history_tour_done', '1');
}
window.addEventListener('load', function () {
if (!localStorage.getItem('sipakartebu_history_tour_done')) {
setTimeout(_hTourStart, 800);
}
});
function ulangiTourRiwayat() {
localStorage.removeItem('sipakartebu_history_tour_done');
_hTourStart();
}
</script>
@endsection

View File

@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'SiPakarTebu')</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=DM+Sans:wght@400;500&display=swap" rel="stylesheet">
<style>
body { font-family: 'DM Sans', sans-serif; background: #f8f4ee; }
.sidebar-active {
background: linear-gradient(135deg, #40916c, #2d6a4f) !important;
box-shadow: 0 4px 12px rgba(64,145,108,.3);
}
.sidebar-item:hover {
background: rgba(255,255,255,.1);
}
</style>
</head>
<body class="bg-[#f8f4ee]">
<div class="flex min-h-screen">
<!-- OVERLAY -->
<div id="overlay"
class="fixed inset-0 bg-black/40 z-40 hidden md:hidden"
onclick="toggleSidebar()"></div>
<!-- SIDEBAR -->
<div id="sidebar"
class="fixed md:sticky md:top-0 md:h-screen z-50 w-64 min-h-screen overflow-y-auto text-white transform -translate-x-full md:translate-x-0 transition duration-300 flex-shrink-0"
style="background: linear-gradient(180deg, #1a3a2a 0%, #1f4a35 60%, #1a3a2a 100%);">
<div class="p-6">
<div class="flex items-center space-x-3 mb-8 pb-6 border-b border-white/10">
<div class="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0"
style="background:linear-gradient(135deg,#40916c,#74c69d);">
<svg width="26" height="26" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Daun tebu -->
<ellipse cx="11" cy="15" rx="5" ry="11" fill="rgba(255,255,255,0.25)" stroke="white" stroke-width="1.3" transform="rotate(-15 11 15)"/>
<line x1="11" y1="6" x2="10" y2="24" stroke="white" stroke-width="1" stroke-linecap="round" opacity="0.8" transform="rotate(-15 11 15)"/>
<line x1="10" y1="13" x2="7" y2="17" stroke="white" stroke-width="0.8" stroke-linecap="round" opacity="0.6"/>
<line x1="10" y1="17" x2="7.5" y2="21" stroke="white" stroke-width="0.8" stroke-linecap="round" opacity="0.6"/>
<!-- Stetoskop -->
<path d="M19 7 C19 7 24 7 24 12 C24 17 20 18 20 22" stroke="white" stroke-width="1.5" stroke-linecap="round" fill="none"/>
<circle cx="20" cy="24.5" r="3" stroke="white" stroke-width="1.3" fill="rgba(255,255,255,0.2)"/>
<line x1="18" y1="7" x2="21" y2="7" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<h1 class="text-xl font-bold" style="font-family:'Playfair Display',serif;">SiPakarTebu</h1>
</div>
<nav class="space-y-1">
<a href="{{ route('dashboard') }}" class="sidebar-item flex items-center space-x-3 p-3 rounded-xl {{ request()->routeIs('dashboard') ? 'sidebar-active' : '' }}">
<i class="fas fa-home"></i><span>Dashboard</span>
</a>
<a href="{{ route('diagnosis.create') }}" class="sidebar-item flex items-center space-x-3 p-3 rounded-xl {{ request()->routeIs('diagnosis.*') ? 'sidebar-active' : '' }}">
<i class="fas fa-stethoscope"></i><span>Diagnosa</span>
</a>
<a href="{{ route('history') }}" class="sidebar-item flex items-center space-x-3 p-3 rounded-xl {{ request()->routeIs('history') ? 'sidebar-active' : '' }}">
<i class="fas fa-history"></i><span>Riwayat</span>
</a>
<a href="{{ route('diseases.index') }}" class="sidebar-item flex items-center space-x-3 p-3 rounded-xl {{ request()->routeIs('diseases.*') ? 'sidebar-active' : '' }}">
<i class="fas fa-book-medical"></i><span>Kamus</span>
</a>
<a href="{{ route('profile') }}" class="sidebar-item flex items-center space-x-3 p-3 rounded-xl {{ request()->routeIs('profile') ? 'sidebar-active' : '' }}">
<i class="fas fa-user"></i><span>Profil</span>
</a>
@if(auth()->user()->role === 'admin')
<a href="{{ route('users.index') }}" class="sidebar-item flex items-center space-x-3 p-3 rounded-xl {{ request()->routeIs('users.*') ? 'sidebar-active' : '' }}">
<i class="fas fa-users"></i><span>Users</span>
</a>
@endif
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="sidebar-item flex items-center space-x-3 p-3 rounded-xl w-full text-left">
<i class="fas fa-sign-out-alt"></i><span>Logout</span>
</button>
</form>
</nav>
</div>
</div>
<!-- MAIN -->
<div class="flex-1 flex flex-col">
<!-- HEADER -->
<div class="bg-white p-4 md:p-6 flex justify-between items-center border-b">
<div class="flex items-center gap-3">
<button onclick="toggleSidebar()" class="md:hidden text-xl">
<i class="fas fa-bars"></i>
</button>
<h2 class="text-lg md:text-2xl font-bold"
style="font-family:'Playfair Display',serif;color:#1a3a2a;">
@yield('page-title')
</h2>
</div>
<div class="flex items-center gap-3">
<!-- Tombol Notifikasi -->
<a href="{{ route('notifications') }}" class="relative flex items-center justify-center w-9 h-9 rounded-full transition"
style="background:#f0fdf4;color:#2d6a4f;"
onmouseover="this.style.background='#d8f3dc'" onmouseout="this.style.background='#f0fdf4'">
<i class="fas fa-bell text-sm"></i>
@php
$unreadCount = auth()->user()->notifications()->where('is_read', false)->count();
@endphp
@if($unreadCount > 0)
<span class="absolute -top-1 -right-1 w-4 h-4 rounded-full text-white flex items-center justify-center"
style="background:#dc2626;font-size:.6rem;font-weight:700;">
{{ $unreadCount > 9 ? '9+' : $unreadCount }}
</span>
@endif
</a>
<!-- Tombol Profil -->
<a href="{{ route('profile') }}" class="flex items-center gap-2">
@if(auth()->user()->photo)
<img src="{{ url('storage/' . auth()->user()->photo) }}"
class="w-9 h-9 rounded-full object-cover border-2"
style="border-color:#b7ddc4;">
@else
<div class="w-9 h-9 rounded-full text-white flex items-center justify-center text-sm font-bold"
style="background:linear-gradient(135deg,#40916c,#2d6a4f);">
{{ strtoupper(substr(auth()->user()->name, 0, 1)) }}
</div>
@endif
</a>
</div>
</div>
<!-- CONTENT -->
<div class="p-4 md:p-6">
@yield('content')
</div>
</div>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('-translate-x-full');
document.getElementById('overlay').classList.toggle('hidden');
}
</script>
@stack('scripts')
</body>
</html>

View File

@ -0,0 +1,234 @@
{{-- resources/views/layouts/auth.blade.php --}}
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>@yield('title', 'PlantCare') - Sistem Diagnosis Penyakit Tanaman</title>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet"/>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--green-dark: #1e5c3a;
--green-mid: #2d7a50;
--green-light: #3a9b68;
--green-pale: #e8f5ee;
--cream: #f0ede6;
--text-dark: #1a2e22;
--text-muted: #5a7a66;
--border: #c8ddd0;
--input-bg: #eef6f1;
--white: #ffffff;
--radius: 14px;
--shadow: 0 8px 32px rgba(30,92,58,0.12);
}
body {
min-height: 100vh;
background: var(--cream);
display: flex; align-items: center; justify-content: center;
font-family: 'DM Sans', sans-serif;
padding: 20px;
background-image:
radial-gradient(ellipse at 20% 80%, rgba(61,155,104,0.12) 0%, transparent 55%),
radial-gradient(ellipse at 80% 20%, rgba(30,92,58,0.08) 0%, transparent 55%);
}
.card {
width: 100%; max-width: 480px;
background: var(--white);
border-radius: 24px;
overflow: hidden;
box-shadow: var(--shadow);
animation: slideUp 0.5s cubic-bezier(0.22,1,0.36,1) both;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(28px); }
to { opacity: 1; transform: translateY(0); }
}
/* Header */
.auth-header {
background: linear-gradient(145deg, var(--green-dark) 0%, var(--green-mid) 100%);
padding: 36px 32px 40px;
position: relative; overflow: hidden; text-align: center;
}
.auth-header::before {
content: ''; position: absolute;
width: 200px; height: 200px;
background: rgba(255,255,255,0.06);
border-radius: 50%; top: -60px; left: -40px;
}
.auth-header::after {
content: ''; position: absolute;
width: 150px; height: 150px;
background: rgba(255,255,255,0.05);
border-radius: 50%; bottom: -50px; right: -30px;
}
.logo-icon {
width: 60px; height: 60px;
background: rgba(255,255,255,0.18);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
margin: 0 auto 14px;
font-size: 26px;
position: relative; z-index: 1;
}
.auth-header h1 {
font-family: 'Playfair Display', serif;
font-size: 26px; color: #fff;
position: relative; z-index: 1;
}
.auth-header p {
font-size: 13.5px; color: rgba(255,255,255,0.72);
margin-top: 4px;
position: relative; z-index: 1;
}
/* Body */
.auth-body { padding: 36px 32px 40px; }
/* Alert */
.alert {
padding: 12px 16px; border-radius: var(--radius);
font-size: 13.5px; margin-bottom: 20px;
}
.alert-success { background: var(--green-pale); color: var(--green-dark); border: 1px solid var(--border); }
.alert-error { background: #fef2f2; color: #b91c1c; border: 1px solid #fecaca; }
/* Form */
.form-group { margin-bottom: 18px; }
label {
display: block; font-size: 12px; font-weight: 600;
letter-spacing: 0.8px; text-transform: uppercase;
color: var(--text-muted); margin-bottom: 8px;
}
input[type="email"],
input[type="password"],
input[type="text"] {
width: 100%; padding: 14px 16px;
background: var(--input-bg);
border: 1.5px solid transparent;
border-radius: var(--radius);
font-size: 15px; font-family: 'DM Sans', sans-serif;
color: var(--text-dark); outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
input:focus {
border-color: var(--green-mid);
box-shadow: 0 0 0 3px rgba(45,122,80,0.12);
}
input.is-invalid { border-color: #e74c3c; }
input::placeholder { color: #a0b8a8; }
.invalid-feedback {
font-size: 12.5px; color: #e74c3c;
margin-top: 5px; display: block;
}
/* Button */
.btn {
width: 100%; padding: 15px;
border: none; border-radius: 50px;
font-size: 16px; font-weight: 600;
font-family: 'DM Sans', sans-serif;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
}
.btn-primary {
background: linear-gradient(135deg, var(--green-dark) 0%, var(--green-mid) 100%);
color: #fff;
box-shadow: 0 4px 16px rgba(30,92,58,0.25);
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(30,92,58,0.3); }
.btn-primary:active { transform: translateY(0); }
.btn-ghost {
background: transparent; color: var(--text-muted);
margin-top: 10px; font-size: 14px;
}
.btn-ghost:hover { color: var(--green-dark); }
/* Page title */
.page-title {
font-family: 'Playfair Display', serif;
font-size: 22px; color: var(--text-dark); margin-bottom: 6px;
}
.page-sub {
font-size: 14px; color: var(--text-muted);
margin-bottom: 28px; line-height: 1.5;
}
/* Steps */
.steps { display: flex; align-items: center; margin-bottom: 28px; }
.step { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--text-muted); }
.step.active { color: var(--green-dark); }
.step.done { color: var(--green-light); }
.step-dot {
width: 24px; height: 24px; border-radius: 50%;
background: var(--border);
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700; color: var(--text-muted);
}
.step.active .step-dot { background: var(--green-dark); color: #fff; }
.step.done .step-dot { background: var(--green-light); color: #fff; }
.step-line { flex: 1; height: 2px; background: var(--border); margin: 0 6px; }
.step-line.done { background: var(--green-light); }
/* Input-wrap */
.input-wrap { position: relative; }
.toggle-pw {
position: absolute; right: 14px; top: 50%;
transform: translateY(-50%);
background: none; border: none; cursor: pointer;
color: var(--text-muted); font-size: 16px; padding: 0;
}
/* OTP */
.otp-group { display: flex; gap: 10px; }
.otp-group input {
text-align: center; font-size: 20px;
font-weight: 700; padding: 14px 0; letter-spacing: 2px;
}
/* Info box */
.info-box {
background: var(--green-pale);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px; font-size: 13.5px;
color: var(--green-dark); margin-bottom: 22px;
display: flex; gap: 10px; align-items: flex-start; line-height: 1.5;
}
/* Resend */
.resend { text-align: center; font-size: 13.5px; color: var(--text-muted); margin-top: 16px; }
.resend button {
background: none; border: none; cursor: pointer;
color: var(--green-dark); font-weight: 600;
font-family: 'DM Sans', sans-serif; font-size: 13.5px;
padding: 0; text-decoration: underline;
}
.resend button:disabled { color: var(--text-muted); cursor: not-allowed; text-decoration: none; }
/* Strength */
.strength-bar { height: 4px; border-radius: 4px; background: var(--border); margin-top: 8px; overflow: hidden; }
.strength-fill { height: 100%; border-radius: 4px; width: 0%; transition: width 0.35s, background 0.35s; }
.strength-label { font-size: 12px; margin-top: 4px; color: var(--text-muted); min-height: 16px; }
/* Back link */
.back-link { display: block; text-align: center; margin-top: 20px; font-size: 13.5px; color: var(--text-muted); }
.back-link a { color: var(--green-dark); font-weight: 600; text-decoration: none; }
.back-link a:hover { text-decoration: underline; }
/* Success */
.success-icon {
width: 68px; height: 68px; background: var(--green-pale);
border-radius: 50%; display: flex; align-items: center;
justify-content: center; font-size: 30px;
margin: 0 auto 20px;
animation: pop 0.5s cubic-bezier(0.22,1,0.36,1) both;
}
@keyframes pop {
from { transform: scale(0.4); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
</style>
@stack('styles')
</head>
<body>
<div class="card">
<div class="auth-header">
<div class="logo-icon">🌿</div>
<h1>PlantCare</h1>
<p>Sistem Diagnosis Penyakit Tanaman</p>
</div>
<div class="auth-body">
@yield('content')
</div>
</div>
@stack('scripts')
</body>
</html>

View File

@ -0,0 +1,93 @@
@extends('layouts.app')
@section('title', 'Notifikasi')
@section('page-title', 'Notifikasi')
@section('content')
<!-- Tombol kembali -->
<div class="mb-4">
<a href="{{ route('dashboard') }}"
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-semibold transition"
style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;"
onmouseover="this.style.background='#d8f3dc'" onmouseout="this.style.background='#f0fdf4'">
<i class="fas fa-arrow-left text-xs"></i> Kembali ke Dashboard
</a>
</div>
<div class="bg-white rounded-2xl border border-[#ede8df]" style="box-shadow:0 4px 16px rgba(26,58,42,.08);">
<!-- Header -->
<div class="flex items-center justify-between p-6" style="border-bottom:1px solid #ede8df;">
<h3 class="text-lg font-bold" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
Semua Notifikasi
@if($notifications->where('is_read', false)->count() > 0)
<span class="ml-2 px-2 py-0.5 rounded-full text-xs font-bold text-white" style="background:#2d6a4f;">
{{ $notifications->where('is_read', false)->count() }} baru
</span>
@endif
</h3>
@if($notifications->count() > 0)
<form method="POST" action="{{ route('notifications.destroyAll') }}">
@csrf
@method('DELETE')
<button type="submit" class="text-sm px-4 py-2 rounded-xl border transition"
style="color:#dc2626;border-color:#fca5a5;"
onclick="return confirm('Hapus semua notifikasi?')">
<i class="fas fa-trash-alt mr-1"></i> Hapus Semua
</button>
</form>
@endif
</div>
<!-- List -->
@if($notifications->count() === 0)
<div class="text-center py-16">
<i class="fas fa-bell-slash text-4xl mb-4" style="color:#c8d8cc;"></i>
<p style="color:#8fa89a;">Belum ada notifikasi</p>
</div>
@else
@foreach($notifications as $notif)
<div class="flex items-start gap-4 p-5 transition hover:bg-[#f8f4ee]"
style="border-bottom:1px solid #f0ece5;{{ !$notif->is_read ? 'background:#f0fdf4;' : '' }}">
<!-- Icon -->
<div class="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 mt-1"
style="background:{{
$notif->type === 'diagnosis' ? '#d8f3dc' :
($notif->type === 'reminder' ? '#e0f0ff' : '#f0e8ff')
}}">
<i class="fas {{
$notif->type === 'diagnosis' ? 'fa-stethoscope' :
($notif->type === 'reminder' ? 'fa-bell' : 'fa-info-circle')
}} text-sm" style="color:{{
$notif->type === 'diagnosis' ? '#2d6a4f' :
($notif->type === 'reminder' ? '#2563eb' : '#7c3aed')
}}"></i>
</div>
<!-- Content -->
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<p class="font-semibold text-sm" style="color:#1a3a2a;">{{ $notif->title }}</p>
@if(!$notif->is_read)
<span class="w-2 h-2 rounded-full flex-shrink-0" style="background:#2d6a4f;"></span>
@endif
</div>
<p class="text-sm" style="color:#5a7a67;">{{ $notif->message }}</p>
<p class="text-xs mt-1" style="color:#a0b4a8;">{{ $notif->created_at->diffForHumans() }}</p>
</div>
<!-- Delete -->
<form method="POST" action="{{ route('notifications.destroy', $notif->id) }}">
@csrf
@method('DELETE')
<button type="submit" class="text-sm mt-1 hover:text-red-500 transition" style="color:#c8d8cc;">
<i class="fas fa-times"></i>
</button>
</form>
</div>
@endforeach
@endif
</div>
@endsection

View File

@ -0,0 +1,285 @@
@extends('layouts.app')
@section('title', 'Profil')
@section('page-title', 'Profil Saya')
@section('content')
<div class="max-w-2xl mx-auto space-y-5">
@if(session('status'))
<div class="px-4 py-3 rounded-xl text-sm font-medium flex items-center gap-2"
style="background:#f0fdf4;color:#16a34a;border:1px solid #86efac;">
<i class="fas fa-check-circle"></i> {{ session('status') }}
</div>
@endif
@if($errors->any())
<div class="px-4 py-3 rounded-xl text-sm font-medium"
style="background:#fff0f0;color:#dc2626;border:1px solid #fca5a5;">
<i class="fas fa-exclamation-circle mr-1"></i> {{ $errors->first() }}
</div>
@endif
{{-- CARD 1: PROFIL --}}
<div class="bg-white rounded-2xl border border-[#ede8df]" style="box-shadow:0 4px 16px rgba(26,58,42,.08);">
<div class="p-6" style="border-bottom:1px solid #ede8df;">
<h3 class="font-bold text-base" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Profil</h3>
</div>
<div class="p-6">
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
@csrf
{{-- Avatar --}}
<div class="flex items-center gap-5 mb-6">
<div class="relative">
@if($user->photo)
<img src="{{ url('storage/' . $user->photo) }}"
class="w-20 h-20 rounded-full object-cover"
style="border:3px solid #b7ddc4;">
@else
<div class="w-20 h-20 rounded-full text-white flex items-center justify-center text-2xl font-bold"
style="background:linear-gradient(135deg,#40916c,#2d6a4f);border:3px solid #b7ddc4;">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
@endif
<label class="absolute -bottom-1 -right-1 w-7 h-7 rounded-full flex items-center justify-center cursor-pointer"
style="background:#2d6a4f;border:2px solid white;">
<i class="fas fa-camera text-white" style="font-size:.6rem;"></i>
<input type="file" name="photo" class="hidden" accept="image/*" onchange="this.form.submit()">
</label>
</div>
<div>
<p class="font-bold text-base" style="color:#1a3a2a;">{{ $user->name }}</p>
<p class="text-sm" style="color:#5a7a67;">{{ $user->email }}</p>
<span class="inline-flex items-center gap-1 mt-1 px-2 py-0.5 rounded-full text-xs font-semibold"
style="background:{{ $user->role === 'admin' ? '#d8f3dc' : '#e0f0ff' }};color:{{ $user->role === 'admin' ? '#2d6a4f' : '#2563eb' }};">
<i class="fas {{ $user->role === 'admin' ? 'fa-user-tie' : 'fa-seedling' }}"></i>
{{ $user->role === 'admin' ? 'Ahli Tanaman' : 'Petani' }}
</span>
</div>
</div>
{{-- Nama --}}
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider mb-1.5" style="color:#5a7a67;">Nama</label>
<input type="text" name="name" value="{{ old('name', $user->name) }}"
class="w-full px-4 py-3 rounded-xl border text-sm"
style="border-color:#ede8df;color:#1a3a2a;outline:none;">
</div>
{{-- Email --}}
<div class="mb-5">
<label class="block text-xs font-semibold uppercase tracking-wider mb-1.5" style="color:#5a7a67;">Email</label>
<input type="email" name="email" value="{{ old('email', $user->email) }}"
class="w-full px-4 py-3 rounded-xl border text-sm"
style="border-color:#ede8df;color:#1a3a2a;outline:none;">
</div>
<button type="submit"
class="px-6 py-2.5 rounded-xl text-sm font-semibold text-white transition"
style="background:#2d6a4f;"
onmouseover="this.style.background='#1a3a2a'" onmouseout="this.style.background='#2d6a4f'">
Simpan Perubahan
</button>
</form>
</div>
</div>
{{-- CARD 2: PENGATURAN AKUN --}}
<div class="bg-white rounded-2xl border border-[#ede8df]" style="box-shadow:0 4px 16px rgba(26,58,42,.08);">
<div class="p-6" style="border-bottom:1px solid #ede8df;">
<h3 class="font-bold text-base" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Pengaturan Akun</h3>
<p class="text-xs mt-0.5" style="color:#8fa89a;">Keamanan akun dan akses</p>
</div>
<div class="divide-y divide-[#ede8df]">
{{-- Ubah Password --}}
<div class="p-5 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl flex items-center justify-center" style="background:#f0fdf4;">
<i class="fas fa-lock text-sm" style="color:#2d6a4f;"></i>
</div>
<div>
<p class="text-sm font-semibold" style="color:#1a3a2a;">Ubah Kata Sandi</p>
<p class="text-xs" style="color:#8fa89a;">Perbarui password akun kamu</p>
</div>
</div>
<a href="{{ route('password.email') }}"
class="px-4 py-2 rounded-xl text-xs font-semibold transition"
style="background:#f0fdf4;color:#2d6a4f;border:1px solid #b7ddc4;"
onmouseover="this.style.background='#d8f3dc'" onmouseout="this.style.background='#f0fdf4'">
Ubah
</a>
</div>
{{-- Manajemen Perangkat --}}
<div class="p-5 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl flex items-center justify-center" style="background:#e0f0ff;">
<i class="fas fa-desktop text-sm" style="color:#2563eb;"></i>
</div>
<div>
<p class="text-sm font-semibold" style="color:#1a3a2a;">Manajemen Perangkat</p>
<p class="text-xs" style="color:#8fa89a;">Lihat perangkat yang mengakses akun</p>
</div>
</div>
<button onclick="openDeviceModal()"
class="px-4 py-2 rounded-xl text-xs font-semibold transition"
style="background:#e0f0ff;color:#2563eb;border:1px solid #93c5fd;"
onmouseover="this.style.background='#bfdbfe'" onmouseout="this.style.background='#e0f0ff'">
Lihat
</button>
</div>
{{-- Hapus Akun --}}
<div class="p-5 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl flex items-center justify-center" style="background:#fff0f0;">
<i class="fas fa-user-times text-sm" style="color:#dc2626;"></i>
</div>
<div>
<p class="text-sm font-semibold" style="color:#1a3a2a;">Hapus Akun</p>
<p class="text-xs" style="color:#8fa89a;">Hapus akun dan semua data kamu secara permanen</p>
</div>
</div>
<button onclick="openDeleteModal()"
class="px-4 py-2 rounded-xl text-xs font-semibold transition"
style="background:#fff0f0;color:#dc2626;border:1px solid #fca5a5;"
onmouseover="this.style.background='#fee2e2'" onmouseout="this.style.background='#fff0f0'">
Hapus
</button>
</div>
</div>
</div>
{{-- CARD 3: LAIN-LAIN --}}
<div class="bg-white rounded-2xl border border-[#ede8df]" style="box-shadow:0 4px 16px rgba(26,58,42,.08);">
<div class="p-6" style="border-bottom:1px solid #ede8df;">
<h3 class="font-bold text-base" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Lain-lain</h3>
</div>
<div class="divide-y divide-[#ede8df]">
{{-- Logout --}}
<div class="p-5 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl flex items-center justify-center" style="background:#f8f4ee;">
<i class="fas fa-sign-out-alt text-sm" style="color:#5a7a67;"></i>
</div>
<div>
<p class="text-sm font-semibold" style="color:#1a3a2a;">Logout</p>
<p class="text-xs" style="color:#8fa89a;">Keluar dari akun ini</p>
</div>
</div>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit"
class="px-4 py-2 rounded-xl text-xs font-semibold transition"
style="background:#f8f4ee;color:#5a7a67;border:1px solid #ede8df;"
onmouseover="this.style.background='#ede8df'" onmouseout="this.style.background='#f8f4ee'">
Logout
</button>
</form>
</div>
{{-- Bantuan --}}
<div class="p-5 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl flex items-center justify-center" style="background:#f0e8ff;">
<i class="fas fa-question-circle text-sm" style="color:#7c3aed;"></i>
</div>
<div>
<p class="text-sm font-semibold" style="color:#1a3a2a;">Bantuan & Dukungan</p>
<p class="text-xs" style="color:#8fa89a;">Hubungi kami jika ada masalah</p>
</div>
</div>
<a href="mailto:canedoc.app@gmail.com"
class="px-4 py-2 rounded-xl text-xs font-semibold transition"
style="background:#f0e8ff;color:#7c3aed;border:1px solid #c4b5fd;"
onmouseover="this.style.background='#e9d5ff'" onmouseout="this.style.background='#f0e8ff'">
Hubungi
</a>
</div>
</div>
</div>
</div>
{{-- Modal Manajemen Perangkat --}}
<div id="deviceModal" class="fixed inset-0 z-50 hidden flex items-center justify-center"
style="background:rgba(0,0,0,0.4);">
<div class="bg-white rounded-2xl p-6 w-full max-w-sm mx-4 shadow-2xl"
style="animation:popIn .25s cubic-bezier(0.22,1,0.36,1);">
<div class="flex items-center justify-between mb-5">
<h3 class="font-bold text-base" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Manajemen Perangkat</h3>
<button onclick="closeDeviceModal()" class="w-8 h-8 rounded-full flex items-center justify-center"
style="background:#f0fdf4;color:#2d6a4f;">
<i class="fas fa-times text-sm"></i>
</button>
</div>
<div class="space-y-3">
<div class="p-3 rounded-xl flex items-center gap-3" style="background:#f0fdf4;border:1px solid #b7ddc4;">
<i class="fas fa-desktop" style="color:#2d6a4f;"></i>
<div>
<p class="text-sm font-semibold" style="color:#1a3a2a;">Perangkat Ini</p>
<p class="text-xs" style="color:#5a7a67;">Sesi aktif sekarang</p>
</div>
<span class="ml-auto text-xs font-semibold px-2 py-1 rounded-full" style="background:#d8f3dc;color:#2d6a4f;">Aktif</span>
</div>
<p class="text-xs text-center" style="color:#8fa89a;">Hanya menampilkan sesi aktif saat ini</p>
</div>
<button onclick="closeDeviceModal()"
class="w-full mt-4 py-2.5 rounded-xl text-sm font-semibold"
style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;">
Tutup
</button>
</div>
</div>
{{-- Modal Hapus Akun --}}
<div id="deleteAccountModal" class="fixed inset-0 z-50 hidden flex items-center justify-center"
style="background:rgba(0,0,0,0.4);">
<div class="bg-white rounded-2xl p-6 w-full max-w-sm mx-4 shadow-2xl"
style="animation:popIn .25s cubic-bezier(0.22,1,0.36,1);">
<div class="text-center mb-4">
<div class="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-3" style="background:#fff0f0;">
<i class="fas fa-user-times text-2xl" style="color:#dc2626;"></i>
</div>
<h3 class="text-lg font-bold mb-1" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Hapus Akun?</h3>
<p class="text-sm" style="color:#5a7a67;">Semua data kamu akan dihapus permanen dan tidak bisa dikembalikan.</p>
</div>
<div class="flex gap-3 mt-4">
<button onclick="closeDeleteModal()"
class="flex-1 py-2.5 rounded-xl text-sm font-semibold"
style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;">
Batal
</button>
<form method="POST" action="{{ url('/profile/delete') }}" class="flex-1">
@csrf
@method('DELETE')
<button type="submit" class="w-full py-2.5 rounded-xl text-sm font-semibold text-white" style="background:#dc2626;">
Ya, Hapus
</button>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
function openDeviceModal() { document.getElementById('deviceModal').classList.remove('hidden'); }
function closeDeviceModal() { document.getElementById('deviceModal').classList.add('hidden'); }
function openDeleteModal() { document.getElementById('deleteAccountModal').classList.remove('hidden'); }
function closeDeleteModal() { document.getElementById('deleteAccountModal').classList.add('hidden'); }
document.getElementById('deviceModal').addEventListener('click', function(e) { if (e.target === this) closeDeviceModal(); });
document.getElementById('deleteAccountModal').addEventListener('click', function(e) { if (e.target === this) closeDeleteModal(); });
</script>
<style>
@keyframes popIn {
from { transform: scale(0.85); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
</style>
@endpush

View File

@ -0,0 +1,274 @@
@extends('layouts.app')
@section('title', 'Kelola User')
@section('page-title', 'Kelola User')
@section('content')
@if(session('status'))
<div class="mb-4 px-4 py-3 rounded-xl text-sm font-medium flex items-center gap-2"
style="background:#f0fdf4;color:#16a34a;border:1px solid #86efac;">
<i class="fas fa-check-circle"></i> {{ session('status') }}
</div>
@endif
@if($errors->any())
<div class="mb-4 px-4 py-3 rounded-xl text-sm font-medium"
style="background:#fff0f0;color:#dc2626;border:1px solid #fca5a5;">
<i class="fas fa-exclamation-circle mr-1"></i> {{ $errors->first() }}
</div>
@endif
{{-- Kartu Statistik --}}
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="bg-white p-5 rounded-2xl border border-[#ede8df]" style="box-shadow:0 2px 12px rgba(26,58,42,.06);">
<p class="text-xs font-semibold uppercase tracking-wider mb-1" style="color:#5a7a67;">Total User</p>
<h3 class="text-3xl font-bold" style="font-family:'Playfair Display',serif;color:#1a3a2a;">
{{ $totalAdmin + $totalUser }}
</h3>
</div>
<div class="bg-white p-5 rounded-2xl border border-[#ede8df]" style="box-shadow:0 2px 12px rgba(26,58,42,.06);">
<p class="text-xs font-semibold uppercase tracking-wider mb-1" style="color:#5a7a67;">Ahli Tanaman</p>
<h3 class="text-3xl font-bold" style="font-family:'Playfair Display',serif;color:#2d6a4f;">
{{ $totalAdmin }}
</h3>
</div>
<div class="bg-white p-5 rounded-2xl border border-[#ede8df]" style="box-shadow:0 2px 12px rgba(26,58,42,.06);">
<p class="text-xs font-semibold uppercase tracking-wider mb-1" style="color:#5a7a67;">Petani</p>
<h3 class="text-3xl font-bold" style="font-family:'Playfair Display',serif;color:#2563eb;">
{{ $totalUser }}
</h3>
</div>
</div>
{{-- Filter & Search --}}
<div class="bg-white rounded-2xl border border-[#ede8df] p-5 mb-5" style="box-shadow:0 2px 12px rgba(26,58,42,.06);">
<form method="GET" action="{{ route('users.index') }}" class="flex flex-wrap gap-3 items-end">
<div class="flex-1 min-w-[200px]">
<label class="block text-xs font-semibold uppercase tracking-wider mb-1" style="color:#5a7a67;">Cari</label>
<input type="text" name="search" value="{{ request('search') }}"
placeholder="Nama atau email..."
class="w-full px-4 py-2.5 rounded-xl border text-sm"
style="border-color:#ede8df;color:#1a3a2a;outline:none;" />
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wider mb-1" style="color:#5a7a67;">Role</label>
<select name="role" class="px-4 py-2.5 rounded-xl border text-sm" style="border-color:#ede8df;color:#1a3a2a;outline:none;">
<option value="">Semua</option>
<option value="admin" {{ request('role') === 'admin' ? 'selected' : '' }}>Ahli Tanaman</option>
<option value="user" {{ request('role') === 'user' ? 'selected' : '' }}>Petani</option>
</select>
</div>
<button type="submit" class="px-5 py-2.5 rounded-xl text-sm font-semibold text-white" style="background:#2d6a4f;">
<i class="fas fa-search mr-1"></i> Cari
</button>
@if(request('search') || request('role'))
<a href="{{ route('users.index') }}" class="px-5 py-2.5 rounded-xl text-sm font-semibold"
style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;">
<i class="fas fa-times mr-1"></i> Reset
</a>
@endif
</form>
</div>
{{-- Tabel User --}}
<div class="bg-white rounded-2xl border border-[#ede8df] overflow-hidden" style="box-shadow:0 2px 12px rgba(26,58,42,.06);">
{{-- Mobile: card --}}
<div class="mobile-cards" style="display:none;">
@forelse($users as $user)
<div style="padding:16px;border-bottom:1px solid #ede8df;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:10px;">
<div style="width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:white;font-weight:700;font-size:14px;flex-shrink:0;background:{{ $user->role === 'admin' ? '#2d6a4f' : '#2563eb' }};">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
<div style="flex:1;min-width:0;">
<p style="font-weight:600;font-size:14px;color:#1a3a2a;margin:0;">
{{ $user->name }}
@if($user->id === auth()->id())
<span style="font-size:12px;font-weight:400;color:#8fa89a;">(Kamu)</span>
@endif
</p>
<p style="font-size:12px;color:#5a7a67;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ $user->email }}</p>
</div>
<span style="padding:4px 10px;border-radius:999px;font-size:11px;font-weight:600;flex-shrink:0;background:{{ $user->role === 'admin' ? '#d8f3dc' : '#e0f0ff' }};color:{{ $user->role === 'admin' ? '#2d6a4f' : '#2563eb' }};">
{{ $user->role === 'admin' ? 'Ahli' : 'Petani' }}
</span>
</div>
@if($user->id !== auth()->id())
<div style="display:flex;gap:8px;">
<form method="POST" action="{{ route('users.updateRole', $user) }}" style="flex:1;">
@csrf
@method('PATCH')
<input type="hidden" name="role" value="{{ $user->role === 'admin' ? 'user' : 'admin' }}">
<button type="submit"
style="width:100%;padding:8px;border-radius:8px;font-size:12px;font-weight:600;background:#f0fdf4;color:#2d6a4f;border:1px solid #b7ddc4;cursor:pointer;"
onclick="return confirm('Ubah role {{ addslashes($user->name) }}?')">
<i class="fas fa-exchange-alt" style="margin-right:4px;"></i>
Jadikan {{ $user->role === 'admin' ? 'Petani' : 'Ahli' }}
</button>
</form>
<button onclick="confirmDeleteUser({{ $user->id }}, '{{ addslashes($user->name) }}')"
style="padding:8px 12px;border-radius:8px;font-size:12px;font-weight:600;background:#fff0f0;color:#dc2626;border:1px solid #fca5a5;cursor:pointer;">
<i class="fas fa-trash"></i>
</button>
</div>
@endif
</div>
@empty
<div style="text-align:center;padding:48px;color:#8fa89a;">
<i class="fas fa-users" style="font-size:32px;color:#c8d8cc;display:block;margin-bottom:12px;"></i>
Tidak ada user ditemukan
</div>
@endforelse
</div>
{{-- Desktop: tabel --}}
<div class="desktop-table overflow-x-auto">
<table class="w-full">
<thead>
<tr style="background:#f8f4ee;border-bottom:2px solid #ede8df;">
<th class="text-left py-3 px-5" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">User</th>
<th class="text-left py-3 px-5" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Email</th>
<th class="text-left py-3 px-5" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Role</th>
<th class="text-left py-3 px-5" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Bergabung</th>
<th class="text-left py-3 px-5" style="color:#5a7a67;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600;">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($users as $user)
<tr style="border-bottom:1px solid rgba(237,232,223,.6);"
class="transition" onmouseover="this.style.background='rgba(216,243,220,.2)'" onmouseout="this.style.background=''">
<td class="py-3 px-5">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full flex items-center justify-center text-white font-bold text-sm flex-shrink-0"
style="background:{{ $user->role === 'admin' ? '#2d6a4f' : '#2563eb' }};">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
<div>
<p class="text-sm font-semibold" style="color:#1a3a2a;">
{{ $user->name }}
@if($user->id === auth()->id())
<span class="text-xs font-normal ml-1" style="color:#8fa89a;">(Kamu)</span>
@endif
</p>
</div>
</div>
</td>
<td class="py-3 px-5 text-sm" style="color:#5a7a67;">{{ $user->email }}</td>
<td class="py-3 px-5">
<span class="px-3 py-1 rounded-full text-xs font-semibold"
style="background:{{ $user->role === 'admin' ? '#d8f3dc' : '#e0f0ff' }};
color:{{ $user->role === 'admin' ? '#2d6a4f' : '#2563eb' }};">
<i class="fas {{ $user->role === 'admin' ? 'fa-user-tie' : 'fa-seedling' }} mr-1"></i>
{{ $user->role === 'admin' ? 'Ahli Tanaman' : 'Petani' }}
</span>
</td>
<td class="py-3 px-5 text-sm" style="color:#8fa89a;">{{ $user->created_at->format('d M Y') }}</td>
<td class="py-3 px-5">
@if($user->id !== auth()->id())
<div class="flex items-center gap-2">
<form method="POST" action="{{ route('users.updateRole', $user) }}">
@csrf
@method('PATCH')
<input type="hidden" name="role" value="{{ $user->role === 'admin' ? 'user' : 'admin' }}">
<button type="submit"
class="px-3 py-1.5 rounded-lg text-xs font-semibold transition"
style="background:#f0fdf4;color:#2d6a4f;border:1px solid #b7ddc4;"
onmouseover="this.style.background='#d8f3dc'" onmouseout="this.style.background='#f0fdf4'"
onclick="return confirm('Ubah role {{ addslashes($user->name) }} menjadi {{ $user->role === 'admin' ? 'Petani' : 'Ahli Tanaman' }}?')">
<i class="fas fa-exchange-alt mr-1"></i>
Jadikan {{ $user->role === 'admin' ? 'Petani' : 'Ahli Tanaman' }}
</button>
</form>
<button onclick="confirmDeleteUser({{ $user->id }}, '{{ addslashes($user->name) }}')"
class="px-3 py-1.5 rounded-lg text-xs font-semibold transition"
style="background:#fff0f0;color:#dc2626;border:1px solid #fca5a5;"
onmouseover="this.style.background='#fee2e2'" onmouseout="this.style.background='#fff0f0'">
<i class="fas fa-trash"></i>
</button>
</div>
@else
<span class="text-xs" style="color:#c8d8cc;"></span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="5" class="text-center py-12" style="color:#8fa89a;">
<i class="fas fa-users text-3xl mb-3 block" style="color:#c8d8cc;"></i>
Tidak ada user ditemukan
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($users->hasPages())
<div class="px-5 py-4" style="border-top:1px solid #ede8df;">
{{ $users->links() }}
</div>
@endif
</div>
{{-- Modal Konfirmasi Hapus User --}}
<div id="deleteUserModal" class="fixed inset-0 z-50 hidden flex items-center justify-center"
style="background:rgba(0,0,0,0.4);">
<div class="bg-white rounded-2xl p-6 w-full max-w-sm mx-4 shadow-2xl"
style="animation: popIn .25s cubic-bezier(0.22,1,0.36,1);">
<div class="text-center mb-4">
<div class="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-3"
style="background:#fff0f0;">
<i class="fas fa-user-times text-2xl" style="color:#dc2626;"></i>
</div>
<h3 class="text-lg font-bold mb-1" style="font-family:'Playfair Display',serif;color:#1a3a2a;">Hapus User?</h3>
<p class="text-sm" style="color:#5a7a67;">
Kamu akan menghapus akun<br>
<strong id="deleteUserName" style="color:#1a3a2a;"></strong>
</p>
<p class="text-xs mt-2" style="color:#dc2626;">⚠️ Semua data diagnosa user ikut terhapus.</p>
</div>
<div class="flex gap-3 mt-4">
<button onclick="closeUserModal()"
class="flex-1 py-2.5 rounded-xl text-sm font-semibold"
style="background:#f0fdf4;color:#2d6a4f;border:1.5px solid #b7ddc4;">
Batal
</button>
<form id="deleteUserForm" method="POST" class="flex-1">
@csrf
@method('DELETE')
<button type="submit" class="w-full py-2.5 rounded-xl text-sm font-semibold text-white" style="background:#dc2626;">
Ya, Hapus
</button>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
function confirmDeleteUser(id, name) {
document.getElementById('deleteUserName').textContent = name;
document.getElementById('deleteUserForm').action = '{{ route("users.destroy", ":id") }}'.replace(':id', id);
document.getElementById('deleteUserModal').classList.remove('hidden');
}
function closeUserModal() {
document.getElementById('deleteUserModal').classList.add('hidden');
}
document.getElementById('deleteUserModal').addEventListener('click', function(e) {
if (e.target === this) closeUserModal();
});
</script>
<style>
@media (max-width: 640px) {
.mobile-cards { display: block !important; }
.desktop-table { display: none !important; }
}
@keyframes popIn {
from { transform: scale(0.85); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
</style>
@endpush

View File

@ -0,0 +1,870 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>SiPakarTebu Diagnosa Penyakit Tanaman Tebu</title>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--green-deep: #1a3a2a;
--green-mid: #2d6a4f;
--green-bright: #40916c;
--green-light: #74c69d;
--green-pale: #b7e4c7;
--cream: #f8f4ee;
--cream-dark: #ede8df;
--brown: #8b5e3c;
--text-dark: #1a2e22;
--text-muted: #5a7a67;
}
html { scroll-behavior: smooth; }
body { font-family: 'DM Sans', sans-serif; background: var(--cream); color: var(--text-dark); overflow-x: hidden; }
nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 1.25rem 3rem;
background: rgba(248, 244, 238, 0.92);
backdrop-filter: blur(14px);
border-bottom: 1px solid rgba(64,145,108,0.12);
gap: 1rem;
}
.nav-logo { display: flex; align-items: center; gap: .6rem; font-family: 'Playfair Display', serif; font-size: 1.4rem; font-weight: 700; color: var(--green-deep); text-decoration: none; flex-shrink: 0; }
.nav-links { display: flex; gap: .75rem; align-items: center; flex-shrink: 0; }
.btn-outline { padding: .5rem 1.4rem; border-radius: 50px; border: 1.5px solid var(--green-mid); color: var(--green-mid); background: transparent; font-family: 'DM Sans', sans-serif; font-size: .875rem; font-weight: 500; cursor: pointer; text-decoration: none; transition: all .25s ease; white-space: nowrap; }
.btn-outline:hover { background: var(--green-mid); color: white; }
.btn-solid { padding: .5rem 1.4rem; border-radius: 50px; background: var(--green-deep); color: white; font-family: 'DM Sans', sans-serif; font-size: .875rem; font-weight: 500; cursor: pointer; text-decoration: none; border: none; transition: all .25s ease; box-shadow: 0 4px 14px rgba(26,58,42,.25); white-space: nowrap; }
.btn-solid:hover { background: var(--green-mid); transform: translateY(-1px); box-shadow: 0 6px 20px rgba(26,58,42,.3); }
.hero { min-height: 100vh; display: grid; grid-template-columns: 1fr 1fr; align-items: center; padding: 7rem 3rem 4rem; position: relative; overflow: hidden; }
.hero::before { content: ''; position: absolute; top: -10%; right: -5%; width: 55%; height: 80%; background: radial-gradient(ellipse at 60% 40%, #b7e4c7 0%, #74c69d33 50%, transparent 75%); border-radius: 60% 40% 70% 30% / 50% 60% 40% 50%; animation: morph 12s ease-in-out infinite; z-index: 0; }
.hero::after { content: ''; position: absolute; bottom: -10%; left: 10%; width: 40%; height: 50%; background: radial-gradient(ellipse, #d8f3dc44 0%, transparent 70%); border-radius: 40% 60% 30% 70% / 60% 40% 70% 30%; animation: morph 16s ease-in-out infinite reverse; z-index: 0; }
@keyframes morph { 0%, 100% { border-radius: 60% 40% 70% 30% / 50% 60% 40% 50%; } 33% { border-radius: 40% 60% 30% 70% / 60% 40% 70% 30%; } 66% { border-radius: 70% 30% 50% 50% / 30% 70% 40% 60%; } }
.hero-content { position: relative; z-index: 1; padding-right: 2rem; }
.hero-badge { display: inline-flex; align-items: center; gap: .5rem; background: var(--green-pale); color: var(--green-deep); padding: .35rem 1rem; border-radius: 50px; font-size: .8rem; font-weight: 500; letter-spacing: .04em; margin-bottom: 1.5rem; animation: fadeUp .6s ease both; }
.hero-badge::before { content: '🌿'; font-size: .9rem; }
h1 { font-family: 'Playfair Display', serif; font-size: clamp(2.6rem, 4.5vw, 3.8rem); line-height: 1.1; color: var(--green-deep); margin-bottom: 1.25rem; animation: fadeUp .6s .1s ease both; }
h1 em { color: var(--green-bright); font-style: italic; }
.hero-desc { font-size: 1.05rem; line-height: 1.7; color: var(--text-muted); max-width: 460px; margin-bottom: 2.25rem; animation: fadeUp .6s .2s ease both; }
.hero-cta { display: flex; gap: 1rem; flex-wrap: wrap; animation: fadeUp .6s .3s ease both; }
.btn-hero { display: inline-flex; align-items: center; gap: .5rem; padding: .85rem 2rem; border-radius: 50px; font-family: 'DM Sans', sans-serif; font-size: .95rem; font-weight: 500; text-decoration: none; cursor: pointer; transition: all .3s ease; border: none; }
.btn-hero-primary { background: var(--green-deep); color: white; box-shadow: 0 6px 24px rgba(26,58,42,.3); }
.btn-hero-primary:hover { background: var(--green-mid); transform: translateY(-2px); }
.btn-hero-secondary { background: white; color: var(--green-deep); border: 1.5px solid var(--green-pale) !important; }
.btn-hero-secondary:hover { border-color: var(--green-bright) !important; background: var(--green-pale); }
.hero-stats { display: flex; gap: 2rem; margin-top: 3rem; animation: fadeUp .6s .4s ease both; }
.stat { display: flex; flex-direction: column; }
.stat-num { font-family: 'Playfair Display', serif; font-size: 1.8rem; color: var(--green-deep); font-weight: 700; }
.stat-label { font-size: .78rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: .06em; }
.hero-visual { position: relative; z-index: 1; display: flex; justify-content: center; align-items: center; animation: fadeUp .6s .2s ease both; }
.plant-card { background: white; border-radius: 28px; padding: 2.5rem 2rem; box-shadow: 0 20px 60px rgba(26,58,42,.15), 0 4px 16px rgba(26,58,42,.08); width: 340px; position: relative; }
.plant-card::before { content: ''; position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; background: linear-gradient(135deg, var(--green-light), var(--green-pale), var(--cream)); border-radius: 30px; z-index: -1; }
.plant-illustration { width: 100%; height: 200px; background: linear-gradient(160deg, #d8f3dc 0%, #b7e4c7 50%, #95d5b2 100%); border-radius: 18px; display: flex; align-items: center; justify-content: center; margin-bottom: 1.5rem; position: relative; overflow: hidden; }
.plant-illustration svg { width: 120px; height: 120px; }
.plant-illustration::before { content: ''; position: absolute; bottom: -20px; right: -20px; width: 100px; height: 100px; background: rgba(64,145,108,.2); border-radius: 50%; }
.plant-illustration::after { content: ''; position: absolute; top: -15px; left: -15px; width: 70px; height: 70px; background: rgba(116,198,157,.25); border-radius: 50%; }
.card-tag { display: inline-flex; align-items: center; gap: .4rem; background: #fff3cd; color: #8b5e3c; padding: .3rem .8rem; border-radius: 50px; font-size: .75rem; font-weight: 500; margin-bottom: .75rem; }
.card-title { font-family: 'Playfair Display', serif; font-size: 1.1rem; color: var(--green-deep); margin-bottom: .4rem; }
.card-subtitle { font-size: .85rem; color: var(--text-muted); margin-bottom: 1.25rem; }
.progress-wrap { margin-bottom: .5rem; }
.progress-label { display: flex; justify-content: space-between; font-size: .78rem; color: var(--text-muted); margin-bottom: .4rem; }
.progress-bar { height: 6px; background: var(--cream-dark); border-radius: 99px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, var(--green-mid), var(--green-light)); border-radius: 99px; animation: grow 1.2s .8s ease both; }
@keyframes grow { from { width: 0 !important; } }
.floating-badge { position: absolute; top: -14px; right: 24px; background: var(--green-deep); color: white; padding: .35rem .85rem; border-radius: 50px; font-size: .75rem; font-weight: 500; box-shadow: 0 4px 12px rgba(26,58,42,.3); }
.features { padding: 6rem 3rem; background: var(--green-deep); position: relative; overflow: hidden; }
.features::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 120px; background: var(--cream); clip-path: ellipse(55% 100% at 50% 0%); }
.section-label { font-size: .78rem; text-transform: uppercase; letter-spacing: .12em; color: var(--green-light); margin-bottom: .75rem; }
.section-title { font-family: 'Playfair Display', serif; font-size: clamp(1.8rem, 3vw, 2.6rem); color: white; margin-bottom: 1rem; line-height: 1.2; }
.section-title em { color: var(--green-light); font-style: italic; }
.section-desc { color: rgba(255,255,255,.6); font-size: .95rem; line-height: 1.7; max-width: 500px; margin-bottom: 3.5rem; }
.features-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
.feature-card { background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.1); border-radius: 20px; padding: 2rem; transition: all .3s ease; position: relative; overflow: hidden; }
.feature-card::before { content: ''; position: absolute; inset: 0; background: linear-gradient(135deg, rgba(116,198,157,.08) 0%, transparent 100%); opacity: 0; transition: opacity .3s; }
.feature-card:hover { transform: translateY(-4px); border-color: rgba(116,198,157,.3); }
.feature-card:hover::before { opacity: 1; }
.feature-icon { width: 52px; height: 52px; border-radius: 14px; background: linear-gradient(135deg, var(--green-bright), var(--green-light)); display: flex; align-items: center; justify-content: center; font-size: 1.4rem; margin-bottom: 1.25rem; box-shadow: 0 6px 16px rgba(64,145,108,.35); }
.feature-title { font-family: 'Playfair Display', serif; font-size: 1.1rem; color: white; margin-bottom: .6rem; }
.feature-text { font-size: .875rem; color: rgba(255,255,255,.55); line-height: 1.65; }
.cta-section { padding: 6rem 3rem; text-align: center; background: linear-gradient(160deg, var(--green-deep) 0%, #0d2318 100%); position: relative; overflow: hidden; }
.cta-section::before { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 600px; height: 600px; background: radial-gradient(circle, rgba(64,145,108,.2) 0%, transparent 65%); border-radius: 50%; }
.cta-label { color: var(--green-light); font-size: .78rem; text-transform: uppercase; letter-spacing: .12em; margin-bottom: .75rem; }
.cta-title { font-family: 'Playfair Display', serif; font-size: clamp(2rem, 3.5vw, 3rem); color: white; margin-bottom: 1rem; position: relative; }
.cta-title em { color: var(--green-light); font-style: italic; }
.cta-desc { color: rgba(255,255,255,.6); max-width: 440px; margin: 0 auto 2.5rem; line-height: 1.7; position: relative; }
.cta-buttons { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; position: relative; }
footer { background: #0d2318; padding: 2rem 3rem; text-align: center; color: rgba(255,255,255,.35); font-size: .825rem; border-top: 1px solid rgba(255,255,255,.06); }
footer span { color: var(--green-light); }
@keyframes fadeUp { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: translateY(0); } }
/* ═══════════════════════════════════════════════════════
MODAL DIAGNOSIS GUEST
═══════════════════════════════════════════════════════ */
#guestModal {
display: none;
position: fixed; inset: 0; z-index: 9000;
background: rgba(15, 30, 20, 0.72);
backdrop-filter: blur(6px);
align-items: center; justify-content: center;
padding: 1rem;
}
#guestModal.open { display: flex; }
#guestModal.open .gm-box {
animation: gmSlideUp .35s cubic-bezier(.22,.97,.44,1) both;
}
@keyframes gmSlideUp { from { opacity:0; transform:translateY(32px) scale(.97); } to { opacity:1; transform:none; } }
.gm-box {
background: #fff;
border-radius: 24px;
width: 100%;
max-width: 540px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 32px 80px rgba(10,25,15,.35);
position: relative;
}
.gm-box::-webkit-scrollbar { width: 4px; }
.gm-box::-webkit-scrollbar-track { background: transparent; }
.gm-box::-webkit-scrollbar-thumb { background: #b7ddc4; border-radius: 2px; }
.gm-header {
padding: 1.5rem 1.5rem 1rem;
border-bottom: 1px solid #ede8df;
position: sticky; top: 0; background: #fff; z-index: 2;
border-radius: 24px 24px 0 0;
}
.gm-close {
position: absolute; top: 1rem; right: 1rem;
width: 32px; height: 32px; border-radius: 50%;
background: #f0fdf4; border: none; cursor: pointer;
font-size: 1rem; color: #2d6a4f;
display: flex; align-items: center; justify-content: center;
transition: background .2s;
}
.gm-close:hover { background: #d8f3dc; }
.gm-progress-bar-wrap { height: 5px; background: #ede8df; border-radius: 99px; overflow: hidden; margin-top: .75rem; }
.gm-progress-fill { height: 100%; background: linear-gradient(90deg,#40916c,#74c69d); border-radius: 99px; transition: width .4s ease; }
.gm-body { padding: 1.5rem; }
/* Step panels */
.gm-step { display: none; }
.gm-step.active { display: block; }
/* Gejala list */
.gm-symptom-list { max-height: 300px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; margin-top: .75rem; }
.gm-symptom-list::-webkit-scrollbar { width: 4px; }
.gm-symptom-list::-webkit-scrollbar-thumb { background: #b7ddc4; border-radius: 2px; }
.gm-sym-item {
display: flex; align-items: center; gap: .75rem;
padding: .65rem 1rem; border-radius: 12px;
border: 1.5px solid #ede8df; background: white;
cursor: pointer; transition: all .18s;
}
.gm-sym-item:hover { border-color: #b7ddc4; background: #f8fffe; }
.gm-sym-item.checked { background: #f0fdf4; border-color: #b7ddc4; }
.gm-sym-item input[type=checkbox] { accent-color: #2d6a4f; width: 15px; height: 15px; flex-shrink: 0; }
.gm-sym-code { font-size: .68rem; font-weight: 700; padding: .2rem .5rem; border-radius: 6px; background: #d8f3dc; color: #2d6a4f; flex-shrink: 0; }
.gm-sym-name { font-size: .85rem; color: #1a3a2a; }
/* CF buttons */
.gm-cf-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 6px; margin-top: .5rem; }
.gm-cf-btn {
padding: .5rem .25rem; border-radius: 10px;
border: 1.5px solid #ede8df; background: white;
font-size: .75rem; font-weight: 600; color: #5a7a67;
cursor: pointer; text-align: center; transition: all .15s;
}
.gm-cf-btn:hover { border-color: #74c69d; background: #f0fdf4; }
.gm-cf-btn.active { border-color: #2d6a4f; background: #d8f3dc; color: #1a3a2a; }
/* Hasil ringkas */
.gm-result-card {
background: linear-gradient(135deg, #f0fdf4, #e8f7ee);
border: 1.5px solid #b7ddc4;
border-radius: 16px; padding: 1.25rem; margin-bottom: 1rem;
}
.gm-result-disease { font-family: 'Playfair Display', serif; font-size: 1.2rem; color: #1a3a2a; margin-bottom: .25rem; }
.gm-result-pct { font-size: 2rem; font-weight: 700; color: #2d6a4f; line-height: 1; }
.gm-result-level { font-size: .75rem; font-weight: 600; padding: .25rem .6rem; border-radius: 50px; display: inline-block; margin-top: .25rem; }
/* Lock overlay */
.gm-lock-wrap { position: relative; border-radius: 16px; overflow: hidden; margin-bottom: 1rem; }
.gm-lock-blur {
filter: blur(5px);
background: #f0fdf4;
border: 1.5px solid #b7ddc4;
border-radius: 16px; padding: 1.25rem;
user-select: none;
}
.gm-lock-overlay {
position: absolute; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
background: rgba(248,254,250,.82);
backdrop-filter: blur(2px);
border-radius: 16px;
padding: 1rem; text-align: center;
}
.gm-lock-icon { font-size: 1.75rem; margin-bottom: .5rem; }
.gm-lock-title { font-family: 'Playfair Display', serif; font-size: .95rem; color: #1a3a2a; font-weight: 700; margin-bottom: .25rem; }
.gm-lock-desc { font-size: .78rem; color: #5a7a67; margin-bottom: .75rem; line-height: 1.5; }
/* Buttons di modal */
.gm-btn-primary {
width: 100%; padding: .85rem; border-radius: 12px;
background: #1a3a2a; color: white;
font-family: 'DM Sans', sans-serif; font-size: .9rem; font-weight: 600;
border: none; cursor: pointer; transition: background .2s;
}
.gm-btn-primary:hover { background: #2d6a4f; }
.gm-btn-secondary {
width: 100%; padding: .75rem; border-radius: 12px;
background: white; color: #2d6a4f;
font-family: 'DM Sans', sans-serif; font-size: .9rem; font-weight: 500;
border: 1.5px solid #b7ddc4; cursor: pointer; transition: all .2s;
}
.gm-btn-secondary:hover { background: #f0fdf4; }
.gm-btn-group { display: flex; gap: .75rem; }
.gm-btn-back { flex-shrink: 0; padding: .75rem 1.25rem; border-radius: 12px; background: white; color: #5a7a67; border: 1.5px solid #ede8df; cursor: pointer; font-size: .875rem; font-weight: 500; transition: background .2s; }
.gm-btn-back:hover { background: #f8f4ee; }
.gm-btn-next { flex: 1; padding: .85rem; border-radius: 12px; background: #1a3a2a; color: white; border: none; cursor: pointer; font-size: .9rem; font-weight: 600; transition: background .2s; }
.gm-btn-next:hover { background: #2d6a4f; }
/* Search */
.gm-search { width: 100%; padding: .65rem 1rem; border: 1.5px solid #ede8df; border-radius: 12px; font-size: .875rem; font-family: inherit; color: #1a3a2a; outline: none; margin-bottom: .75rem; transition: border-color .2s; }
.gm-search:focus { border-color: #74c69d; }
/* Loading spinner */
.gm-spinner { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 2.5rem 0; gap: 1rem; }
.gm-spin { width: 40px; height: 40px; border: 3px solid #d8f3dc; border-top-color: #2d6a4f; border-radius: 50%; animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Other results (ringkas) */
.gm-other-item { display: flex; align-items: center; justify-content: space-between; padding: .6rem .9rem; border-radius: 10px; background: #f8fffe; border: 1px solid #e0f0e6; margin-bottom: 5px; }
.gm-other-name { font-size: .82rem; color: #1a3a2a; font-weight: 500; }
.gm-other-pct { font-size: .82rem; font-weight: 700; color: #2d6a4f; }
/* Responsive */
@media (max-width: 900px) { .hero { grid-template-columns: 1fr; text-align: center; padding: 6rem 1.5rem 3rem; } .hero-desc { margin: 0 auto 2rem; } .hero-cta { justify-content: center; } .hero-stats { justify-content: center; } .hero-visual { margin-top: 3rem; } nav { padding: 1rem 1.5rem; } .features, .cta-section { padding: 4rem 1.5rem; } .features-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 600px) { .features-grid { grid-template-columns: 1fr; } .plant-card { width: 290px; } }
@media (max-width: 640px) { .features { padding: 5rem 1.5rem 3rem; } .features::before { height: 80px; } .section-title { font-size: 1.8rem; } .artikel-desktop { display: none !important; } .artikel-mobile { display: block !important; } }
@media (max-width: 480px) { nav { padding: .75rem 1rem; gap: .5rem; } .nav-logo { font-size: .95rem; gap: .4rem; } .nav-links { gap: .4rem; } .btn-outline { padding: .4rem .75rem; font-size: .75rem; } .btn-solid { padding: .4rem .75rem; font-size: .75rem; } }
</style>
</head>
<body>
<!-- NAV -->
<nav>
<a href="/" class="nav-logo">
<svg width="34" height="34" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="11" cy="15" rx="5" ry="11" fill="#d8f3dc" stroke="#2d6a4f" stroke-width="1.3" transform="rotate(-15 11 15)"/>
<line x1="11" y1="6" x2="10" y2="24" stroke="#2d6a4f" stroke-width="1" stroke-linecap="round" opacity="0.8" transform="rotate(-15 11 15)"/>
<line x1="10" y1="13" x2="7" y2="17" stroke="#2d6a4f" stroke-width="0.8" stroke-linecap="round" opacity="0.6"/>
<line x1="10" y1="17" x2="7.5" y2="21" stroke="#2d6a4f" stroke-width="0.8" stroke-linecap="round" opacity="0.6"/>
<path d="M19 7 C19 7 24 7 24 12 C24 17 20 18 20 22" stroke="#1a3a2a" stroke-width="1.5" stroke-linecap="round" fill="none"/>
<circle cx="20" cy="24.5" r="3" stroke="#1a3a2a" stroke-width="1.3" fill="#d8f3dc"/>
<line x1="18" y1="7" x2="21" y2="7" stroke="#1a3a2a" stroke-width="1.5" stroke-linecap="round"/>
</svg>
SiPakarTebu
</a>
<div class="nav-links">
<a href="{{ route('login') }}" class="btn-outline">Masuk</a>
<a href="{{ route('register') }}" class="btn-solid">Daftar Gratis</a>
</div>
</nav>
<!-- HERO -->
<section class="hero">
<div class="hero-content">
<div class="hero-badge">Didukung data oleh pakar</div>
<h1>Kenali Penyakit<br><em>Tanaman</em> Tebu<br>Lebih Cepat</h1>
<p class="hero-desc">
SiPakarTebu menggunakan kecerdasan buatan berbasis <em>Certainty Factor</em> untuk mendiagnosa penyakit tanaman secara akurat cukup jawab beberapa pertanyaan gejala.
</p>
<div class="hero-cta">
{{-- Tombol utama: buka modal diagnosis guest --}}
<button onclick="gmOpen()" class="btn-hero btn-hero-primary">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2v12M2 8h12" stroke="white" stroke-width="2" stroke-linecap="round"/></svg>
Mulai Diagnosa
</button>
<a href="{{ route('login') }}" class="btn-hero btn-hero-secondary">
Sudah punya akun?
</a>
</div>
<div class="hero-stats">
<div class="stat">
<span class="stat-num">90%</span>
<span class="stat-label">Akurasi Rata-rata</span>
</div>
<div class="stat" style="border-left:1px solid var(--green-pale);padding-left:2rem">
<span class="stat-num">10</span>
<span class="stat-label">Jenis Penyakit</span>
</div>
<div class="stat" style="border-left:1px solid var(--green-pale);padding-left:2rem">
<span class="stat-num">1 mnt</span>
<span class="stat-label">Waktu Diagnosa</span>
</div>
</div>
</div>
<div class="hero-visual">
<div class="plant-card">
<div class="floating-badge"> Diagnosa Selesai</div>
<div class="plant-illustration">
<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="60" cy="55" rx="34" ry="44" fill="#52b788" opacity=".8"/>
<ellipse cx="60" cy="55" rx="34" ry="44" fill="none" stroke="#40916c" stroke-width="1.5"/>
<path d="M60 20 Q80 45 60 95" stroke="#1a3a2a" stroke-width="1.5" stroke-linecap="round" opacity=".5"/>
<path d="M60 40 Q48 45 42 58" stroke="#1a3a2a" stroke-width="1" stroke-linecap="round" opacity=".4"/>
<path d="M60 52 Q72 55 76 66" stroke="#1a3a2a" stroke-width="1" stroke-linecap="round" opacity=".4"/>
<ellipse cx="38" cy="72" rx="20" ry="28" fill="#74c69d" transform="rotate(-20 38 72)" opacity=".7"/>
<ellipse cx="86" cy="68" rx="18" ry="26" fill="#95d5b2" transform="rotate(15 86 68)" opacity=".6"/>
<rect x="54" y="90" width="12" height="22" rx="4" fill="#8b5e3c"/>
</svg>
</div>
<div class="card-tag">⚠️ Terdeteksi</div>
<div class="card-title">Luka Api (Leaf Scald)</div>
<div class="card-subtitle">Penyakit disebabkan bakteri Xanthomonas albilineans</div>
<div class="progress-wrap">
<div class="progress-label"><span>Tingkat Kepastian</span><span>78%</span></div>
<div class="progress-bar"><div class="progress-fill" style="width:78%"></div></div>
</div>
<div class="progress-wrap">
<div class="progress-label"><span>Kesesuaian Gejala</span><span>91%</span></div>
<div class="progress-bar"><div class="progress-fill" style="width:91%"></div></div>
</div>
</div>
</div>
</section>
<!-- FEATURES -->
<section class="features">
<p class="section-label">Fitur Unggulan</p>
<h2 class="section-title">Teknologi Cermat untuk<br><em>Kebun Sehat</em> Kamu</h2>
<p class="section-desc">Sistem pakar kami menggabungkan pengetahuan ahli pertanian dengan teknologi untuk hasil diagnosa yang dapat dipercaya.</p>
<div class="features-grid">
<div class="feature-card"><div class="feature-icon">🔬</div><div class="feature-title">Diagnosa Akurat</div><p class="feature-text">Menggunakan metode Certainty Factor yang telah terbukti untuk mengidentifikasi penyakit tanaman dengan tingkat akurasi tinggi.</p></div>
<div class="feature-card"><div class="feature-icon"></div><div class="feature-title">Hasil Instan</div><p class="feature-text">Dapatkan hasil diagnosa dalam hitungan menit. Cukup jawab pertanyaan gejala dan sistem kami akan menganalisis secara otomatis.</p></div>
<div class="feature-card"><div class="feature-icon">📋</div><div class="feature-title">Rekomendasi Penanganan</div><p class="feature-text">Setiap hasil diagnosa disertai panduan penanganan lengkap yang bisa langsung dipraktikkan di kebun kamu.</p></div>
<div class="feature-card"><div class="feature-icon">📊</div><div class="feature-title">Riwayat Diagnosa</div><p class="feature-text">Simpan dan pantau semua diagnosa sebelumnya. Lacak perkembangan kesehatan tanaman kamu dari waktu ke waktu.</p></div>
<div class="feature-card"><div class="feature-icon">🌱</div><div class="feature-title">Database Penyakit Lengkap</div><p class="feature-text">Mencakup lebih dari 5 jenis penyakit umum pada tanaman dengan gejala dan penanganan yang terperinci.</p></div>
<div class="feature-card"><div class="feature-icon">🛡️</div><div class="feature-title">Data Aman & Privat</div><p class="feature-text">Data diagnosa kamu tersimpan dengan aman. Hanya kamu yang bisa mengakses riwayat dan hasil diagnosa milik kamu.</p></div>
</div>
</section>
<!-- ARTIKEL -->
<section style="background:#f8f4ee;padding:5rem 1.5rem;">
<div style="max-width:1100px;margin:0 auto;">
<div style="text-align:center;margin-bottom:3rem;">
<p style="font-size:.78rem;font-weight:600;color:#2d6a4f;letter-spacing:.12em;text-transform:uppercase;margin-bottom:.75rem;">BERITA & RISET TERKINI</p>
<h2 style="font-family:'Playfair Display',serif;font-size:clamp(1.8rem,3vw,2.6rem);color:#1a3a2a;margin-bottom:1rem;line-height:1.2;">Dari Sumber <em style="color:#40916c;font-style:italic;">Terpercaya</em></h2>
<p style="color:#5a7a67;font-size:.95rem;max-width:480px;margin:0 auto;">Artikel terbaru seputar pertanian dan penyakit tanaman dari media & lembaga penelitian nasional</p>
</div>
@if(isset($articles) && count($articles) > 0)
<div class="artikel-desktop" style="display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem;">
@foreach($articles as $article)
<a href="{{ $article['link'] ?? '#' }}" target="_blank" rel="noopener"
style="display:flex;flex-direction:column;background:white;border-radius:20px;overflow:hidden;border:1px solid #ede8df;text-decoration:none;box-shadow:0 4px 16px rgba(26,58,42,.06);transition:transform .3s,box-shadow .3s;"
onmouseover="this.style.transform='translateY(-4px)';this.style.boxShadow='0 12px 32px rgba(26,58,42,.12)'"
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='0 4px 16px rgba(26,58,42,.06)'">
<div style="height:180px;overflow:hidden;background:#e8f5ec;position:relative;flex-shrink:0;">
@if(!empty($article['image']))
<img src="{{ $article['image'] }}" alt="{{ $article['title'] }}" style="width:100%;height:100%;object-fit:cover;" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
<div style="display:none;width:100%;height:100%;align-items:center;justify-content:center;position:absolute;top:0;left:0;"><span style="font-size:3rem;">🌿</span></div>
@else
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;"><span style="font-size:3rem;">🌿</span></div>
@endif
</div>
<div style="padding:1.25rem;display:flex;flex-direction:column;flex:1;">
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem;">
<span style="font-size:.7rem;font-weight:700;padding:.25rem .6rem;border-radius:.5rem;background:#d8f3dc;color:#2d6a4f;">{{ $article['source'] ?? 'SiPakarTebu' }}</span>
<span style="font-size:.7rem;color:#a0b4a8;">{{ $article['date'] ?? date('d M Y') }}</span>
</div>
<h3 style="font-family:'Playfair Display',serif;font-size:.95rem;color:#1a3a2a;line-height:1.5;margin-bottom:.6rem;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;">{{ $article['title'] ?? 'Artikel Pertanian' }}</h3>
<p style="font-size:.8rem;color:#8fa89a;line-height:1.6;margin-bottom:1rem;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;flex:1;">{{ $article['excerpt'] ?? '' }}</p>
<div style="font-size:.8rem;font-weight:600;color:#2d6a4f;margin-top:auto;">Baca Selengkapnya </div>
</div>
</a>
@endforeach
</div>
<div class="artikel-mobile" style="display:none;">
@foreach($articles as $i => $article)
<div class="artikel-slide" style="display:{{ $i === 0 ? 'flex' : 'none' }};flex-direction:column;background:white;border-radius:20px;overflow:hidden;border:1px solid #ede8df;box-shadow:0 4px 16px rgba(26,58,42,.06);">
<a href="{{ $article['link'] ?? '#' }}" target="_blank" rel="noopener" style="text-decoration:none;display:flex;flex-direction:column;">
<div style="height:200px;overflow:hidden;background:#e8f5ec;position:relative;">
@if(!empty($article['image']))
<img src="{{ $article['image'] }}" alt="{{ $article['title'] }}" style="width:100%;height:100%;object-fit:cover;" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
<div style="display:none;width:100%;height:100%;align-items:center;justify-content:center;position:absolute;top:0;left:0;"><span style="font-size:3rem;">🌿</span></div>
@else
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;"><span style="font-size:3rem;">🌿</span></div>
@endif
</div>
<div style="padding:1.5rem;">
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem;">
<span style="font-size:.7rem;font-weight:700;padding:.25rem .6rem;border-radius:.5rem;background:#d8f3dc;color:#2d6a4f;">{{ $article['source'] ?? 'SiPakarTebu' }}</span>
<span style="font-size:.7rem;color:#a0b4a8;">{{ $article['date'] ?? date('d M Y') }}</span>
</div>
<h3 style="font-family:'Playfair Display',serif;font-size:1.1rem;color:#1a3a2a;line-height:1.5;margin-bottom:.75rem;">{{ $article['title'] ?? 'Artikel Pertanian' }}</h3>
<p style="font-size:.875rem;color:#8fa89a;line-height:1.6;margin-bottom:1rem;">{{ $article['excerpt'] ?? '' }}</p>
<div style="font-size:.875rem;font-weight:600;color:#2d6a4f;">Baca Selengkapnya </div>
</div>
</a>
</div>
@endforeach
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:1.25rem;">
<button onclick="prevArtikel()" id="btn-prev" style="padding:.6rem 1.25rem;border-radius:50px;border:1.5px solid #b7ddc4;background:white;color:#2d6a4f;font-size:.875rem;font-weight:600;cursor:pointer;opacity:0.4;"> Sebelumnya</button>
<span id="artikel-counter" style="font-size:.8rem;color:#8fa89a;">1 / {{ count($articles) }}</span>
<button onclick="nextArtikel()" id="btn-next" style="padding:.6rem 1.25rem;border-radius:50px;border:none;background:#2d6a4f;color:white;font-size:.875rem;font-weight:600;cursor:pointer;">Selanjutnya </button>
</div>
</div>
@else
<div style="text-align:center;padding:4rem 0;"><p style="color:#8fa89a;">Artikel sedang dimuat...</p></div>
@endif
</div>
</section>
<!-- CTA -->
<section class="cta-section">
<p class="cta-label">Mulai Sekarang</p>
<h2 class="cta-title">Jaga Tanaman Kamu<br>tetap <em>Sehat & Subur</em></h2>
<p class="cta-desc">Daftar gratis sekarang dan mulai diagnosa pertama kamu dalam hitungan menit. Tidak perlu kartu kredit.</p>
<div class="cta-buttons">
<a href="{{ route('register') }}" class="btn-hero btn-hero-primary" style="font-size:1rem;padding:1rem 2.25rem;">Daftar Gratis Sekarang</a>
<a href="{{ route('login') }}" class="btn-hero btn-hero-secondary" style="font-size:1rem;padding:1rem 2.25rem;background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.2) !important;color:white;">Masuk ke Akun</a>
</div>
</section>
<!-- FOOTER -->
<footer>
<p>© 2026 <span>SiPakarTebu</span> Sistem Pakar Diagnosa Penyakit Tanaman. Dibuat dengan 🌿</p>
</footer>
<!-- ═══════════════════════════════════════════════════════════════
MODAL DIAGNOSIS GUEST
═══════════════════════════════════════════════════════════════ -->
<div id="guestModal">
<div class="gm-box">
<!-- Header sticky -->
<div class="gm-header">
<button class="gm-close" onclick="gmClose()"></button>
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.2rem;">
<span style="font-size:.7rem;font-weight:700;color:#2d6a4f;text-transform:uppercase;letter-spacing:.08em;">
Coba Diagnosa
</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:.8rem;color:#8fa89a;">
Step <span id="gm-step-num">1</span> dari 3
<span id="gm-step-label">Pilih Gejala</span>
</span>
</div>
<div class="gm-progress-bar-wrap">
<div class="gm-progress-fill" id="gm-prog" style="width:33%;"></div>
</div>
</div>
<!-- Body -->
<div class="gm-body">
<!-- ── STEP 1: Pilih Gejala ─────────────────────── -->
<div class="gm-step active" id="gm-s1">
<h3 style="font-family:'Playfair Display',serif;color:#1a3a2a;font-size:1.1rem;margin-bottom:.25rem;">Pilih Gejala</h3>
<p style="font-size:.8rem;color:#8fa89a;margin-bottom:.75rem;">Pilih minimal 4 gejala yang tampak pada tanaman</p>
<input type="text" class="gm-search" id="gm-search" placeholder="Cari gejala..." oninput="gmFilterSearch(this.value)">
<div class="gm-symptom-list" id="gm-sym-list">
<div style="text-align:center;padding:2rem;color:#8fa89a;font-size:.875rem;">
Memuat daftar gejala...
</div>
</div>
<div style="margin-top:.75rem;display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:.82rem;color:#5a7a67;">
Dipilih: <strong id="gm-count" style="color:#2d6a4f;">0</strong> gejala
<span id="gm-warn" style="color:#dc2626;font-size:.75rem;display:none;"> (min. 4)</span>
</span>
</div>
<div style="margin-top:1rem;">
<button class="gm-btn-next" style="width:100%;" onclick="gmNext()">Lanjut </button>
</div>
</div>
<!-- ── STEP 2: Tingkat Keyakinan ──────────────────── -->
<div class="gm-step" id="gm-s2">
<h3 style="font-family:'Playfair Display',serif;color:#1a3a2a;font-size:1.1rem;margin-bottom:.25rem;">Tingkat Keyakinan</h3>
<p style="font-size:.8rem;color:#8fa89a;margin-bottom:1rem;">Seberapa yakin kamu melihat gejala ini pada tanaman?</p>
<div id="gm-cf-container" style="display:flex;flex-direction:column;gap:12px;"></div>
<div class="gm-btn-group" style="margin-top:1.25rem;">
<button class="gm-btn-back" onclick="gmPrev()"> Kembali</button>
<button class="gm-btn-next" onclick="gmNext()">Lanjut </button>
</div>
</div>
<!-- ── STEP 3: Konfirmasi ──────────────────────────── -->
<div class="gm-step" id="gm-s3">
<h3 style="font-family:'Playfair Display',serif;color:#1a3a2a;font-size:1.1rem;margin-bottom:.25rem;">Konfirmasi</h3>
<p style="font-size:.8rem;color:#8fa89a;margin-bottom:1rem;">Periksa kembali sebelum diagnosa dimulai</p>
<div id="gm-summary" style="display:flex;flex-direction:column;gap:6px;margin-bottom:1.25rem;"></div>
<div class="gm-btn-group">
<button class="gm-btn-back" onclick="gmPrev()"> Kembali</button>
<button class="gm-btn-next" onclick="gmSubmit()">Diagnosa Sekarang</button>
</div>
</div>
<!-- ── STEP 4: Loading ─────────────────────────────── -->
<div class="gm-step" id="gm-s4">
<div class="gm-spinner">
<div class="gm-spin"></div>
<p style="font-size:.9rem;color:#5a7a67;">Menganalisis gejala...</p>
</div>
</div>
<!-- ── STEP 5: Hasil ───────────────────────────────── -->
<div class="gm-step" id="gm-s5">
<h3 style="font-family:'Playfair Display',serif;color:#1a3a2a;font-size:1.1rem;margin-bottom:.25rem;">Hasil Diagnosa</h3>
<p style="font-size:.8rem;color:#8fa89a;margin-bottom:1rem;">Berdasarkan gejala yang kamu pilih</p>
<!-- Hasil utama (terlihat) -->
<div class="gm-result-card" id="gm-result-main"></div>
<!-- Hasil lain + penanganan (dikunci) -->
<div class="gm-lock-wrap">
<div class="gm-lock-blur" id="gm-result-blur">
<p style="font-size:.8rem;font-weight:600;color:#1a3a2a;margin-bottom:.5rem;">Penanganan yang Disarankan</p>
<p style="font-size:.8rem;color:#5a7a67;"> Semprot fungisida berbasis tembaga...</p>
<p style="font-size:.8rem;color:#5a7a67;"> Cabut dan musnahkan tanaman yang terinfeksi berat</p>
<p style="font-size:.8rem;color:#5a7a67;"> Lakukan rotasi tanaman minimal 2 musim</p>
<p style="font-size:.8rem;color:#5a7a67;margin-top:.75rem;font-weight:600;">Kemungkinan Penyakit Lain</p>
<p style="font-size:.8rem;color:#5a7a67;"> ██████████ (██%)</p>
<p style="font-size:.8rem;color:#5a7a67;"> ██████████ (██%)</p>
</div>
<div class="gm-lock-overlay">
<div class="gm-lock-icon">🔒</div>
<div class="gm-lock-title">Detail Lengkap Dikunci</div>
<div class="gm-lock-desc">Daftar atau masuk untuk melihat penanganan lengkap, kemungkinan penyakit lain, dan simpan riwayat diagnosa kamu.</div>
<a href="{{ route('register') }}"
style="display:block;width:100%;padding:.7rem;border-radius:10px;background:#1a3a2a;color:white;text-align:center;font-size:.85rem;font-weight:600;text-decoration:none;margin-bottom:.5rem;">
Daftar Gratis Lihat Hasil Lengkap
</a>
<a href="{{ route('login') }}"
style="display:block;width:100%;padding:.6rem;border-radius:10px;border:1.5px solid #b7ddc4;color:#2d6a4f;text-align:center;font-size:.82rem;font-weight:500;text-decoration:none;background:white;">
Sudah punya akun? Masuk
</a>
</div>
</div>
<button onclick="gmReset()" style="width:100%;padding:.65rem;border-radius:10px;background:white;border:1.5px solid #ede8df;color:#5a7a67;font-size:.85rem;cursor:pointer;margin-top:.75rem;font-family:inherit;">
Ulangi Diagnosa
</button>
</div>
</div><!-- /gm-body -->
</div><!-- /gm-box -->
</div><!-- /guestModal -->
<script>
/* ═══════════════════════════════════════════════════════
ARTIKEL SLIDER (mobile)
═══════════════════════════════════════════════════════ */
const totalArtikel = {{ isset($articles) ? count($articles) : 0 }};
let currentArtikel = 0;
function showArtikel(index) {
document.querySelectorAll('.artikel-slide').forEach((el,i)=>{ el.style.display = i===index?'flex':'none'; });
document.getElementById('artikel-counter').textContent = (index+1)+' / '+totalArtikel;
document.getElementById('btn-prev').style.opacity = index===0?'0.4':'1';
document.getElementById('btn-next').style.opacity = index===totalArtikel-1?'0.4':'1';
}
function prevArtikel() { if(currentArtikel>0){currentArtikel--;showArtikel(currentArtikel);} }
function nextArtikel() { if(currentArtikel<totalArtikel-1){currentArtikel++;showArtikel(currentArtikel);} }
/* ═══════════════════════════════════════════════════════
GUEST MODAL State
═══════════════════════════════════════════════════════ */
let gmStep = 1;
let gmSymptoms = []; // [{code, name}]
let gmChecked = []; // [code, ...]
let gmCFValues = {}; // {code: '0.8'}
const gmStepLabels = { 1:'Pilih Gejala', 2:'Tingkat Keyakinan', 3:'Konfirmasi' };
const gmProgWidth = { 1:'33%', 2:'66%', 3:'100%', 4:'100%', 5:'100%' };
const gmCFOpts = [
{val:'0.8', label:'Sangat Yakin'},
{val:'0.6', label:'Yakin'},
{val:'0.4', label:'Cukup Yakin'},
{val:'0.2', label:'Kurang Yakin'},
{val:'0.0', label:'Tidak Yakin'},
];
const gmCFMap = {'0.8':'Sangat Yakin','0.6':'Yakin','0.4':'Cukup Yakin','0.2':'Kurang Yakin','0.0':'Tidak Yakin'};
/* ── Buka/tutup modal ── */
function gmOpen() {
document.getElementById('guestModal').classList.add('open');
document.body.style.overflow = 'hidden';
if (gmSymptoms.length === 0) gmLoadSymptoms();
}
function gmClose() {
document.getElementById('guestModal').classList.remove('open');
document.body.style.overflow = '';
}
// Klik backdrop tutup modal
document.getElementById('guestModal').addEventListener('click', function(e) {
if (e.target === this) gmClose();
});
/* ── Load gejala dari server ── */
async function gmLoadSymptoms() {
try {
const res = await fetch('{{ route("guest.symptoms") }}');
gmSymptoms = await res.json();
gmRenderSymptoms(gmSymptoms);
} catch(e) {
document.getElementById('gm-sym-list').innerHTML =
'<div style="text-align:center;padding:2rem;color:#dc2626;font-size:.875rem;">Gagal memuat gejala. Refresh halaman.</div>';
}
}
/* ── Render daftar gejala ── */
function gmRenderSymptoms(list) {
const container = document.getElementById('gm-sym-list');
if (!list.length) {
container.innerHTML = '<div style="text-align:center;padding:2rem;color:#8fa89a;font-size:.875rem;">Tidak ada gejala ditemukan.</div>';
return;
}
container.innerHTML = list.map(s => `
<label class="gm-sym-item ${gmChecked.includes(s.code)?'checked':''}" id="gm-lbl-${s.code}">
<input type="checkbox" ${gmChecked.includes(s.code)?'checked':''}
onchange="gmToggle('${s.code}', this)"
style="accent-color:#2d6a4f;width:15px;height:15px;flex-shrink:0;">
<span class="gm-sym-code">${s.code}</span>
<span class="gm-sym-name">${s.name}</span>
</label>
`).join('');
gmUpdateCount();
}
/* ── Toggle gejala ── */
function gmToggle(code, cb) {
if (cb.checked) {
if (!gmChecked.includes(code)) gmChecked.push(code);
cb.closest('label').classList.add('checked');
} else {
gmChecked = gmChecked.filter(c => c !== code);
cb.closest('label').classList.remove('checked');
delete gmCFValues[code];
}
gmUpdateCount();
}
/* ── Update hitungan ── */
function gmUpdateCount() {
document.getElementById('gm-count').textContent = gmChecked.length;
document.getElementById('gm-warn').style.display = gmChecked.length < 4 ? 'inline' : 'none';
}
/* ── Filter search ── */
function gmFilterSearch(keyword) {
const kw = keyword.toLowerCase();
const filtered = gmSymptoms.filter(s =>
s.code.toLowerCase().includes(kw) || s.name.toLowerCase().includes(kw)
);
gmRenderSymptoms(filtered);
}
/* ── Navigasi step ── */
function gmShowStep(n) {
document.querySelectorAll('.gm-step').forEach(el => el.classList.remove('active'));
document.getElementById('gm-s' + n).classList.add('active');
gmStep = n;
if (n <= 3) {
document.getElementById('gm-step-num').textContent = n;
document.getElementById('gm-step-label').textContent = gmStepLabels[n] || '';
}
document.getElementById('gm-prog').style.width = gmProgWidth[n] || '100%';
}
function gmNext() {
if (gmStep === 1) {
if (gmChecked.length < 4) {
document.getElementById('gm-warn').style.display = 'inline';
return;
}
gmBuildCF();
gmShowStep(2);
} else if (gmStep === 2) {
gmBuildSummary();
gmShowStep(3);
}
}
function gmPrev() {
if (gmStep === 2) gmShowStep(1);
else if (gmStep === 3) gmShowStep(2);
}
/* ── Build CF step ── */
function gmBuildCF() {
const container = document.getElementById('gm-cf-container');
container.innerHTML = '';
gmChecked.forEach(code => {
const sym = gmSymptoms.find(s => s.code === code);
if (!sym) return;
// Default ke 0.8
if (!gmCFValues[code]) gmCFValues[code] = '0.8';
const btns = gmCFOpts.map(opt => `
<button type="button"
class="gm-cf-btn ${gmCFValues[code] === opt.val ? 'active' : ''}"
id="gmcfb-${code}-${opt.val.replace('.','_')}"
onclick="gmSelectCF('${code}', '${opt.val}')">
${opt.label}
</button>
`).join('');
container.innerHTML += `
<div style="padding:.9rem 1rem;border-radius:14px;border:1px solid #ede8df;background:#fafaf8;">
<p style="font-size:.82rem;font-weight:600;color:#1a3a2a;margin-bottom:.65rem;">
<span style="font-size:.68rem;font-weight:700;padding:.15rem .45rem;border-radius:5px;background:#d8f3dc;color:#2d6a4f;margin-right:.35rem;">${code}</span>
${sym.name}
</p>
<div class="gm-cf-grid">${btns}</div>
</div>
`;
});
}
function gmSelectCF(code, val) {
gmCFValues[code] = val;
gmCFOpts.forEach(opt => {
const btn = document.getElementById('gmcfb-' + code + '-' + opt.val.replace('.','_'));
if (btn) btn.classList.toggle('active', opt.val === val);
});
}
/* ── Build Summary ── */
function gmBuildSummary() {
const el = document.getElementById('gm-summary');
el.innerHTML = gmChecked.map(code => {
const sym = gmSymptoms.find(s => s.code === code);
const val = gmCFValues[code] || '0.8';
return `
<div style="display:flex;align-items:center;justify-content:space-between;padding:.6rem .9rem;border-radius:10px;background:#f0fdf4;border:1px solid #b7ddc4;">
<span style="font-size:.82rem;color:#1a3a2a;">
<span style="font-size:.68rem;font-weight:700;padding:.15rem .4rem;border-radius:5px;background:#d8f3dc;color:#2d6a4f;margin-right:.3rem;">${code}</span>
${sym ? sym.name : code}
</span>
<span style="font-size:.75rem;font-weight:600;padding:.2rem .6rem;border-radius:50px;background:#d8f3dc;color:#2d6a4f;flex-shrink:0;margin-left:.5rem;">${gmCFMap[val]||val}</span>
</div>
`;
}).join('');
}
/* ── Submit ke server ── */
async function gmSubmit() {
gmShowStep(4);
// Susun payload
const payload = { symptoms: {} };
gmChecked.forEach(code => { payload.symptoms[code] = gmCFValues[code] || '0.8'; });
try {
const res = await fetch('{{ route("guest.diagnosis") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
},
body: JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok) {
alert(data.error || 'Terjadi kesalahan. Coba lagi.');
gmShowStep(3);
return;
}
gmShowResult(data);
} catch(e) {
alert('Gagal terhubung ke server. Periksa koneksi internet kamu.');
gmShowStep(3);
}
}
/* ── Tampilkan hasil ── */
function gmShowResult(data) {
const pct = data.confidence;
const level = data.level;
// Warna badge level
const levelColors = {
'Sangat Tinggi': {bg:'#d8f3dc', color:'#2d6a4f'},
'Tinggi': {bg:'#dbeafe', color:'#1d4ed8'},
'Sedang': {bg:'#fef3c7', color:'#92400e'},
'Rendah': {bg:'#fee2e2', color:'#991b1b'},
'Sangat Rendah': {bg:'#f3f4f6', color:'#6b7280'},
'Tidak Terdeteksi': {bg:'#f3f4f6', color:'#6b7280'},
};
const lc = levelColors[level] || levelColors['Tidak Terdeteksi'];
document.getElementById('gm-result-main').innerHTML = `
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:.75rem;">
<div>
<p style="font-size:.7rem;font-weight:600;color:#5a7a67;text-transform:uppercase;letter-spacing:.06em;margin-bottom:.2rem;">Penyakit Terdeteksi</p>
<div class="gm-result-disease">${data.disease_name}</div>
</div>
<span class="gm-result-level" style="background:${lc.bg};color:${lc.color};">${level}</span>
</div>
<div style="margin-bottom:.5rem;">
<div style="display:flex;justify-content:space-between;font-size:.78rem;color:#5a7a67;margin-bottom:.35rem;">
<span>Tingkat Kepastian</span>
<span style="font-weight:700;color:#2d6a4f;">${pct}%</span>
</div>
<div style="height:8px;background:#d8f3dc;border-radius:99px;overflow:hidden;">
<div style="height:100%;width:${pct}%;background:linear-gradient(90deg,#2d6a4f,#74c69d);border-radius:99px;transition:width .8s ease;"></div>
</div>
</div>
<p style="font-size:.75rem;color:#8fa89a;margin-top:.5rem;">
🌿 Berdasarkan ${gmChecked.length} gejala yang dipilih
</p>
`;
gmShowStep(5);
}
/* ── Reset modal ── */
function gmReset() {
gmChecked = [];
gmCFValues = {};
gmShowStep(1);
gmRenderSymptoms(gmSymptoms);
document.getElementById('gm-search').value = '';
}
</script>
</body>
</html>

8
routes/console.php Normal file
View File

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

104
routes/web.php Normal file
View File

@ -0,0 +1,104 @@
<?php
use App\Http\Controllers\AuthController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\DiagnosisController;
use App\Http\Controllers\HistoryController;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\NotificationController;
use App\Http\Controllers\DiseaseController;
use App\Http\Controllers\WelcomeController;
use App\Http\Controllers\UserController;
// Tambah di bagian use (atas file):
use App\Http\Controllers\GuestDiagnosisController;
// Tambah setelah Route::get('/', ...) :
Route::get('/guest/symptoms', [GuestDiagnosisController::class, 'symptoms'])->name('guest.symptoms');
Route::post('/guest/diagnosis', [GuestDiagnosisController::class, 'process'])->name('guest.diagnosis');
// ── Halaman utama ─────────────────────────────────────────────────
Route::get('/', [WelcomeController::class, 'index'])->name('home');
Route::delete('/profile/delete', [ProfileController::class, 'destroy'])->name('profile.destroy')->middleware('auth');
// ── Guest routes ──────────────────────────────────────────────────
Route::middleware('guest')->group(function () {
Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
Route::get('/register', [AuthController::class, 'showRegister'])->name('register');
Route::post('/login', [AuthController::class, 'login']);
Route::post('/register',[AuthController::class, 'register']);
Route::get('/forgot-password', [ForgotPasswordController::class, 'showEmailForm'])->name('password.email');
Route::post('/forgot-password', [ForgotPasswordController::class, 'sendOtp'])->name('password.send-otp');
Route::get('/forgot-password/otp', [ForgotPasswordController::class, 'showOtpForm'])->name('password.otp.form');
Route::post('/forgot-password/otp', [ForgotPasswordController::class, 'verifyOtp'])->name('password.otp.verify');
Route::post('/forgot-password/otp/resend', [ForgotPasswordController::class, 'resendOtp'])->name('password.otp.resend');
Route::get('/forgot-password/reset', [ForgotPasswordController::class, 'showResetForm'])->name('password.reset.form');
Route::post('/forgot-password/reset', [ForgotPasswordController::class, 'resetPassword'])->name('password.reset');
});
// ── Logout ────────────────────────────────────────────────────────
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
// ── Authenticated routes ─────────────────────────────────────────
Route::middleware('auth')->group(function () {
// Dashboard
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
// Diagnosis
Route::prefix('diagnosis')->name('diagnosis.')->group(function () {
Route::get('/', [DiagnosisController::class, 'index'])->name('index');
Route::get('/create', [DiagnosisController::class, 'create'])->name('create');
Route::post('/', [DiagnosisController::class, 'store'])->name('store');
Route::get('/{id}/result', [DiagnosisController::class, 'result'])->name('result');
});
// Riwayat
Route::get('/riwayat', [HistoryController::class, 'index'])->name('history');
// ── KAMUS (READ dulu) ─────────────────────────────
Route::get('/kamus', [DiseaseController::class, 'index'])->name('diseases.index');
// ⚠️ JANGAN TARUH {disease} DI ATAS
// nanti bentrok dengan /kamus/tambah
// Notifikasi
Route::get('/notifications', [NotificationController::class, 'index'])->name('notifications');
Route::post('/notifications/{id}/read', [NotificationController::class, 'markRead'])->name('notifications.read');
Route::delete('/notifications/{id}', [NotificationController::class, 'destroy'])->name('notifications.destroy');
Route::delete('/notifications', [NotificationController::class, 'destroyAll'])->name('notifications.destroyAll');
// Profil
Route::get('/profile', [ProfileController::class, 'show'])->name('profile');
Route::post('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::post('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password');
});
// ── ADMIN ONLY ───────────────────────────────────────────────────
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/kamus/tambah', [DiseaseController::class, 'create'])->name('diseases.create');
Route::post('/kamus', [DiseaseController::class, 'store'])->name('diseases.store');
Route::delete('/kamus/{disease}', [DiseaseController::class, 'destroy'])->name('diseases.destroy');
Route::get('/diseases/{disease}/edit',[DiseaseController::class, 'edit'])->name('diseases.edit');
Route::put('/diseases/{disease}', [DiseaseController::class, 'update'])->name('diseases.update');
// User management
Route::get('/users', [UserController::class, 'index'])->name('users.index');
Route::patch('/users/{user}/role', [UserController::class, 'updateRole'])->name('users.updateRole');
Route::delete('/users/{user}', [UserController::class, 'destroy'])->name('users.destroy');
});
// ── TARUH PALING BAWAH (ANTI BENTROK) ───────────────────────────
Route::get('/kamus/{disease}', [DiseaseController::class, 'show'])
->middleware('auth')
->name('diseases.show');
// Redirect /diseases/{id} ke /kamus/{id} supaya tidak 405
Route::get('/diseases/{disease}', function($disease) {
return redirect()->route('diseases.show', $disease);
})->middleware('auth');