first commit
This commit is contained in:
commit
93d9eee2c6
|
|
@ -0,0 +1,18 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[compose.yaml]
|
||||||
|
indent_size = 4
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
APP_NAME=Laravel
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
# PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=sqlite
|
||||||
|
# DB_HOST=127.0.0.1
|
||||||
|
# DB_PORT=3306
|
||||||
|
# DB_DATABASE=laravel
|
||||||
|
# DB_USERNAME=root
|
||||||
|
# DB_PASSWORD=
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
/.github export-ignore
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
.styleci.yml export-ignore
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.phpunit.cache
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
|
/auth.json
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/vendor
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
Thumbs.db
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## About Laravel
|
||||||
|
|
||||||
|
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||||
|
|
||||||
|
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||||
|
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||||
|
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||||
|
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||||
|
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||||
|
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||||
|
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||||
|
|
||||||
|
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||||
|
|
||||||
|
## Learning Laravel
|
||||||
|
|
||||||
|
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
|
||||||
|
|
||||||
|
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||||
|
|
||||||
|
## Laravel Sponsors
|
||||||
|
|
||||||
|
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||||
|
|
||||||
|
### Premium Partners
|
||||||
|
|
||||||
|
- **[Vehikl](https://vehikl.com)**
|
||||||
|
- **[Tighten Co.](https://tighten.co)**
|
||||||
|
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||||
|
- **[64 Robots](https://64robots.com)**
|
||||||
|
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
||||||
|
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||||
|
- **[Redberry](https://redberry.international/laravel-development)**
|
||||||
|
- **[Active Logic](https://activelogic.com)**
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||||
|
|
||||||
|
## Security Vulnerabilities
|
||||||
|
|
||||||
|
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Konsultasi;
|
||||||
|
use App\Models\Penyakit;
|
||||||
|
use App\Models\Gejala;
|
||||||
|
use App\Models\Rule;
|
||||||
|
use App\Models\HasilDiagnosa;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$totalKonsultasi = Konsultasi::count();
|
||||||
|
$konsultasiHariIni = Konsultasi::whereDate('tanggal', today())->count();
|
||||||
|
$totalPenyakit = Penyakit::count();
|
||||||
|
$totalHama = Penyakit::where('jenis', 'Hama')->count();
|
||||||
|
$totalPenyakitOnly = Penyakit::where('jenis', 'Penyakit')->count();
|
||||||
|
$totalGejala = Gejala::count();
|
||||||
|
$totalRule = Rule::count();
|
||||||
|
$totalTerdeteksi = Konsultasi::where('status', 'selesai')->count();
|
||||||
|
$totalTidakTerdeteksi = Konsultasi::where('status', 'tidak_terdeteksi')->count();
|
||||||
|
|
||||||
|
// Penyakit paling sering menjadi diagnosa utama
|
||||||
|
$penyakitTerbanyak = HasilDiagnosa::select('id_penyakit', DB::raw('count(*) as total'))
|
||||||
|
->with('penyakit')
|
||||||
|
->where('ranking', 1)
|
||||||
|
->groupBy('id_penyakit')
|
||||||
|
->orderByDesc('total')
|
||||||
|
->limit(6)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// 6 konsultasi terbaru
|
||||||
|
$konsultasiTerbaru = Konsultasi::with(['hasilUtama.penyakit', 'user'])
|
||||||
|
->latest('tanggal')
|
||||||
|
->limit(6)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Konsultasi per bulan (tahun ini) — untuk grafik
|
||||||
|
$konsultasiPerBulanRaw = Konsultasi::selectRaw('MONTH(tanggal) as bulan, COUNT(*) as total')
|
||||||
|
->whereYear('tanggal', now()->year)
|
||||||
|
->groupBy('bulan')
|
||||||
|
->orderBy('bulan')
|
||||||
|
->pluck('total', 'bulan');
|
||||||
|
|
||||||
|
// Isi array 12 bulan, bulan yang kosong = 0
|
||||||
|
$konsultasiPerBulan = collect(range(1, 12))->map(fn($b) => $konsultasiPerBulanRaw[$b] ?? 0)->values()->toArray();
|
||||||
|
|
||||||
|
return view('admin.dashboard', compact(
|
||||||
|
'totalKonsultasi', 'konsultasiHariIni',
|
||||||
|
'totalPenyakit', 'totalHama', 'totalPenyakitOnly',
|
||||||
|
'totalGejala', 'totalRule',
|
||||||
|
'totalTerdeteksi', 'totalTidakTerdeteksi',
|
||||||
|
'penyakitTerbanyak', 'konsultasiTerbaru',
|
||||||
|
'konsultasiPerBulan'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Gejala;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class GejalaController extends Controller
|
||||||
|
{
|
||||||
|
// Tampilkan daftar semua gejala
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$gejala = Gejala::with('rules.penyakit')
|
||||||
|
->when(request('search'), fn($q) =>
|
||||||
|
$q->where('nama', 'like', '%' . request('search') . '%')
|
||||||
|
->orWhere('kode', 'like', '%' . request('search') . '%')
|
||||||
|
)
|
||||||
|
->orderBy('kode')
|
||||||
|
->paginate(20);
|
||||||
|
|
||||||
|
return view('admin.gejala.index', compact('gejala'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan form tambah gejala
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('admin.gejala.form');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan gejala baru
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'kode' => 'required|unique:tbl_gejala,kode|max:10',
|
||||||
|
'nama' => 'required|max:255',
|
||||||
|
'bagian' => 'required|in:daun,batang,seluruh,buah',
|
||||||
|
], [
|
||||||
|
'kode.unique' => 'Kode gejala sudah digunakan.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Gejala::create([
|
||||||
|
'kode' => strtoupper($request->kode),
|
||||||
|
'nama' => $request->nama,
|
||||||
|
'deskripsi' => $request->deskripsi,
|
||||||
|
'bagian' => $request->bagian,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.gejala.index')
|
||||||
|
->with('success', 'Gejala "' . $request->kode . '" berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan form edit gejala
|
||||||
|
public function edit(Gejala $gejala)
|
||||||
|
{
|
||||||
|
return view('admin.gejala.form', compact('gejala'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update data gejala
|
||||||
|
public function update(Request $request, Gejala $gejala)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'nama' => 'required|max:255',
|
||||||
|
'bagian' => 'required|in:daun,batang,seluruh,buah',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gejala->update([
|
||||||
|
'nama' => $request->nama,
|
||||||
|
'deskripsi' => $request->deskripsi,
|
||||||
|
'bagian' => $request->bagian,
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.gejala.index')
|
||||||
|
->with('success', 'Gejala "' . $gejala->kode . '" berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus gejala beserta rule yang menggunakannya
|
||||||
|
public function destroy(Gejala $gejala)
|
||||||
|
{
|
||||||
|
$kode = $gejala->kode;
|
||||||
|
$gejala->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.gejala.index')
|
||||||
|
->with('success', 'Gejala "' . $kode . '" berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Konsultasi;
|
||||||
|
use App\Services\CertaintyFactorService;
|
||||||
|
|
||||||
|
class KonsultasiController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private CertaintyFactorService $cfService) {}
|
||||||
|
|
||||||
|
// Tampilkan semua riwayat konsultasi
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$konsultasi = Konsultasi::with(['hasilUtama.penyakit', 'hasilDiagnosa', 'user'])
|
||||||
|
->when(request('search'), fn($q) =>
|
||||||
|
$q->where('kode_konsultasi', 'like', '%' . request('search') . '%')
|
||||||
|
)
|
||||||
|
->latest('tanggal')
|
||||||
|
->paginate(15);
|
||||||
|
|
||||||
|
return view('admin.konsultasi.index', compact('konsultasi'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan detail satu konsultasi beserta log perhitungan CF
|
||||||
|
public function show(int $id)
|
||||||
|
{
|
||||||
|
$konsultasi = Konsultasi::with([
|
||||||
|
'hasilDiagnosa.penyakit.solusi',
|
||||||
|
'gejalaList.gejala',
|
||||||
|
'user',
|
||||||
|
])->findOrFail($id);
|
||||||
|
|
||||||
|
// Hitung ulang untuk mendapatkan detail log CF
|
||||||
|
$gejalaIds = $konsultasi->gejalaList->pluck('id_gejala')->toArray();
|
||||||
|
$hasilInfer = $this->cfService->hitung($gejalaIds);
|
||||||
|
|
||||||
|
return view('admin.konsultasi.show', compact('konsultasi', 'hasilInfer'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Penyakit;
|
||||||
|
use App\Models\Solusi;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class PenyakitController extends Controller
|
||||||
|
{
|
||||||
|
// Tampilkan daftar semua penyakit
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$penyakit = Penyakit::withCount('rules')
|
||||||
|
->when(request('search'), fn($q) =>
|
||||||
|
$q->where('nama', 'like', '%' . request('search') . '%')
|
||||||
|
)
|
||||||
|
->orderBy('kode')
|
||||||
|
->paginate(15);
|
||||||
|
|
||||||
|
return view('admin.penyakit.index', compact('penyakit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan form tambah penyakit
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('admin.penyakit.form');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan penyakit baru ke database
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'kode' => 'required|unique:tbl_penyakit,kode|max:10',
|
||||||
|
'nama' => 'required|max:150',
|
||||||
|
'jenis' => 'required|in:Hama,Penyakit',
|
||||||
|
], [
|
||||||
|
'kode.unique' => 'Kode penyakit sudah digunakan.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gambarPath = null;
|
||||||
|
if ($request->hasFile('gambar')) {
|
||||||
|
$gambarPath = $request->file('gambar')->store('penyakit', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
$penyakit = Penyakit::create([
|
||||||
|
'kode' => strtoupper($request->kode),
|
||||||
|
'nama' => $request->nama,
|
||||||
|
'jenis' => $request->jenis,
|
||||||
|
'penyebab' => $request->penyebab ?: '-',
|
||||||
|
'deskripsi' => $request->deskripsi,
|
||||||
|
'gambar' => $gambarPath, // tambah ini
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
// Simpan solusi jika diisi
|
||||||
|
if ($request->filled('solusi') || $request->filled('pencegahan')) {
|
||||||
|
Solusi::create([
|
||||||
|
'id_penyakit' => $penyakit->id,
|
||||||
|
'solusi' => $request->solusi ?: '-',
|
||||||
|
'pencegahan' => $request->pencegahan,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('admin.penyakit.index')
|
||||||
|
->with('success', 'Penyakit "' . $penyakit->nama . '" berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan form edit penyakit
|
||||||
|
public function edit(Penyakit $penyakit)
|
||||||
|
{
|
||||||
|
$penyakit->load('solusi');
|
||||||
|
return view('admin.penyakit.form', compact('penyakit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update data penyakit
|
||||||
|
public function update(Request $request, Penyakit $penyakit)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'nama' => 'required|max:150',
|
||||||
|
'jenis' => 'required|in:Hama,Penyakit',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->hasFile('gambar')) {
|
||||||
|
// Hapus gambar lama jika ada
|
||||||
|
if ($penyakit->gambar) {
|
||||||
|
\Storage::disk('public')->delete($penyakit->gambar);
|
||||||
|
}
|
||||||
|
$gambarPath = $request->file('gambar')->store('penyakit', 'public');
|
||||||
|
$penyakit->gambar = $gambarPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
$penyakit->update([
|
||||||
|
'nama' => $request->nama,
|
||||||
|
'jenis' => $request->jenis,
|
||||||
|
'penyebab' => $request->penyebab ?: '-',
|
||||||
|
'deskripsi' => $request->deskripsi,
|
||||||
|
'gambar' => $penyakit->gambar,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update atau buat solusi
|
||||||
|
Solusi::updateOrCreate(
|
||||||
|
['id_penyakit' => $penyakit->id],
|
||||||
|
[
|
||||||
|
'solusi' => $request->solusi ?: '-',
|
||||||
|
'pencegahan' => $request->pencegahan,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return redirect()->route('admin.penyakit.index')
|
||||||
|
->with('success', 'Penyakit "' . $penyakit->nama . '" berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus penyakit beserta rule dan solusinya
|
||||||
|
public function destroy(Penyakit $penyakit)
|
||||||
|
{
|
||||||
|
$nama = $penyakit->nama;
|
||||||
|
$penyakit->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.penyakit.index')
|
||||||
|
->with('success', 'Penyakit "' . $nama . '" berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Rule;
|
||||||
|
use App\Models\Penyakit;
|
||||||
|
use App\Models\Gejala;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class RuleController extends Controller
|
||||||
|
{
|
||||||
|
// Tentukan nilai pakar dari nilai CF
|
||||||
|
private function nilaiPakarDariCF(float $cf): int
|
||||||
|
{
|
||||||
|
if ($cf <= 0.2) return 1;
|
||||||
|
if ($cf <= 0.4) return 2;
|
||||||
|
if ($cf <= 0.6) return 3;
|
||||||
|
if ($cf <= 0.8) return 4;
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan daftar semua rule
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$rules = Rule::with(['penyakit', 'gejala'])
|
||||||
|
->when(request('penyakit_id'), fn($q) =>
|
||||||
|
$q->where('id_penyakit', request('penyakit_id'))
|
||||||
|
)
|
||||||
|
->when(request('search'), fn($q) =>
|
||||||
|
$q->where('kode_rule', 'like', '%' . request('search') . '%')
|
||||||
|
)
|
||||||
|
->orderBy('kode_rule')
|
||||||
|
->paginate(15);
|
||||||
|
|
||||||
|
$semuaPenyakit = Penyakit::orderBy('kode')->get();
|
||||||
|
|
||||||
|
return view('admin.rule.index', compact('rules', 'semuaPenyakit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan form tambah rule
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$penyakit = Penyakit::orderBy('kode')->get();
|
||||||
|
$gejala = Gejala::orderBy('kode')->get();
|
||||||
|
return view('admin.rule.form', compact('penyakit', 'gejala'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan rule baru
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'id_penyakit' => 'required|exists:tbl_penyakit,id',
|
||||||
|
'id_gejala' => 'required|exists:tbl_gejala,id',
|
||||||
|
'nilai_mb' => 'required|numeric|min:0|max:1',
|
||||||
|
'nilai_md' => 'required|numeric|min:0|max:1',
|
||||||
|
], [
|
||||||
|
'id_penyakit.required' => 'Pilih penyakit terlebih dahulu.',
|
||||||
|
'id_gejala.required' => 'Pilih gejala terlebih dahulu.',
|
||||||
|
'nilai_mb.required' => 'Nilai MB wajib diisi.',
|
||||||
|
'nilai_md.required' => 'Nilai MD wajib diisi.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Cek kombinasi penyakit-gejala sudah ada
|
||||||
|
$sudahAda = Rule::where('id_penyakit', $request->id_penyakit)
|
||||||
|
->where('id_gejala', $request->id_gejala)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($sudahAda) {
|
||||||
|
return back()->withErrors(['id_gejala' => 'Rule untuk kombinasi penyakit dan gejala ini sudah ada.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hitung CF dan nilai pakar otomatis
|
||||||
|
$mb = (float) $request->nilai_mb;
|
||||||
|
$md = (float) $request->nilai_md;
|
||||||
|
$cf = round($mb - $md, 4);
|
||||||
|
$pakar = $this->nilaiPakarDariCF($cf);
|
||||||
|
|
||||||
|
// Auto-generate kode rule jika kosong
|
||||||
|
$kodeRule = $request->kode_rule;
|
||||||
|
if (empty($kodeRule)) {
|
||||||
|
$lastRule = Rule::orderByDesc('id')->first();
|
||||||
|
$nextNum = $lastRule ? (intval(substr($lastRule->kode_rule, 1)) + 1) : 1;
|
||||||
|
$kodeRule = 'R' . str_pad($nextNum, 3, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Rule::create([
|
||||||
|
'kode_rule' => strtoupper($kodeRule),
|
||||||
|
'id_penyakit' => $request->id_penyakit,
|
||||||
|
'id_gejala' => $request->id_gejala,
|
||||||
|
'nilai_mb' => $mb,
|
||||||
|
'nilai_md' => $md,
|
||||||
|
'nilai_cf' => $cf,
|
||||||
|
'nilai_pakar' => $pakar,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.rule.index')
|
||||||
|
->with('success', 'Rule "' . $kodeRule . '" berhasil ditambahkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan form edit rule
|
||||||
|
public function edit(Rule $rule)
|
||||||
|
{
|
||||||
|
$penyakit = Penyakit::orderBy('kode')->get();
|
||||||
|
$gejala = Gejala::orderBy('kode')->get();
|
||||||
|
return view('admin.rule.form', compact('rule', 'penyakit', 'gejala'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update rule
|
||||||
|
public function update(Request $request, Rule $rule)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'id_penyakit' => 'required|exists:tbl_penyakit,id',
|
||||||
|
'id_gejala' => 'required|exists:tbl_gejala,id',
|
||||||
|
'nilai_mb' => 'required|numeric|min:0|max:1',
|
||||||
|
'nilai_md' => 'required|numeric|min:0|max:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Hitung CF dan nilai pakar otomatis
|
||||||
|
$mb = (float) $request->nilai_mb;
|
||||||
|
$md = (float) $request->nilai_md;
|
||||||
|
$cf = round($mb - $md, 4);
|
||||||
|
$pakar = $this->nilaiPakarDariCF($cf);
|
||||||
|
|
||||||
|
$rule->update([
|
||||||
|
'id_penyakit' => $request->id_penyakit,
|
||||||
|
'id_gejala' => $request->id_gejala,
|
||||||
|
'nilai_mb' => $mb,
|
||||||
|
'nilai_md' => $md,
|
||||||
|
'nilai_cf' => $cf,
|
||||||
|
'nilai_pakar' => $pakar,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.rule.index')
|
||||||
|
->with('success', 'Rule "' . $rule->kode_rule . '" berhasil diperbarui.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus rule
|
||||||
|
public function destroy(Rule $rule)
|
||||||
|
{
|
||||||
|
$kode = $rule->kode_rule;
|
||||||
|
$rule->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.rule.index')
|
||||||
|
->with('success', 'Rule "' . $kode . '" berhasil dihapus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Auth\LoginRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AuthenticatedSessionController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the login view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.login');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming authentication request.
|
||||||
|
*/
|
||||||
|
public function store(LoginRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->authenticate();
|
||||||
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
if (!auth()->check()) {
|
||||||
|
return back()->withErrors(['email' => 'Login gagal']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect berdasarkan role
|
||||||
|
if (auth()->user()->role === 'admin') {
|
||||||
|
return redirect('/admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect('/konsultasi');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy an authenticated session.
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
Auth::guard('web')->logout();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ConfirmablePasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show the confirm password view.
|
||||||
|
*/
|
||||||
|
public function show(): View
|
||||||
|
{
|
||||||
|
return view('auth.confirm-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm the user's password.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if (! Auth::guard('web')->validate([
|
||||||
|
'email' => $request->user()->email,
|
||||||
|
'password' => $request->password,
|
||||||
|
])) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'password' => __('auth.password'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->session()->put('auth.password_confirmed_at', time());
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class EmailVerificationNotificationController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Send a new email verification notification.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($request->user()->hasVerifiedEmail()) {
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->user()->sendEmailVerificationNotification();
|
||||||
|
|
||||||
|
return back()->with('status', 'verification-link-sent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class EmailVerificationPromptController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the email verification prompt.
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request): RedirectResponse|View
|
||||||
|
{
|
||||||
|
return $request->user()->hasVerifiedEmail()
|
||||||
|
? redirect()->intended(route('dashboard', absolute: false))
|
||||||
|
: view('auth.verify-email');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Events\PasswordReset;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class NewPasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the password reset view.
|
||||||
|
*/
|
||||||
|
public function create(Request $request): View
|
||||||
|
{
|
||||||
|
return view('auth.reset-password', ['request' => $request]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming new password request.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'token' => ['required'],
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Here we will attempt to reset the user's password. If it is successful we
|
||||||
|
// will update the password on an actual user model and persist it to the
|
||||||
|
// database. Otherwise we will parse the error and return the response.
|
||||||
|
$status = Password::reset(
|
||||||
|
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||||
|
function (User $user) use ($request) {
|
||||||
|
$user->forceFill([
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
'remember_token' => Str::random(60),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
event(new PasswordReset($user));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the password was successfully reset, we will redirect the user back to
|
||||||
|
// the application's home authenticated view. If there is an error we can
|
||||||
|
// redirect them back to where they came from with their error message.
|
||||||
|
return $status == Password::PASSWORD_RESET
|
||||||
|
? redirect()->route('login')->with('status', __($status))
|
||||||
|
: back()->withInput($request->only('email'))
|
||||||
|
->withErrors(['email' => __($status)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class PasswordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Update the user's password.
|
||||||
|
*/
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validateWithBag('updatePassword', [
|
||||||
|
'current_password' => ['required', 'current_password'],
|
||||||
|
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request->user()->update([
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('status', 'password-updated');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class PasswordResetLinkController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the password reset link request view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.forgot-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming password reset link request.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// We will send the password reset link to this user. Once we have attempted
|
||||||
|
// to send the link, we will examine the response then see the message we
|
||||||
|
// need to show to the user. Finally, we'll send out a proper response.
|
||||||
|
$status = Password::sendResetLink(
|
||||||
|
$request->only('email')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $status == Password::RESET_LINK_SENT
|
||||||
|
? back()->with('status', __($status))
|
||||||
|
: back()->withInput($request->only('email'))
|
||||||
|
->withErrors(['email' => __($status)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Events\Registered;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class RegisteredUserController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the registration view.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('auth.register');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming registration request.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||||
|
'no_telp' => ['nullable', 'string', 'max:15'],
|
||||||
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $request->name,
|
||||||
|
'email' => $request->email,
|
||||||
|
'no_telp' => $request->no_telp,
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
'role' => 'user',
|
||||||
|
]);
|
||||||
|
|
||||||
|
event(new Registered($user));
|
||||||
|
|
||||||
|
Auth::login($user);
|
||||||
|
|
||||||
|
return redirect(route('konsultasi.index', absolute: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Auth\Events\Verified;
|
||||||
|
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
class VerifyEmailController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Mark the authenticated user's email address as verified.
|
||||||
|
*/
|
||||||
|
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($request->user()->hasVerifiedEmail()) {
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->user()->markEmailAsVerified()) {
|
||||||
|
event(new Verified($request->user()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
// app/Http/Controllers/DiagnosaController.php
|
||||||
|
//
|
||||||
|
// KEGUNAAN: Controller untuk halaman konsultasi user.
|
||||||
|
// Menangani 3 halaman utama:
|
||||||
|
// 1. Tampilkan form checklist gejala (/konsultasi)
|
||||||
|
// 2. Proses diagnosa CF dan simpan hasil (/konsultasi/proses)
|
||||||
|
// 3. Tampilkan hasil diagnosa (/hasil/{id})
|
||||||
|
// 4. Tampilkan riwayat konsultasi (/riwayat)
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Gejala;
|
||||||
|
use App\Models\Konsultasi;
|
||||||
|
use App\Services\CertaintyFactorService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class DiagnosaController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private CertaintyFactorService $cfService) {}
|
||||||
|
|
||||||
|
// ── GET /konsultasi ───────────────────────────────────────
|
||||||
|
// Menampilkan halaman checklist gejala
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$semuaGejala = Gejala::orderBy('kode')->get();
|
||||||
|
|
||||||
|
// G01-G30 = Hama, G31-G60 = Penyakit
|
||||||
|
$gejalaHama = $semuaGejala->filter(fn($g) => (int) substr($g->kode, 1) <= 30);
|
||||||
|
$gejalaPenyakit = $semuaGejala->filter(fn($g) => (int) substr($g->kode, 1) > 30);
|
||||||
|
|
||||||
|
return view('user.konsultasi', compact('semuaGejala', 'gejalaHama', 'gejalaPenyakit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── POST /konsultasi/proses ───────────────────────────────
|
||||||
|
// Memproses gejala yang dipilih, menjalankan CF, simpan hasil
|
||||||
|
public function proses(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'gejala' => ['required', 'array', 'min:1'],
|
||||||
|
'gejala.*' => ['integer', 'exists:tbl_gejala,id'],
|
||||||
|
], [
|
||||||
|
'gejala.required' => 'Pilih minimal 1 gejala terlebih dahulu.',
|
||||||
|
'gejala.min' => 'Pilih minimal 1 gejala terlebih dahulu.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gejalaIds = array_map('intval', $request->input('gejala'));
|
||||||
|
$cfUserMap = [];
|
||||||
|
|
||||||
|
// Jalankan mesin inferensi CF
|
||||||
|
$hasil = $this->cfService->hitung($gejalaIds, $cfUserMap);
|
||||||
|
|
||||||
|
if (isset($hasil['error'])) {
|
||||||
|
return back()->withErrors(['gejala' => $hasil['error']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gunakan user_id login, fallback ke 1 jika belum login
|
||||||
|
$userId = auth()->id() ?? 1;
|
||||||
|
|
||||||
|
$konsultasi = $this->cfService->simpan($userId, $gejalaIds, $cfUserMap, $hasil);
|
||||||
|
|
||||||
|
return redirect()->route('hasil.show', $konsultasi->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GET /hasil/{id} ───────────────────────────────────────
|
||||||
|
// Menampilkan halaman hasil diagnosa
|
||||||
|
public function hasilShow(int $id)
|
||||||
|
{
|
||||||
|
$konsultasi = Konsultasi::with([
|
||||||
|
'hasilDiagnosa.penyakit.solusi',
|
||||||
|
'gejalaList.gejala',
|
||||||
|
])->findOrFail($id);
|
||||||
|
|
||||||
|
// Jika sudah login, pastikan hanya bisa lihat hasil sendiri
|
||||||
|
if (auth()->check() && $konsultasi->user_id !== auth()->id()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$gejalaIds = $konsultasi->gejalaList->pluck('id_gejala')->toArray();
|
||||||
|
$hasilInfer = $this->cfService->hitung($gejalaIds);
|
||||||
|
|
||||||
|
return view('user.hasil', compact('konsultasi', 'hasilInfer'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GET /riwayat ──────────────────────────────────────────
|
||||||
|
// Menampilkan riwayat konsultasi milik user yang login
|
||||||
|
public function riwayat()
|
||||||
|
{
|
||||||
|
$query = auth()->check()
|
||||||
|
? Konsultasi::where('user_id', auth()->id())
|
||||||
|
: Konsultasi::query();
|
||||||
|
|
||||||
|
$riwayat = $query
|
||||||
|
->with('hasilUtama.penyakit')
|
||||||
|
->latest('tanggal')
|
||||||
|
->paginate(10);
|
||||||
|
|
||||||
|
return view('user.riwayat', compact('riwayat'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Penyakit;
|
||||||
|
use App\Models\Gejala;
|
||||||
|
use App\Models\Rule;
|
||||||
|
use App\Models\Konsultasi;
|
||||||
|
|
||||||
|
class LandingController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$totalPenyakit = Penyakit::count();
|
||||||
|
$totalGejala = Gejala::count();
|
||||||
|
$totalRule = Rule::count();
|
||||||
|
$totalKonsultasi = Konsultasi::count();
|
||||||
|
|
||||||
|
// Ambil 6 penyakit untuk ditampilkan di landing page
|
||||||
|
$penyakitList = Penyakit::withCount('rules')
|
||||||
|
->orderBy('kode')
|
||||||
|
->limit(6)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('landing', compact(
|
||||||
|
'totalPenyakit',
|
||||||
|
'totalGejala',
|
||||||
|
'totalRule',
|
||||||
|
'totalKonsultasi',
|
||||||
|
'penyakitList'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
// app/Http/Controllers/PdfController.php
|
||||||
|
//
|
||||||
|
// KEGUNAAN: Menghasilkan file PDF dari hasil diagnosa.
|
||||||
|
// User bisa download PDF dari halaman hasil diagnosa
|
||||||
|
// maupun dari halaman riwayat konsultasi.
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Konsultasi;
|
||||||
|
use App\Services\CertaintyFactorService;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
|
||||||
|
class PdfController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private CertaintyFactorService $cfService) {}
|
||||||
|
|
||||||
|
// ── Download PDF hasil diagnosa ───────────────────────────
|
||||||
|
public function download(int $id)
|
||||||
|
{
|
||||||
|
$konsultasi = Konsultasi::with([
|
||||||
|
'hasilDiagnosa.penyakit.solusi',
|
||||||
|
'gejalaList.gejala',
|
||||||
|
'user',
|
||||||
|
])->findOrFail($id);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Pastikan hanya pemilik yang bisa download
|
||||||
|
if (auth()->check() && $konsultasi->user_id != auth()->id()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
// Hitung ulang untuk log CF
|
||||||
|
$gejalaIds = $konsultasi->gejalaList->pluck('id_gejala')->toArray();
|
||||||
|
$hasilInfer = $this->cfService->hitung($gejalaIds);
|
||||||
|
|
||||||
|
$pdf = Pdf::loadView('pdf.hasil-diagnosa', compact('konsultasi', 'hasilInfer'))
|
||||||
|
->setPaper('a4', 'portrait')
|
||||||
|
->setOptions([
|
||||||
|
'defaultFont' => 'sans-serif',
|
||||||
|
'isHtml5ParserEnabled' => true,
|
||||||
|
'isRemoteEnabled' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$filename = 'RedGuard-' . $konsultasi->kode_konsultasi . '.pdf';
|
||||||
|
|
||||||
|
return $pdf->download($filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\ProfileUpdateRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Redirect;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ProfileController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the user's profile form.
|
||||||
|
*/
|
||||||
|
public function edit(Request $request): View
|
||||||
|
{
|
||||||
|
return view('profile.edit', [
|
||||||
|
'user' => $request->user(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's profile information.
|
||||||
|
*/
|
||||||
|
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->user()->fill($request->validated());
|
||||||
|
|
||||||
|
if ($request->user()->isDirty('email')) {
|
||||||
|
$request->user()->email_verified_at = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->user()->save();
|
||||||
|
|
||||||
|
return Redirect::route('profile.edit')->with('status', 'profile-updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the user's account.
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validateWithBag('userDeletion', [
|
||||||
|
'password' => ['required', 'current_password'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
Auth::logout();
|
||||||
|
|
||||||
|
$user->delete();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return Redirect::to('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\User;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Konsultasi;
|
||||||
|
use App\Models\Gejala;
|
||||||
|
use App\Models\Rule;
|
||||||
|
use App\Models\HasilDiagnosa;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class KonsultasiController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Gejala dikelompokkan berdasarkan bagian tanaman
|
||||||
|
// Sesuaikan kolom 'bagian' dengan struktur tabel gejala kamu
|
||||||
|
// Jika tidak ada kolom bagian, ganti dengan logika lain
|
||||||
|
|
||||||
|
$gejalaDaun = Gejala::where('bagian', 'daun')->orderBy('nama')->get();
|
||||||
|
$gejalaBatang = Gejala::where('bagian', 'batang')->orderBy('nama')->get();
|
||||||
|
$gejalaLain = Gejala::where('bagian', 'seluruh')->orWhereNull('bagian')->orderBy('nama')->get();
|
||||||
|
$gejalaBuah = Gejala::where('bagian', 'buah')->orderBy('nama')->get();
|
||||||
|
|
||||||
|
return view('user.index', compact('gejalaDaun', 'gejalaBatang', 'gejalaLain', 'gejalaBuah')); }
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'gejala' => ['required', 'array', 'min:1'],
|
||||||
|
'gejala.*' => ['exists:tbl_gejala,id'],
|
||||||
|
'cf_user' => ['required', 'array'],
|
||||||
|
'cf_user.*' => ['required', 'numeric', 'min:0.1', 'max:1'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gejalaIds = array_map('intval', $request->gejala);
|
||||||
|
$cfUserInput = [];
|
||||||
|
foreach ($request->cf_user as $k => $v) {
|
||||||
|
$cfUserInput[(int)$k] = (float)$v;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules = Rule::whereIn('id_gejala', $gejalaIds)->with('penyakit')->get();
|
||||||
|
|
||||||
|
// CF combine per penyakit
|
||||||
|
$cfPerPenyakit = [];
|
||||||
|
|
||||||
|
foreach ($rules as $rule) {
|
||||||
|
$idPenyakit = $rule->id_penyakit;
|
||||||
|
$idGejala = $rule->id_gejala;
|
||||||
|
$cfUser = $cfUserInput[(int)$idGejala] ?? 0.5;
|
||||||
|
$cfPakar = $rule->nilai_cf;
|
||||||
|
|
||||||
|
// CF gejala = CF user × CF pakar
|
||||||
|
$cfGejala = $cfUser * $cfPakar;
|
||||||
|
|
||||||
|
if (!isset($cfPerPenyakit[$idPenyakit])) {
|
||||||
|
$cfPerPenyakit[$idPenyakit] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CF combine = CF old + CF gejala × (1 - CF old)
|
||||||
|
$cfOld = $cfPerPenyakit[$idPenyakit];
|
||||||
|
$cfPerPenyakit[$idPenyakit] = $cfOld + $cfGejala * (1 - $cfOld);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buat record konsultasi
|
||||||
|
$konsultasi = Konsultasi::create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'kode_konsultasi' => 'KST-' . strtoupper(Str::random(8)),
|
||||||
|
'tanggal' => now(),
|
||||||
|
'status' => count($cfPerPenyakit) > 0 ? 'selesai' : 'tidak_terdeteksi',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Simpan gejala
|
||||||
|
$attachData = [];
|
||||||
|
foreach ($gejalaIds as $idGejala) {
|
||||||
|
$attachData[$idGejala] = [
|
||||||
|
'cf_user' => $cfUserInput[(int)$idGejala] ?? 0.5
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$konsultasi->gejala()->attach($attachData);
|
||||||
|
|
||||||
|
// Simpan hasil diagnosa
|
||||||
|
arsort($cfPerPenyakit);
|
||||||
|
$ranking = 1;
|
||||||
|
foreach ($cfPerPenyakit as $idPenyakit => $cf) {
|
||||||
|
if ($cf <= 0) continue;
|
||||||
|
HasilDiagnosa::create([
|
||||||
|
'id_konsultasi' => $konsultasi->id,
|
||||||
|
'id_penyakit' => $idPenyakit,
|
||||||
|
'nilai_cf_akhir' => round($cf, 4),
|
||||||
|
'persentase' => round($cf * 100, 2),
|
||||||
|
'ranking' => $ranking++,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('konsultasi.show', $konsultasi->id)
|
||||||
|
->with('success', 'Diagnosa berhasil! Berikut hasil analisis tanamanmu.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
$konsultasi = Konsultasi::with([
|
||||||
|
'hasilDiagnosa.penyakit',
|
||||||
|
'hasilUtama.penyakit',
|
||||||
|
'gejala',
|
||||||
|
])->where('user_id', auth()->id())
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
return view('user.show', compact('konsultasi')); }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\User;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use App\Models\Konsultasi;
|
||||||
|
|
||||||
|
class ProfilController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$totalDiagnosa = Konsultasi::where('user_id', $user->id)->count();
|
||||||
|
|
||||||
|
$bulanIni = Konsultasi::where('user_id', $user->id)
|
||||||
|
->whereMonth('tanggal', now()->month)
|
||||||
|
->whereYear('tanggal', now()->year)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return view('user.profil', compact('totalDiagnosa', 'bulanIni'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'no_telp' => ['nullable', 'string', 'max:15'],
|
||||||
|
'foto' => ['nullable', 'image', 'mimes:jpeg,png,jpg', 'max:2048'],
|
||||||
|
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update foto jika ada
|
||||||
|
if ($request->hasFile('foto')) {
|
||||||
|
// Hapus foto lama jika ada
|
||||||
|
if ($user->foto) {
|
||||||
|
Storage::disk('public')->delete($user->foto);
|
||||||
|
}
|
||||||
|
$path = $request->file('foto')->store('foto-profil', 'public');
|
||||||
|
$user->foto = $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->name = $request->name;
|
||||||
|
$user->no_telp = $request->no_telp;
|
||||||
|
|
||||||
|
// Update password jika diisi
|
||||||
|
if ($request->filled('password')) {
|
||||||
|
$user->password = Hash::make($request->password);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return redirect()->route('user.profil')
|
||||||
|
->with('success', 'Profil berhasil diperbarui!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\User;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Konsultasi;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class RiwayatController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$riwayat = Konsultasi::with(['hasilUtama.penyakit', 'gejala'])
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->latest('tanggal')
|
||||||
|
->paginate(10);
|
||||||
|
|
||||||
|
$totalKonsultasi = Konsultasi::where('user_id', $user->id)->count();
|
||||||
|
|
||||||
|
$totalTerdeteksi = Konsultasi::where('user_id', $user->id)
|
||||||
|
->where('status', 'selesai')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$bulanIni = Konsultasi::where('user_id', $user->id)
|
||||||
|
->whereMonth('tanggal', now()->month)
|
||||||
|
->whereYear('tanggal', now()->year)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return view('user.riwayat', compact(
|
||||||
|
'riwayat',
|
||||||
|
'totalKonsultasi',
|
||||||
|
'totalTerdeteksi',
|
||||||
|
'bulanIni'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class RoleMiddleware
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next, string $role): Response
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!auth()->check()) {
|
||||||
|
return redirect()->route('login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth()->user()->role !== $role) {
|
||||||
|
abort(403, 'Akses ditolak. Anda tidak memiliki izin untuk halaman ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Events\Lockout;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class LoginRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email' => ['required', 'string', 'email'],
|
||||||
|
'password' => ['required', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to authenticate the request's credentials.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function authenticate(): void
|
||||||
|
{
|
||||||
|
$this->ensureIsNotRateLimited();
|
||||||
|
|
||||||
|
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||||
|
RateLimiter::hit($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => trans('auth.failed'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
RateLimiter::clear($this->throttleKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the login request is not rate limited.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function ensureIsNotRateLimited(): void
|
||||||
|
{
|
||||||
|
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event(new Lockout($this));
|
||||||
|
|
||||||
|
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => trans('auth.throttle', [
|
||||||
|
'seconds' => $seconds,
|
||||||
|
'minutes' => ceil($seconds / 60),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rate limiting throttle key for the request.
|
||||||
|
*/
|
||||||
|
public function throttleKey(): string
|
||||||
|
{
|
||||||
|
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ProfileUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'lowercase',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique(User::class)->ignore($this->user()->id),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
// app/Models/Gejala.php
|
||||||
|
//
|
||||||
|
// KEGUNAAN: Model untuk tabel tbl_gejala.
|
||||||
|
// Menyimpan 60 gejala yang ditampilkan sebagai checklist
|
||||||
|
// di halaman konsultasi user.
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Gejala extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'tbl_gejala';
|
||||||
|
protected $fillable = ['kode', 'nama', 'deskripsi', 'bagian'];
|
||||||
|
|
||||||
|
// Satu gejala bisa dipakai di banyak rule
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Rule::class, 'id_gejala');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
// app/Models/HasilDiagnosa.php
|
||||||
|
//
|
||||||
|
// KEGUNAAN: Model untuk tabel tbl_hasil_diagnosa.
|
||||||
|
// Menyimpan output dari mesin inferensi CF — penyakit apa
|
||||||
|
// yang terdeteksi, berapa nilai CF-nya, dan rankingnya.
|
||||||
|
// Satu konsultasi bisa menghasilkan beberapa baris di tabel ini
|
||||||
|
// (satu per penyakit yang CF-nya >= 0.2).
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class HasilDiagnosa extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'tbl_hasil_diagnosa';
|
||||||
|
protected $fillable = ['id_konsultasi', 'id_penyakit', 'nilai_cf_akhir', 'persentase', 'ranking'];
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
// Penyakit apa yang terdeteksi
|
||||||
|
public function penyakit()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Penyakit::class, 'id_penyakit');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hasil ini dari konsultasi mana
|
||||||
|
public function konsultasi()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Konsultasi::class, 'id_konsultasi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
// app/Models/Konsultasi.php
|
||||||
|
//
|
||||||
|
// KEGUNAAN: Model untuk tabel tbl_konsultasi.
|
||||||
|
// Menyimpan setiap sesi konsultasi user — kapan dilakukan,
|
||||||
|
// siapa yang melakukan, dan apa statusnya (selesai/tidak terdeteksi).
|
||||||
|
// Satu konsultasi = satu kali user mengisi checklist gejala.
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Models\Gejala;
|
||||||
|
|
||||||
|
class Konsultasi extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'tbl_konsultasi';
|
||||||
|
protected $fillable = ['kode_konsultasi', 'user_id', 'tanggal', 'status'];
|
||||||
|
|
||||||
|
// Konsultasi ini dilakukan oleh user mana
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gejala apa saja yang dipilih dalam konsultasi ini
|
||||||
|
public function gejalaList()
|
||||||
|
{
|
||||||
|
return $this->hasMany(KonsultasiGejala::class, 'id_konsultasi');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semua hasil diagnosa dari konsultasi ini (diurutkan ranking)
|
||||||
|
public function hasilDiagnosa()
|
||||||
|
{
|
||||||
|
return $this->hasMany(HasilDiagnosa::class, 'id_konsultasi')->orderBy('ranking');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function gejala()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Gejala::class, 'tbl_konsultasi_gejala', 'id_konsultasi', 'id_gejala');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hanya hasil utama (ranking #1 / tertinggi)
|
||||||
|
public function hasilUtama()
|
||||||
|
{
|
||||||
|
return $this->hasOne(HasilDiagnosa::class, 'id_konsultasi')->where('ranking', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
// app/Models/KonsultasiGejala.php
|
||||||
|
//
|
||||||
|
// KEGUNAAN: Model untuk tabel tbl_konsultasi_gejala.
|
||||||
|
// Menyimpan detail gejala yang dipilih user dalam satu sesi
|
||||||
|
// konsultasi. Satu baris = satu gejala yang dicentang user.
|
||||||
|
// Nilai cf_user = 1.0 (mode checklist biasa ya/tidak).
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class KonsultasiGejala extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'tbl_konsultasi_gejala';
|
||||||
|
protected $fillable = ['id_konsultasi', 'id_gejala', 'cf_user'];
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
// Gejala apa yang dipilih
|
||||||
|
public function gejala()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Gejala::class, 'id_gejala');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Konsultasi mana yang memiliki gejala ini
|
||||||
|
public function konsultasi()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Konsultasi::class, 'id_konsultasi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
// app/Models/Penyakit.php
|
||||||
|
//
|
||||||
|
// KEGUNAAN: Model untuk tabel tbl_penyakit.
|
||||||
|
// Menyimpan data 12 penyakit/hama beserta relasinya ke
|
||||||
|
// tabel rule, solusi, dan hasil diagnosa.
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Penyakit extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'tbl_penyakit';
|
||||||
|
protected $fillable = ['kode', 'nama', 'jenis', 'penyebab', 'deskripsi', 'gambar'];
|
||||||
|
|
||||||
|
// Satu penyakit punya banyak rule
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Rule::class, 'id_penyakit');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Satu penyakit punya satu solusi
|
||||||
|
public function solusi()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Solusi::class, 'id_penyakit');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Satu penyakit bisa muncul di banyak hasil diagnosa
|
||||||
|
public function hasilDiagnosa()
|
||||||
|
{
|
||||||
|
return $this->hasMany(HasilDiagnosa::class, 'id_penyakit');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
// app/Models/Rule.php
|
||||||
|
//
|
||||||
|
// KEGUNAAN: Model untuk tabel tbl_rule.
|
||||||
|
// Menyimpan relasi antara penyakit dan gejala beserta
|
||||||
|
// nilai CF pakar. Inilah inti dari knowledge base sistem pakar.
|
||||||
|
// Admin bisa mengubah nilai CF lewat CRUD tanpa menyentuh kode.
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Rule extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'tbl_rule';
|
||||||
|
protected $fillable = ['kode_rule', 'id_penyakit', 'id_gejala', 'nilai_pakar', 'nilai_cf'];
|
||||||
|
|
||||||
|
// Rule ini milik penyakit mana
|
||||||
|
public function penyakit()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Penyakit::class, 'id_penyakit');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule ini menggunakan gejala mana
|
||||||
|
public function gejala()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Gejala::class, 'id_gejala');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
// app/Models/Solusi.php
|
||||||
|
//
|
||||||
|
// KEGUNAAN: Model untuk tabel tbl_solusi.
|
||||||
|
// Menyimpan solusi penanganan dan pencegahan untuk setiap
|
||||||
|
// penyakit. Ditampilkan di halaman hasil diagnosa user.
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Solusi extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'tbl_solusi';
|
||||||
|
protected $fillable = ['id_penyakit', 'solusi', 'pencegahan'];
|
||||||
|
|
||||||
|
// Solusi ini milik penyakit mana
|
||||||
|
public function penyakit()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Penyakit::class, 'id_penyakit');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Database\Factories\UserFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<UserFactory> */
|
||||||
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'no_telp',
|
||||||
|
'password',
|
||||||
|
'role',
|
||||||
|
'foto',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,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
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
<?php
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
// app/Services/CertaintyFactorService.php
|
||||||
|
//
|
||||||
|
// KEGUNAAN: Inti dari sistem pakar — mesin inferensi Certainty
|
||||||
|
// Factor. Bertugas menghitung nilai CF untuk setiap penyakit
|
||||||
|
// berdasarkan gejala yang dipilih user, lalu menyimpan hasilnya
|
||||||
|
// ke database. Semua logika perhitungan CF ada di sini,
|
||||||
|
// sehingga Controller tetap bersih dan mudah dibaca.
|
||||||
|
// ════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Rule;
|
||||||
|
use App\Models\Konsultasi;
|
||||||
|
use App\Models\KonsultasiGejala;
|
||||||
|
use App\Models\HasilDiagnosa;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class CertaintyFactorService
|
||||||
|
{
|
||||||
|
// Nilai minimum CF agar penyakit dianggap terdeteksi
|
||||||
|
private float $threshold = 0.2;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
// FORMULA CF KOMBINASI
|
||||||
|
// Rumus: CF = CF_lama + CF_baru × (1 - CF_lama)
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
private function kombinasiCF(float $cfLama, float $cfBaru): float
|
||||||
|
{
|
||||||
|
return $cfLama + $cfBaru * (1 - $cfLama);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
// INTERPRETASI NILAI CF
|
||||||
|
// Mengubah angka CF menjadi kalimat yang mudah dipahami.
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
public function interpretasi(float $cf): string
|
||||||
|
{
|
||||||
|
if ($cf >= 0.81) return 'Sangat Yakin';
|
||||||
|
if ($cf >= 0.61) return 'Yakin';
|
||||||
|
if ($cf >= 0.41) return 'Cukup Yakin';
|
||||||
|
if ($cf >= 0.21) return 'Mungkin';
|
||||||
|
if ($cf >= 0.01) return 'Tidak Yakin';
|
||||||
|
return 'Tidak Terdeteksi';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
// PROSES UTAMA: HITUNG CF
|
||||||
|
// Menerima array ID gejala yang dipilih user, lalu
|
||||||
|
// menghitung nilai CF gabungan untuk setiap penyakit.
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
public function hitung(array $gejalaIds, array $cfUserMap = []): array
|
||||||
|
{
|
||||||
|
if (empty($gejalaIds)) {
|
||||||
|
return ['error' => 'Minimal 1 gejala harus dipilih.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($gejalaIds as $id) {
|
||||||
|
if (!isset($cfUserMap[$id])) {
|
||||||
|
$cfUserMap[$id] = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ruleCocok = Rule::with(['penyakit', 'gejala'])
|
||||||
|
->whereIn('id_gejala', $gejalaIds)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($ruleCocok->isEmpty()) {
|
||||||
|
return ['error' => 'Tidak ada rule yang cocok dengan gejala yang dipilih.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasilCF = [];
|
||||||
|
$detailLog = [];
|
||||||
|
$penyakitIds = $ruleCocok->pluck('id_penyakit')->unique();
|
||||||
|
|
||||||
|
foreach ($penyakitIds as $idPenyakit) {
|
||||||
|
$rulesPerPenyakit = $ruleCocok->where('id_penyakit', $idPenyakit);
|
||||||
|
$cfGabungan = 0.0;
|
||||||
|
$log = [];
|
||||||
|
$step = 1;
|
||||||
|
|
||||||
|
foreach ($rulesPerPenyakit as $rule) {
|
||||||
|
$cfPakar = (float) $rule->nilai_cf;
|
||||||
|
$cfUser = (float) ($cfUserMap[$rule->id_gejala] ?? 1.0);
|
||||||
|
$cfIndividu = $cfPakar * $cfUser;
|
||||||
|
$cfSebelum = $cfGabungan;
|
||||||
|
|
||||||
|
if ($cfGabungan == 0.0) {
|
||||||
|
$cfGabungan = $cfIndividu;
|
||||||
|
$rumus = number_format($cfIndividu, 4);
|
||||||
|
} else {
|
||||||
|
$cfGabungan = $this->kombinasiCF($cfGabungan, $cfIndividu);
|
||||||
|
$rumus = number_format($cfSebelum, 4)
|
||||||
|
. ' + ' . number_format($cfIndividu, 4)
|
||||||
|
. ' × (1 - ' . number_format($cfSebelum, 4) . ')'
|
||||||
|
. ' = ' . number_format($cfGabungan, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
$log[] = [
|
||||||
|
'step' => $step++,
|
||||||
|
'rule' => $rule->kode_rule,
|
||||||
|
'kode_gejala' => $rule->gejala->kode ?? '-',
|
||||||
|
'nama_gejala' => $rule->gejala->nama ?? '-',
|
||||||
|
'cf_pakar' => $cfPakar,
|
||||||
|
'cf_user' => $cfUser,
|
||||||
|
'cf_individu' => round($cfIndividu, 4),
|
||||||
|
'cf_sebelum' => round($cfSebelum, 4),
|
||||||
|
'cf_sesudah' => round($cfGabungan, 4),
|
||||||
|
'rumus' => $rumus,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasilCF[$idPenyakit] = round($cfGabungan, 6);
|
||||||
|
$detailLog[$idPenyakit] = $log;
|
||||||
|
}
|
||||||
|
|
||||||
|
$terdeteksi = array_filter($hasilCF, fn($cf) => $cf >= $this->threshold);
|
||||||
|
arsort($terdeteksi);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'gejala_ids' => $gejalaIds,
|
||||||
|
'cf_user_map' => $cfUserMap,
|
||||||
|
'semua_cf' => $hasilCF,
|
||||||
|
'terdeteksi' => $terdeteksi,
|
||||||
|
'total_diperiksa' => count($hasilCF),
|
||||||
|
'total_terdeteksi' => count($terdeteksi),
|
||||||
|
'detail_log' => $detailLog,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
// SIMPAN HASIL KE DATABASE
|
||||||
|
// Menyimpan sesi konsultasi, gejala yang dipilih, dan
|
||||||
|
// hasil CF ke tiga tabel sekaligus dalam satu transaksi.
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
public function simpan(int $userId, array $gejalaIds, array $cfUserMap, array $hasilInferensi): Konsultasi
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($userId, $gejalaIds, $cfUserMap, $hasilInferensi) {
|
||||||
|
|
||||||
|
$kode = 'KON-' . date('Ymd') . '-' . str_pad(
|
||||||
|
Konsultasi::whereDate('created_at', today())->count() + 1,
|
||||||
|
4, '0', STR_PAD_LEFT
|
||||||
|
);
|
||||||
|
|
||||||
|
$status = empty($hasilInferensi['terdeteksi']) ? 'tidak_terdeteksi' : 'selesai';
|
||||||
|
|
||||||
|
$konsultasi = Konsultasi::create([
|
||||||
|
'kode_konsultasi' => $kode,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'tanggal' => now(),
|
||||||
|
'status' => $status,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($gejalaIds as $idGejala) {
|
||||||
|
KonsultasiGejala::create([
|
||||||
|
'id_konsultasi' => $konsultasi->id,
|
||||||
|
'id_gejala' => $idGejala,
|
||||||
|
'cf_user' => $cfUserMap[$idGejala] ?? 1.0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ranking = 1;
|
||||||
|
foreach ($hasilInferensi['terdeteksi'] as $idPenyakit => $cfAkhir) {
|
||||||
|
HasilDiagnosa::create([
|
||||||
|
'id_konsultasi' => $konsultasi->id,
|
||||||
|
'id_penyakit' => $idPenyakit,
|
||||||
|
'nilai_cf_akhir' => $cfAkhir,
|
||||||
|
'persentase' => round($cfAkhir * 100, 2),
|
||||||
|
'ranking' => $ranking++,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $konsultasi;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AppLayout extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represents the component.
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layouts.app');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class GuestLayout extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represents the component.
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layouts.guest');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||||
|
|
||||||
|
$status = $app->handleCommand(new ArgvInput);
|
||||||
|
|
||||||
|
exit($status);
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Auth\AuthenticatedSessionController;
|
||||||
|
use App\Http\Controllers\Auth\ConfirmablePasswordController;
|
||||||
|
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
|
||||||
|
use App\Http\Controllers\Auth\EmailVerificationPromptController;
|
||||||
|
use App\Http\Controllers\Auth\NewPasswordController;
|
||||||
|
use App\Http\Controllers\Auth\PasswordController;
|
||||||
|
use App\Http\Controllers\Auth\PasswordResetLinkController;
|
||||||
|
use App\Http\Controllers\Auth\RegisteredUserController;
|
||||||
|
use App\Http\Controllers\Auth\VerifyEmailController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::middleware('guest')->group(function () {
|
||||||
|
Route::get('register', [RegisteredUserController::class, 'create'])
|
||||||
|
->name('register');
|
||||||
|
|
||||||
|
Route::post('register', [RegisteredUserController::class, 'store']);
|
||||||
|
|
||||||
|
Route::get('login', [AuthenticatedSessionController::class, 'create'])
|
||||||
|
->name('login');
|
||||||
|
|
||||||
|
Route::post('login', [AuthenticatedSessionController::class, 'store']);
|
||||||
|
|
||||||
|
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
|
||||||
|
->name('password.request');
|
||||||
|
|
||||||
|
Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
|
||||||
|
->name('password.email');
|
||||||
|
|
||||||
|
Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
|
||||||
|
->name('password.reset');
|
||||||
|
|
||||||
|
Route::post('reset-password', [NewPasswordController::class, 'store'])
|
||||||
|
->name('password.store');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware('auth')->group(function () {
|
||||||
|
Route::get('verify-email', EmailVerificationPromptController::class)
|
||||||
|
->name('verification.notice');
|
||||||
|
|
||||||
|
Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
|
||||||
|
->middleware(['signed', 'throttle:6,1'])
|
||||||
|
->name('verification.verify');
|
||||||
|
|
||||||
|
Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
|
||||||
|
->middleware('throttle:6,1')
|
||||||
|
->name('verification.send');
|
||||||
|
|
||||||
|
Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
|
||||||
|
->name('password.confirm');
|
||||||
|
|
||||||
|
Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);
|
||||||
|
|
||||||
|
Route::put('password', [PasswordController::class, 'update'])->name('password.update');
|
||||||
|
|
||||||
|
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
|
||||||
|
->name('logout');
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?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([
|
||||||
|
'role' => \App\Http\Middleware\RoleMiddleware::class,
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
//
|
||||||
|
})->create();
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
|
||||||
|
return [
|
||||||
|
AppServiceProvider::class,
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,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",
|
||||||
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/tinker": "^2.10.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/breeze": "^2.4",
|
||||||
|
"laravel/pail": "^1.2.2",
|
||||||
|
"laravel/pint": "^1.24",
|
||||||
|
"laravel/sail": "^1.41",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"phpunit/phpunit": "^11.5.50"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/",
|
||||||
|
"Database\\Factories\\": "database/factories/",
|
||||||
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"setup": [
|
||||||
|
"composer install",
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
|
"@php artisan key:generate",
|
||||||
|
"@php artisan migrate --force",
|
||||||
|
"npm install",
|
||||||
|
"npm run build"
|
||||||
|
],
|
||||||
|
"dev": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"@php artisan config:clear --ansi",
|
||||||
|
"@php artisan test"
|
||||||
|
],
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
"@php artisan package:discover --ansi"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||||
|
],
|
||||||
|
"post-root-package-install": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@php artisan key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||||
|
"@php artisan migrate --graceful --ansi"
|
||||||
|
],
|
||||||
|
"pre-package-uninstall": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"dont-discover": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true,
|
||||||
|
"php-http/discovery": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,125 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the name of your application, which will be used when the
|
||||||
|
| framework needs to place the application's name in a notification or
|
||||||
|
| other UI elements where an application name needs to be displayed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Environment
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the "environment" your application is currently
|
||||||
|
| running in. This may determine how you prefer to configure various
|
||||||
|
| services the application utilizes. Set this in your ".env" file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Debug Mode
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When your application is in debug mode, detailed error messages with
|
||||||
|
| stack traces will be shown on every error that occurs within your
|
||||||
|
| application. If disabled, a simple generic error page is shown.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application URL
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This URL is used by the console to properly generate URLs when using
|
||||||
|
| the Artisan command line tool. You should set this to the root of
|
||||||
|
| the application so that it's available within Artisan commands.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Timezone
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default timezone for your application, which
|
||||||
|
| will be used by the PHP date and date-time functions. The timezone
|
||||||
|
| is set to "UTC" by default as it is suitable for most use cases.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'timezone' => 'Asia/Jakarta',
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Locale Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The application locale determines the default locale that will be used
|
||||||
|
| by Laravel's translation / localization methods. This option can be
|
||||||
|
| set to any locale for which you plan to have translation strings.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'locale' => env('APP_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Encryption Key
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This key is utilized by Laravel's encryption services and should be set
|
||||||
|
| to a random, 32 character string to ensure that all encrypted values
|
||||||
|
| are secure. You should do this prior to deploying the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
|
'key' => env('APP_KEY'),
|
||||||
|
|
||||||
|
'previous_keys' => [
|
||||||
|
...array_filter(
|
||||||
|
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Maintenance Mode Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options determine the driver used to determine and
|
||||||
|
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||||
|
| allow maintenance mode to be controlled across multiple machines.
|
||||||
|
|
|
||||||
|
| Supported drivers: "file", "cache"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'maintenance' => [
|
||||||
|
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||||
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default authentication "guard" and password
|
||||||
|
| reset "broker" for your application. You may change these values
|
||||||
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'guard' => env('AUTH_GUARD', 'web'),
|
||||||
|
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, you may define every authentication guard for your application.
|
||||||
|
| Of course, a great default configuration has been defined for you
|
||||||
|
| which utilizes session storage plus the Eloquent user provider.
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| Supported: "session"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| User Providers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| If you have multiple user tables or models you may configure multiple
|
||||||
|
| providers to represent the model / table. These providers may then
|
||||||
|
| be assigned to any extra authentication guards you have defined.
|
||||||
|
|
|
||||||
|
| Supported: "database", "eloquent"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => env('AUTH_MODEL', User::class),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 'users' => [
|
||||||
|
// 'driver' => 'database',
|
||||||
|
// 'table' => 'users',
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Resetting Passwords
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options specify the behavior of Laravel's password
|
||||||
|
| reset functionality, including the table utilized for token storage
|
||||||
|
| and the user provider that is invoked to actually retrieve users.
|
||||||
|
|
|
||||||
|
| The expiry time is the number of minutes that each reset token will be
|
||||||
|
| considered valid. This security feature keeps tokens short-lived so
|
||||||
|
| they have less time to be guessed. You may change this as needed.
|
||||||
|
|
|
||||||
|
| The throttle setting is the number of seconds a user must wait before
|
||||||
|
| generating more password reset tokens. This prevents the user from
|
||||||
|
| quickly generating a very large amount of password reset tokens.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => [
|
||||||
|
'users' => [
|
||||||
|
'provider' => 'users',
|
||||||
|
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||||
|
'expire' => 60,
|
||||||
|
'throttle' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Confirmation Timeout
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the number of seconds before a password confirmation
|
||||||
|
| window expires and users are asked to re-enter their password via the
|
||||||
|
| confirmation screen. By default, the timeout lasts for three hours.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default cache store that will be used by the
|
||||||
|
| framework. This connection is utilized if another isn't explicitly
|
||||||
|
| specified when running a cache operation inside the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('CACHE_STORE', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Stores
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define all of the cache "stores" for your application as
|
||||||
|
| well as their drivers. You may even define multiple stores for the
|
||||||
|
| same cache driver to group types of items stored in your caches.
|
||||||
|
|
|
||||||
|
| Supported drivers: "array", "database", "file", "memcached",
|
||||||
|
| "redis", "dynamodb", "octane",
|
||||||
|
| "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stores' => [
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'driver' => 'array',
|
||||||
|
'serialize' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_CACHE_CONNECTION'),
|
||||||
|
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||||
|
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||||
|
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'file' => [
|
||||||
|
'driver' => 'file',
|
||||||
|
'path' => storage_path('framework/cache/data'),
|
||||||
|
'lock_path' => storage_path('framework/cache/data'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'memcached' => [
|
||||||
|
'driver' => 'memcached',
|
||||||
|
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||||
|
'sasl' => [
|
||||||
|
env('MEMCACHED_USERNAME'),
|
||||||
|
env('MEMCACHED_PASSWORD'),
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||||
|
],
|
||||||
|
'servers' => [
|
||||||
|
[
|
||||||
|
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MEMCACHED_PORT', 11211),
|
||||||
|
'weight' => 100,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||||
|
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'dynamodb' => [
|
||||||
|
'driver' => 'dynamodb',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||||
|
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'octane' => [
|
||||||
|
'driver' => 'octane',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'stores' => [
|
||||||
|
'database',
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Key Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||||
|
| stores, there might be other applications using the same cache. For
|
||||||
|
| that reason, you may prefix every cache key to avoid collisions.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Pdo\Mysql;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Database Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which of the database connections below you wish
|
||||||
|
| to use as your default connection for database operations. This is
|
||||||
|
| the connection which will be utilized unless another connection
|
||||||
|
| is explicitly specified when you execute a query / statement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Database Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below are all of the database connections defined for your application.
|
||||||
|
| An example configuration is provided for each database system which
|
||||||
|
| is supported by Laravel. You're free to add / remove connections.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sqlite' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
|
'prefix' => '',
|
||||||
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
'busy_timeout' => null,
|
||||||
|
'journal_mode' => null,
|
||||||
|
'synchronous' => null,
|
||||||
|
'transaction_mode' => 'DEFERRED',
|
||||||
|
],
|
||||||
|
|
||||||
|
'mysql' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mariadb' => [
|
||||||
|
'driver' => 'mariadb',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'pgsql' => [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '5432'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'search_path' => 'public',
|
||||||
|
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqlsrv' => [
|
||||||
|
'driver' => 'sqlsrv',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', 'localhost'),
|
||||||
|
'port' => env('DB_PORT', '1433'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||||
|
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Migration Repository Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This table keeps track of all the migrations that have already run for
|
||||||
|
| your application. Using this information, we can determine which of
|
||||||
|
| the migrations on disk haven't actually been run on the database.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'migrations' => [
|
||||||
|
'table' => 'migrations',
|
||||||
|
'update_date_on_publish' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Redis Databases
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Redis is an open source, fast, and advanced key-value store that also
|
||||||
|
| provides a richer body of commands than a typical key-value system
|
||||||
|
| such as Memcached. You may define your connection settings here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
|
||||||
|
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
|
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||||
|
'persistent' => env('REDIS_PERSISTENT', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
'default' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_DB', '0'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_CACHE_DB', '1'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Filesystem Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that should be used
|
||||||
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
|
| based disks are available to your application for file storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filesystem Disks
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below you may configure as many filesystem disks as necessary, and you
|
||||||
|
| may even configure multiple disks for the same driver. Examples for
|
||||||
|
| most supported storage drivers are configured here for reference.
|
||||||
|
|
|
||||||
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'disks' => [
|
||||||
|
|
||||||
|
'local' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/private'),
|
||||||
|
'serve' => true,
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'public' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/public'),
|
||||||
|
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
's3' => [
|
||||||
|
'driver' => 's3',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION'),
|
||||||
|
'bucket' => env('AWS_BUCKET'),
|
||||||
|
'url' => env('AWS_URL'),
|
||||||
|
'endpoint' => env('AWS_ENDPOINT'),
|
||||||
|
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Symbolic Links
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the symbolic links that will be created when the
|
||||||
|
| `storage:link` Artisan command is executed. The array keys should be
|
||||||
|
| the locations of the links and the values should be their targets.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'links' => [
|
||||||
|
public_path('storage') => storage_path('app/public'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default log channel that is utilized to write
|
||||||
|
| messages to your logs. The value provided here should match one of
|
||||||
|
| the channels present in the list of "channels" configured below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('LOG_CHANNEL', 'stack'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Deprecations Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the log channel that should be used to log warnings
|
||||||
|
| regarding deprecated PHP and library features. This allows you to get
|
||||||
|
| your application ready for upcoming major versions of dependencies.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'deprecations' => [
|
||||||
|
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||||
|
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Log Channels
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the log channels for your application. Laravel
|
||||||
|
| utilizes the Monolog PHP logging library, which includes a variety
|
||||||
|
| of powerful log handlers and formatters that you're free to use.
|
||||||
|
|
|
||||||
|
| Available drivers: "single", "daily", "slack", "syslog",
|
||||||
|
| "errorlog", "monolog", "custom", "stack"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'channels' => [
|
||||||
|
|
||||||
|
'stack' => [
|
||||||
|
'driver' => 'stack',
|
||||||
|
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||||
|
'ignore_exceptions' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'single' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'daily' => [
|
||||||
|
'driver' => 'daily',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'days' => env('LOG_DAILY_DAYS', 14),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'driver' => 'slack',
|
||||||
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
|
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
|
||||||
|
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||||
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'papertrail' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||||
|
'handler_with' => [
|
||||||
|
'host' => env('PAPERTRAIL_URL'),
|
||||||
|
'port' => env('PAPERTRAIL_PORT'),
|
||||||
|
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||||
|
],
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'stderr' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => StreamHandler::class,
|
||||||
|
'handler_with' => [
|
||||||
|
'stream' => 'php://stderr',
|
||||||
|
],
|
||||||
|
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'syslog' => [
|
||||||
|
'driver' => 'syslog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'errorlog' => [
|
||||||
|
'driver' => 'errorlog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'null' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'handler' => NullHandler::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'emergency' => [
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Mailer
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default mailer that is used to send all email
|
||||||
|
| messages unless another mailer is explicitly specified when sending
|
||||||
|
| the message. All additional mailers can be configured within the
|
||||||
|
| "mailers" array. Examples of each type of mailer are provided.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('MAIL_MAILER', 'log'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Mailer Configurations
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure all of the mailers used by your application plus
|
||||||
|
| their respective settings. Several examples have been configured for
|
||||||
|
| you and you are free to add your own as your application requires.
|
||||||
|
|
|
||||||
|
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||||
|
| when delivering an email. You may specify which one you're using for
|
||||||
|
| your mailers below. You may also add additional mailers if needed.
|
||||||
|
|
|
||||||
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||||
|
| "postmark", "resend", "log", "array",
|
||||||
|
| "failover", "roundrobin"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'mailers' => [
|
||||||
|
|
||||||
|
'smtp' => [
|
||||||
|
'transport' => 'smtp',
|
||||||
|
'scheme' => env('MAIL_SCHEME'),
|
||||||
|
'url' => env('MAIL_URL'),
|
||||||
|
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MAIL_PORT', 2525),
|
||||||
|
'username' => env('MAIL_USERNAME'),
|
||||||
|
'password' => env('MAIL_PASSWORD'),
|
||||||
|
'timeout' => null,
|
||||||
|
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'transport' => 'ses',
|
||||||
|
],
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'transport' => 'postmark',
|
||||||
|
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||||
|
// 'client' => [
|
||||||
|
// 'timeout' => 5,
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'transport' => 'resend',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sendmail' => [
|
||||||
|
'transport' => 'sendmail',
|
||||||
|
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'log' => [
|
||||||
|
'transport' => 'log',
|
||||||
|
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'transport' => 'array',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'transport' => 'failover',
|
||||||
|
'mailers' => [
|
||||||
|
'smtp',
|
||||||
|
'log',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
'roundrobin' => [
|
||||||
|
'transport' => 'roundrobin',
|
||||||
|
'mailers' => [
|
||||||
|
'ses',
|
||||||
|
'postmark',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global "From" Address
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| You may wish for all emails sent by your application to be sent from
|
||||||
|
| the same address. Here you may specify a name and address that is
|
||||||
|
| used globally for all emails that are sent by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'from' => [
|
||||||
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
|
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Queue Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Laravel's queue supports a variety of backends via a single, unified
|
||||||
|
| API, giving you convenient access to each backend using identical
|
||||||
|
| syntax for each. The default queue connection is defined below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queue Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the connection options for every queue backend
|
||||||
|
| used by your application. An example configuration is provided for
|
||||||
|
| each backend supported by Laravel. You're also free to add more.
|
||||||
|
|
|
||||||
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||||
|
| "deferred", "background", "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sync' => [
|
||||||
|
'driver' => 'sync',
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||||
|
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||||
|
'queue' => env('DB_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'beanstalkd' => [
|
||||||
|
'driver' => 'beanstalkd',
|
||||||
|
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||||
|
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => 0,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqs' => [
|
||||||
|
'driver' => 'sqs',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||||
|
'queue' => env('SQS_QUEUE', 'default'),
|
||||||
|
'suffix' => env('SQS_SUFFIX'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||||
|
'queue' => env('REDIS_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => null,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'deferred' => [
|
||||||
|
'driver' => 'deferred',
|
||||||
|
],
|
||||||
|
|
||||||
|
'background' => [
|
||||||
|
'driver' => 'background',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'connections' => [
|
||||||
|
'database',
|
||||||
|
'deferred',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Job Batching
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following options configure the database and table that store job
|
||||||
|
| batching information. These options can be updated to any database
|
||||||
|
| connection and table which has been defined by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'batching' => [
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'job_batches',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Failed Queue Jobs
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These options configure the behavior of failed queue job logging so you
|
||||||
|
| can control how and where failed jobs are stored. Laravel ships with
|
||||||
|
| support for storing failed jobs in a simple file or in a database.
|
||||||
|
|
|
||||||
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'failed' => [
|
||||||
|
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'failed_jobs',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Third Party Services
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This file is for storing the credentials for third party services such
|
||||||
|
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||||
|
| location for this type of information, allowing packages to have
|
||||||
|
| a conventional file to locate the various service credentials.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'key' => env('POSTMARK_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'key' => env('RESEND_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'notifications' => [
|
||||||
|
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||||
|
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Session Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines the default session driver that is utilized for
|
||||||
|
| incoming requests. Laravel supports a variety of storage options to
|
||||||
|
| persist session data. Database storage is a great default choice.
|
||||||
|
|
|
||||||
|
| Supported: "file", "cookie", "database", "memcached",
|
||||||
|
| "redis", "dynamodb", "array"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'driver' => env('SESSION_DRIVER', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Lifetime
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the number of minutes that you wish the session
|
||||||
|
| to be allowed to remain idle before it expires. If you want them
|
||||||
|
| to expire immediately when the browser is closed then you may
|
||||||
|
| indicate that via the expire_on_close configuration option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||||
|
|
||||||
|
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Encryption
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option allows you to easily specify that all of your session data
|
||||||
|
| should be encrypted before it's stored. All encryption is performed
|
||||||
|
| automatically by Laravel and you may use the session like normal.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session File Location
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the "file" session driver, the session files are placed
|
||||||
|
| on disk. The default storage location is defined here; however, you
|
||||||
|
| are free to provide another location where they should be stored.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'files' => storage_path('framework/sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Connection
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" or "redis" session drivers, you may specify a
|
||||||
|
| connection that should be used to manage these sessions. This should
|
||||||
|
| correspond to a connection in your database configuration options.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connection' => env('SESSION_CONNECTION'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" session driver, you may specify the table to
|
||||||
|
| be used to store sessions. Of course, a sensible default is defined
|
||||||
|
| for you; however, you're welcome to change this to another table.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'table' => env('SESSION_TABLE', 'sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using one of the framework's cache driven session backends, you may
|
||||||
|
| define the cache store which should be used to store the session data
|
||||||
|
| between requests. This must match one of your defined cache stores.
|
||||||
|
|
|
||||||
|
| Affects: "dynamodb", "memcached", "redis"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => env('SESSION_STORE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Sweeping Lottery
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Some session drivers must manually sweep their storage location to get
|
||||||
|
| rid of old sessions from storage. Here are the chances that it will
|
||||||
|
| happen on a given request. By default, the odds are 2 out of 100.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lottery' => [2, 100],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may change the name of the session cookie that is created by
|
||||||
|
| the framework. Typically, you should not need to change this value
|
||||||
|
| since doing so does not grant a meaningful security improvement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cookie' => env(
|
||||||
|
'SESSION_COOKIE',
|
||||||
|
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||||
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The session cookie path determines the path for which the cookie will
|
||||||
|
| be regarded as available. Typically, this will be the root path of
|
||||||
|
| your application, but you're free to change this when necessary.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'path' => env('SESSION_PATH', '/'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Domain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the domain and subdomains the session cookie is
|
||||||
|
| available to. By default, the cookie will be available to the root
|
||||||
|
| domain without subdomains. Typically, this shouldn't be changed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'domain' => env('SESSION_DOMAIN'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTPS Only Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By setting this option to true, session cookies will only be sent back
|
||||||
|
| to the server if the browser has a HTTPS connection. This will keep
|
||||||
|
| the cookie from being sent to you when it can't be done securely.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTP Access Only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will prevent JavaScript from accessing the
|
||||||
|
| value of the cookie and the cookie will only be accessible through
|
||||||
|
| the HTTP protocol. It's unlikely you should disable this option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Same-Site Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines how your cookies behave when cross-site requests
|
||||||
|
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||||
|
| will set this value to "lax" to permit secure cross-site requests.
|
||||||
|
|
|
||||||
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||||
|
|
|
||||||
|
| Supported: "lax", "strict", "none", null
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Partitioned Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will tie the cookie to the top-level site for
|
||||||
|
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||||
|
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
*.sqlite*
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<User>
|
||||||
|
*/
|
||||||
|
class UserFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The current password being used by the factory.
|
||||||
|
*/
|
||||||
|
protected static ?string $password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => fake()->name(),
|
||||||
|
'email' => fake()->unique()->safeEmail(),
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
|
'remember_token' => Str::random(10),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the model's email address should be unverified.
|
||||||
|
*/
|
||||||
|
public function unverified(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'email_verified_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('users', function (Blueprint $table) {
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->unique();
|
||||||
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
|
$table->string('password');
|
||||||
|
$table->enum('role', ['admin', 'user'])->default('user');
|
||||||
|
$table->rememberToken();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('email')->primary();
|
||||||
|
$table->string('token');
|
||||||
|
$table->timestamp('created_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('sessions', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
Schema::dropIfExists('password_reset_tokens');
|
||||||
|
Schema::dropIfExists('sessions');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->mediumText('value');
|
||||||
|
$table->integer('expiration')->index();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->string('owner');
|
||||||
|
$table->integer('expiration')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('cache');
|
||||||
|
Schema::dropIfExists('cache_locks');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('queue')->index();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->unsignedTinyInteger('attempts');
|
||||||
|
$table->unsignedInteger('reserved_at')->nullable();
|
||||||
|
$table->unsignedInteger('available_at');
|
||||||
|
$table->unsignedInteger('created_at');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('job_batches', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->string('name');
|
||||||
|
$table->integer('total_jobs');
|
||||||
|
$table->integer('pending_jobs');
|
||||||
|
$table->integer('failed_jobs');
|
||||||
|
$table->longText('failed_job_ids');
|
||||||
|
$table->mediumText('options')->nullable();
|
||||||
|
$table->integer('cancelled_at')->nullable();
|
||||||
|
$table->integer('created_at');
|
||||||
|
$table->integer('finished_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('uuid')->unique();
|
||||||
|
$table->text('connection');
|
||||||
|
$table->text('queue');
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->longText('exception');
|
||||||
|
$table->timestamp('failed_at')->useCurrent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('jobs');
|
||||||
|
Schema::dropIfExists('job_batches');
|
||||||
|
Schema::dropIfExists('failed_jobs');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,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(): void
|
||||||
|
{
|
||||||
|
Schema::create('tbl_penyakit', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('kode', 10)->unique();
|
||||||
|
$table->string('nama', 150);
|
||||||
|
$table->enum('jenis', ['Penyakit', 'Hama']);
|
||||||
|
$table->string('penyebab', 200)->default('-');
|
||||||
|
$table->text('deskripsi')->nullable();
|
||||||
|
$table->string('gambar', 255)->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('tbl_penyakit');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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('tbl_gejala', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('kode', 10)->unique();
|
||||||
|
$table->string('nama', 255);
|
||||||
|
$table->text('deskripsi')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('tbl_gejala');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?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('tbl_rule', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('kode_rule', 10)->unique();
|
||||||
|
$table->unsignedBigInteger('id_penyakit');
|
||||||
|
$table->unsignedBigInteger('id_gejala');
|
||||||
|
$table->tinyInteger('nilai_pakar');
|
||||||
|
$table->decimal('nilai_cf', 3, 1);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->foreign('id_penyakit')->references('id')->on('tbl_penyakit')->onDelete('cascade');
|
||||||
|
$table->foreign('id_gejala')->references('id')->on('tbl_gejala')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('tbl_solusi', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('id_penyakit');
|
||||||
|
$table->text('solusi');
|
||||||
|
$table->text('pencegahan')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->foreign('id_penyakit')->references('id')->on('tbl_penyakit')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('tbl_konsultasi', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('kode_konsultasi', 20)->unique();
|
||||||
|
$table->unsignedBigInteger('user_id');
|
||||||
|
$table->dateTime('tanggal')->useCurrent();
|
||||||
|
$table->enum('status', ['selesai', 'tidak_terdeteksi'])->default('selesai');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('tbl_konsultasi_gejala', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('id_konsultasi');
|
||||||
|
$table->unsignedBigInteger('id_gejala');
|
||||||
|
$table->decimal('cf_user', 3, 1)->default(1.0);
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
$table->foreign('id_konsultasi')->references('id')->on('tbl_konsultasi')->onDelete('cascade');
|
||||||
|
$table->foreign('id_gejala')->references('id')->on('tbl_gejala')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('tbl_hasil_diagnosa', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('id_konsultasi');
|
||||||
|
$table->unsignedBigInteger('id_penyakit');
|
||||||
|
$table->decimal('nilai_cf_akhir', 6, 4);
|
||||||
|
$table->decimal('persentase', 5, 2);
|
||||||
|
$table->tinyInteger('ranking');
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
$table->foreign('id_konsultasi')->references('id')->on('tbl_konsultasi')->onDelete('cascade');
|
||||||
|
$table->foreign('id_penyakit')->references('id')->on('tbl_penyakit')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('tbl_hasil_diagnosa');
|
||||||
|
Schema::dropIfExists('tbl_konsultasi_gejala');
|
||||||
|
Schema::dropIfExists('tbl_konsultasi');
|
||||||
|
Schema::dropIfExists('tbl_solusi');
|
||||||
|
Schema::dropIfExists('tbl_rule');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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('no_telp', 15)->nullable()->after('email');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('no_telp');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class DatabaseSeeder extends Seeder
|
||||||
|
{
|
||||||
|
use WithoutModelEvents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed the application's database.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// User::factory(10)->create();
|
||||||
|
|
||||||
|
$this->call([
|
||||||
|
SistemPakarSeeder::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class SistemPakarSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// ── Users ─────────────────────────────────────────────
|
||||||
|
DB::table('users')->insert([
|
||||||
|
[
|
||||||
|
'name' => 'Administrator',
|
||||||
|
'email' => 'admin@sipakar.com',
|
||||||
|
'password' => Hash::make('admin123'),
|
||||||
|
'role' => 'admin',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'User Demo',
|
||||||
|
'email' => 'user@sipakar.com',
|
||||||
|
'password' => Hash::make('user123'),
|
||||||
|
'role' => 'user',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Penyakit ──────────────────────────────────────────
|
||||||
|
$penyakit = [
|
||||||
|
['kode'=>'P01','nama'=>'Thrips','jenis'=>'Hama','penyebab'=>'-','deskripsi'=>'Serangga kecil berwarna kuning kecoklatan yang menyerang bagian daun, bunga, dan pucuk tanaman. Serangan menyebabkan daun mengeriting, berubah warna, dan pertumbuhan tanaman terhambat.'],
|
||||||
|
['kode'=>'P02','nama'=>'Tungau Kuning','jenis'=>'Hama','penyebab'=>'-','deskripsi'=>'Hama berukuran sangat kecil yang menyerang daun muda dan pucuk tanaman. Gejala berupa daun mengeriting, menebal, pertumbuhan terhambat, dan tanaman tampak kerdil.'],
|
||||||
|
['kode'=>'P03','nama'=>'Lalat Buah','jenis'=>'Hama','penyebab'=>'-','deskripsi'=>'Serangga yang menyerang buah cabai dengan cara meletakkan telur di dalam buah. Larva yang menetas akan merusak daging buah sehingga buah membusuk dan gugur sebelum panen.'],
|
||||||
|
['kode'=>'P04','nama'=>'Kutu Daun Persik','jenis'=>'Hama','penyebab'=>'-','deskripsi'=>'Hama kecil berwarna hijau atau hitam yang hidup berkelompok pada pucuk dan daun muda. Kutu daun juga dapat menjadi vektor penyakit virus.'],
|
||||||
|
['kode'=>'P05','nama'=>'Kutu Kebul Bemisia','jenis'=>'Hama','penyebab'=>'-','deskripsi'=>'Serangga kecil berwarna putih yang hidup di bagian bawah daun. Menghasilkan embun madu yang memicu pertumbuhan jamur jelaga.'],
|
||||||
|
['kode'=>'P06','nama'=>'Kutu Kebul Parasitica','jenis'=>'Hama','penyebab'=>'-','deskripsi'=>'Hama berbentuk kecil berwarna putih yang hidup berkelompok di permukaan daun. Serangan menyebabkan daun menguning dan pertumbuhan tanaman terganggu.'],
|
||||||
|
['kode'=>'P07','nama'=>'Layu Bakteri','jenis'=>'Penyakit','penyebab'=>'Ralstonia solanacearum','deskripsi'=>'Penyakit yang menyebabkan tanaman layu secara tiba-tiba akibat infeksi bakteri pada jaringan pembuluh. Umumnya menyerang pada kondisi tanah lembap.'],
|
||||||
|
['kode'=>'P08','nama'=>'Layu Fusarium','jenis'=>'Penyakit','penyebab'=>'Fusarium oxysporum','deskripsi'=>'Penyakit jamur yang menyerang akar dan pembuluh tanaman sehingga aliran air terganggu. Gejala awal berupa daun menguning dari bagian bawah.'],
|
||||||
|
['kode'=>'P09','nama'=>'Antraknosa','jenis'=>'Penyakit','penyebab'=>'Colletotrichum capsici','deskripsi'=>'Penyakit yang menyerang buah cabai dengan gejala bercak cekung berwarna cokelat hingga kehitaman.'],
|
||||||
|
['kode'=>'P10','nama'=>'Virus Kuning (YLCV)','jenis'=>'Penyakit','penyebab'=>'Begomovirus','deskripsi'=>'Disebabkan oleh virus yang ditularkan oleh serangga vektor. Gejala berupa daun menguning, menggulung, dan pertumbuhan tanaman terhambat.'],
|
||||||
|
['kode'=>'P11','nama'=>'Penyakit Mosaik','jenis'=>'Penyakit','penyebab'=>'Cucumber Mosaic Virus','deskripsi'=>'Daun menunjukkan pola belang hijau muda dan tua (mosaik), pertumbuhan tidak normal, dan tanaman menjadi kerdil.'],
|
||||||
|
['kode'=>'P12','nama'=>'Virus Keriting','jenis'=>'Penyakit','penyebab'=>'Leaf Curl Virus','deskripsi'=>'Menimbulkan gejala daun keriting, menebal, dan menggulung ke atas atau ke bawah. Tanaman tumbuh tidak normal dan hasil panen menurun.'],
|
||||||
|
];
|
||||||
|
foreach ($penyakit as $p) {
|
||||||
|
DB::table('tbl_penyakit')->insert([...$p, 'created_at'=>now(), 'updated_at'=>now()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gejala ────────────────────────────────────────────
|
||||||
|
$gejala = [
|
||||||
|
'G01'=>'Daun muda mengeriting',
|
||||||
|
'G02'=>'Daun tampak kusam dan mengering di tepi',
|
||||||
|
'G03'=>'Terdapat bercak atau garis putih pada daun',
|
||||||
|
'G04'=>'Permukaan daun berwarna keperakan',
|
||||||
|
'G05'=>'Pertumbuhan tanaman terhambat',
|
||||||
|
'G06'=>'Daun bagian bawah terdapat tungau kecil',
|
||||||
|
'G07'=>'Daun menguning',
|
||||||
|
'G08'=>'Permukaan daun terasa kasar',
|
||||||
|
'G09'=>'Daun menebal dan menggulung',
|
||||||
|
'G10'=>'Tanaman tampak kerdil',
|
||||||
|
'G11'=>'Buah terdapat lubang kecil bekas tusukan',
|
||||||
|
'G12'=>'Terdapat larva atau belatung di dalam buah',
|
||||||
|
'G13'=>'Permukaan buah terdapat bercak hitam lunak',
|
||||||
|
'G14'=>'Buah membusuk sebelum matang',
|
||||||
|
'G15'=>'Buah rontok lebih cepat',
|
||||||
|
'G16'=>'Daun menggulung atau keriting',
|
||||||
|
'G17'=>'Terdapat koloni serangga kecil di pucuk',
|
||||||
|
'G18'=>'Permukaan daun lengket atau embun madu',
|
||||||
|
'G19'=>'Pertumbuhan pucuk terhambat',
|
||||||
|
'G20'=>'Tanaman tampak lemah dan kerdil',
|
||||||
|
'G21'=>'Terdapat serangga putih kecil di bawah daun',
|
||||||
|
'G22'=>'Permukaan daun lengket akibat embun madu',
|
||||||
|
'G23'=>'Muncul cendawan jelaga hitam pada daun',
|
||||||
|
'G24'=>'Serangga beterbangan saat daun digoyang',
|
||||||
|
'G25'=>'Daun bagian bawah penuh serangga putih',
|
||||||
|
'G26'=>'Daun layu perlahan',
|
||||||
|
'G27'=>'Serangga putih menempel di bawah daun',
|
||||||
|
'G28'=>'Pertumbuhan tanaman melambat',
|
||||||
|
'G29'=>'Tanaman tampak pucat',
|
||||||
|
'G30'=>'Daun rontok lebih awal',
|
||||||
|
'G31'=>'Tanaman layu mendadak',
|
||||||
|
'G32'=>'Batang mengeluarkan lendir putih atau keruh saat dipotong',
|
||||||
|
'G33'=>'Pembuluh batang berwarna kecoklatan',
|
||||||
|
'G34'=>'Daun tetap hijau tetapi layu',
|
||||||
|
'G35'=>'Tanaman mati dengan cepat',
|
||||||
|
'G36'=>'Tanaman layu secara perlahan',
|
||||||
|
'G37'=>'Daun menguning mulai dari bagian bawah',
|
||||||
|
'G38'=>'Pembuluh batang berwarna coklat gelap',
|
||||||
|
'G39'=>'Akar berwarna coklat dan membusuk',
|
||||||
|
'G40'=>'Tanaman mati bertahap dari bawah ke atas',
|
||||||
|
'G41'=>'Terdapat lingkaran konsentris pada bercak buah',
|
||||||
|
'G42'=>'Bercak berwarna coklat kehitaman pada buah',
|
||||||
|
'G43'=>'Buah terdapat bercak cekung',
|
||||||
|
'G44'=>'Buah membusuk akibat serangan jamur',
|
||||||
|
'G45'=>'Serangan sering muncul saat buah hampir matang',
|
||||||
|
'G46'=>'Daun berwarna kuning cerah merata',
|
||||||
|
'G47'=>'Daun menggulung ke atas',
|
||||||
|
'G48'=>'Tulang daun menebal dan menonjol',
|
||||||
|
'G49'=>'Tanaman tumbuh kerdil',
|
||||||
|
'G50'=>'Produksi buah sangat berkurang',
|
||||||
|
'G51'=>'Permukaan daun tidak rata dan bergelombang',
|
||||||
|
'G52'=>'Daun menyempit dan memanjang',
|
||||||
|
'G53'=>'Daun belang hijau muda dan hijau tua',
|
||||||
|
'G54'=>'Buah berukuran kecil dan cacat',
|
||||||
|
'G55'=>'Tanaman tidak berkembang normal',
|
||||||
|
'G56'=>'Daun keriting dan menggulung tidak beraturan',
|
||||||
|
'G57'=>'Tulang daun menebal berwarna lebih muda',
|
||||||
|
'G58'=>'Daun menjadi kecil dan kaku',
|
||||||
|
'G59'=>'Ruas batang memendek',
|
||||||
|
'G60'=>'Produksi buah menurun drastis',
|
||||||
|
];
|
||||||
|
foreach ($gejala as $kode => $nama) {
|
||||||
|
DB::table('tbl_gejala')->insert([
|
||||||
|
'kode' => $kode, 'nama' => $nama,
|
||||||
|
'created_at' => now(), 'updated_at' => now()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Rule ──────────────────────────────────────────────
|
||||||
|
$rules = [
|
||||||
|
// P01 Thrips
|
||||||
|
['R001','P01','G01',1,0.2],['R002','P01','G02',2,0.4],['R003','P01','G03',3,0.5],
|
||||||
|
['R004','P01','G04',4,0.6],['R005','P01','G05',5,0.8],
|
||||||
|
// P02 Tungau
|
||||||
|
['R006','P02','G06',1,0.2],['R007','P02','G07',2,0.4],['R008','P02','G08',3,0.5],
|
||||||
|
['R009','P02','G09',4,0.6],['R010','P02','G10',5,0.8],
|
||||||
|
// P03 Lalat Buah
|
||||||
|
['R011','P03','G11',1,0.2],['R012','P03','G12',2,0.4],['R013','P03','G13',3,0.5],
|
||||||
|
['R014','P03','G14',4,0.6],['R015','P03','G15',5,0.8],
|
||||||
|
// P04 Kutu Daun
|
||||||
|
['R016','P04','G16',1,0.2],['R017','P04','G17',2,0.4],['R018','P04','G18',3,0.5],
|
||||||
|
['R019','P04','G19',4,0.6],['R020','P04','G20',5,0.8],
|
||||||
|
// P05 Kutu Kebul Bemisia
|
||||||
|
['R021','P05','G21',1,0.2],['R022','P05','G22',2,0.4],['R023','P05','G23',3,0.5],
|
||||||
|
['R024','P05','G24',4,0.6],['R025','P05','G25',5,0.8],
|
||||||
|
// P06 Kutu Kebul Parasitica
|
||||||
|
['R026','P06','G26',1,0.2],['R027','P06','G27',2,0.4],['R028','P06','G28',3,0.5],
|
||||||
|
['R029','P06','G29',4,0.6],['R030','P06','G30',5,0.8],
|
||||||
|
// P07 Layu Bakteri
|
||||||
|
['R031','P07','G31',1,0.2],['R032','P07','G32',2,0.4],['R033','P07','G33',3,0.5],
|
||||||
|
['R034','P07','G34',4,0.6],['R035','P07','G35',5,0.8],
|
||||||
|
// P08 Layu Fusarium
|
||||||
|
['R036','P08','G36',1,0.2],['R037','P08','G37',2,0.4],['R038','P08','G38',3,0.5],
|
||||||
|
['R039','P08','G39',4,0.6],['R040','P08','G40',5,0.8],
|
||||||
|
// P09 Antraknosa
|
||||||
|
['R041','P09','G41',1,0.2],['R042','P09','G42',2,0.4],['R043','P09','G43',3,0.5],
|
||||||
|
['R044','P09','G44',4,0.6],['R045','P09','G45',5,0.8],
|
||||||
|
// P10 Virus Kuning
|
||||||
|
['R046','P10','G46',1,0.2],['R047','P10','G47',2,0.4],['R048','P10','G48',3,0.5],
|
||||||
|
['R049','P10','G49',4,0.6],['R050','P10','G50',5,0.8],
|
||||||
|
// P11 Mosaik
|
||||||
|
['R051','P11','G51',1,0.2],['R052','P11','G52',2,0.4],['R053','P11','G53',3,0.5],
|
||||||
|
['R054','P11','G54',4,0.6],['R055','P11','G55',5,0.8],
|
||||||
|
// P12 Virus Keriting
|
||||||
|
['R056','P12','G56',1,0.2],['R057','P12','G57',2,0.4],['R058','P12','G58',3,0.5],
|
||||||
|
['R059','P12','G59',4,0.6],['R060','P12','G60',5,0.8],
|
||||||
|
];
|
||||||
|
foreach ($rules as [$kodeRule, $kodePenyakit, $kodeGejala, $nilaiPakar, $nilaiCf]) {
|
||||||
|
DB::table('tbl_rule')->insert([
|
||||||
|
'kode_rule' => $kodeRule,
|
||||||
|
'id_penyakit' => DB::table('tbl_penyakit')->where('kode', $kodePenyakit)->value('id'),
|
||||||
|
'id_gejala' => DB::table('tbl_gejala')->where('kode', $kodeGejala)->value('id'),
|
||||||
|
'nilai_pakar' => $nilaiPakar,
|
||||||
|
'nilai_cf' => $nilaiCf,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Solusi ────────────────────────────────────────────
|
||||||
|
$solusi = [
|
||||||
|
'P01' => ['Semprot insektisida berbahan aktif abamektin atau spinosad. Lakukan penyemprotan pagi atau sore hari secara rutin.',
|
||||||
|
'Gunakan mulsa plastik untuk mengurangi populasi thrips. Tanam tanaman perangkap seperti jagung di sekitar kebun.'],
|
||||||
|
'P02' => ['Semprot akarisida seperti abamektin atau propargit. Fokus pada bagian bawah daun. Ulangi setiap 5-7 hari.',
|
||||||
|
'Jaga kelembapan kebun agar tidak terlalu kering. Bersihkan gulma di sekitar tanaman.'],
|
||||||
|
'P03' => ['Gunakan perangkap protein bait. Bungkus buah dengan plastik jika perlu untuk mencegah serangan.',
|
||||||
|
'Kumpulkan dan musnahkan buah yang terserang. Pasang perangkap lalat buah di sekitar kebun.'],
|
||||||
|
'P04' => ['Semprot insektisida berbahan aktif imidakloprid atau thiamethoxam. Fokus pada pucuk dan bagian bawah daun.',
|
||||||
|
'Pantau tanaman secara rutin. Gunakan musuh alami seperti kumbang coccinellid.'],
|
||||||
|
'P05' => ['Gunakan insektisida sistemik seperti imidakloprid atau buprofezin. Semprotkan pada bagian bawah daun.',
|
||||||
|
'Pasang perangkap kuning. Gunakan varietas tahan virus untuk musim berikutnya.'],
|
||||||
|
'P06' => ['Semprot insektisida sistemik secara rutin. Bersihkan gulma di sekitar tanaman.',
|
||||||
|
'Jaga kebersihan kebun dan sanitasi lingkungan. Rotasi tanaman secara berkala.'],
|
||||||
|
'P07' => ['Cabut dan bakar tanaman yang terinfeksi agar tidak menular ke tanaman lain.',
|
||||||
|
'Gunakan varietas tahan Ralstonia. Lakukan rotasi tanaman dengan non-solanaceae. Perbaiki drainase lahan.'],
|
||||||
|
'P08' => ['Rendam akar bibit dengan fungisida berbahan trichoderma sebelum tanam. Siram fungisida sistemik ke pangkal batang.',
|
||||||
|
'Gunakan benih sehat bersertifikat. Lakukan solarisasi tanah sebelum tanam.'],
|
||||||
|
'P09' => ['Semprot fungisida berbahan mankozeb atau azoksistrobin. Petik dan musnahkan buah yang terinfeksi.',
|
||||||
|
'Gunakan benih yang sudah direndam fungisida. Atur drainase agar tidak menggenang.'],
|
||||||
|
'P10' => ['Tidak ada obat langsung untuk virus. Cabut dan musnahkan tanaman terinfeksi. Kendalikan serangga vektor dengan insektisida.',
|
||||||
|
'Gunakan varietas tahan YLCV. Pasang mulsa perak untuk mengusir kutu kebul.'],
|
||||||
|
'P11' => ['Segera cabut tanaman bergejala. Kendalikan vektor kutu daun dan kutu kebul dengan insektisida sistemik.',
|
||||||
|
'Gunakan benih bebas virus. Desinfeksi tangan dan alat setelah menangani tanaman sakit.'],
|
||||||
|
'P12' => ['Cabut dan musnahkan tanaman terinfeksi. Kendalikan serangga vektor secara intensif.',
|
||||||
|
'Gunakan varietas tahan Leaf Curl Virus. Pasang screen atau jaring pada persemaian.'],
|
||||||
|
];
|
||||||
|
foreach ($solusi as $kodePenyakit => [$sol, $cegah]) {
|
||||||
|
DB::table('tbl_solusi')->insert([
|
||||||
|
'id_penyakit' => DB::table('tbl_penyakit')->where('kode', $kodePenyakit)->value('id'),
|
||||||
|
'solusi' => $sol,
|
||||||
|
'pencegahan' => $cegah,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->command->info('✅ Seeder berhasil!');
|
||||||
|
$this->command->info(' Admin : admin@sipakar.com / admin123');
|
||||||
|
$this->command->info(' User : user@sipakar.com / user123');
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://www.schemastore.org/package.json",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"dev": "vite"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"alpinejs": "^3.4.2",
|
||||||
|
"autoprefixer": "^10.4.2",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"concurrently": "^9.0.1",
|
||||||
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
|
"postcss": "^8.4.31",
|
||||||
|
"tailwindcss": "^3.1.0",
|
||||||
|
"vite": "^7.0.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="Feature">
|
||||||
|
<directory>tests/Feature</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory>app</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
<php>
|
||||||
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
|
<env name="CACHE_STORE" value="array"/>
|
||||||
|
<env name="DB_CONNECTION" value="sqlite"/>
|
||||||
|
<env name="DB_DATABASE" value=":memory:"/>
|
||||||
|
<env name="DB_URL" value=""/>
|
||||||
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
|
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
<IfModule mod_negotiation.c>
|
||||||
|
Options -MultiViews -Indexes
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Handle Authorization Header
|
||||||
|
RewriteCond %{HTTP:Authorization} .
|
||||||
|
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||||
|
|
||||||
|
# Handle X-XSRF-Token Header
|
||||||
|
RewriteCond %{HTTP:x-xsrf-token} .
|
||||||
|
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||||
|
|
||||||
|
# Redirect Trailing Slashes If Not A Folder...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_URI} (.+)/$
|
||||||
|
RewriteRule ^ %1 [L,R=301]
|
||||||
|
|
||||||
|
# Send Requests To Front Controller...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ index.php [L]
|
||||||
|
</IfModule>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Determine if the application is in maintenance mode...
|
||||||
|
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||||
|
require $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the request...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||||
|
|
||||||
|
$app->handleRequest(Request::capture());
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import './bootstrap';
|
||||||
|
|
||||||
|
import Alpine from 'alpinejs';
|
||||||
|
|
||||||
|
window.Alpine = Alpine;
|
||||||
|
|
||||||
|
Alpine.start();
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
window.axios = axios;
|
||||||
|
|
||||||
|
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
{{-- resources/views/admin/dashboard.blade.php --}}
|
||||||
|
@extends('admin.layouts.app')
|
||||||
|
@section('title', 'Dashboard')
|
||||||
|
|
||||||
|
@push('styles')
|
||||||
|
<style>
|
||||||
|
.chart-container { position: relative; height: 220px; }
|
||||||
|
</style>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="breadcrumb">Admin / Dashboard</div>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p>Selamat datang, {{ auth()->user()->name }}.</p>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:0.78rem; color:var(--text-2)">
|
||||||
|
{{ \Carbon\Carbon::now()->locale('id')->isoFormat('dddd, D MMMM Y') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- STATS --}}
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Total Konsultasi</div>
|
||||||
|
<div class="stat-val red">{{ $totalKonsultasi }}</div>
|
||||||
|
<div class="stat-sub">{{ $konsultasiHariIni }} konsultasi hari ini</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Data Penyakit</div>
|
||||||
|
<div class="stat-val">{{ $totalPenyakit }}</div>
|
||||||
|
<div class="stat-sub">{{ $totalHama }} hama · {{ $totalPenyakitOnly }} penyakit</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Total Gejala</div>
|
||||||
|
<div class="stat-val">{{ $totalGejala }}</div>
|
||||||
|
<div class="stat-sub">{{ $totalRule }} rule aktif</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Terdeteksi</div>
|
||||||
|
<div class="stat-val green">{{ $totalTerdeteksi }}</div>
|
||||||
|
<div class="stat-sub">{{ $totalTidakTerdeteksi }} tidak terdeteksi</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:1.25rem; margin-bottom:1.25rem;">
|
||||||
|
|
||||||
|
{{-- PENYAKIT TERBANYAK --}}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">Penyakit Paling Sering Terdeteksi</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@forelse($penyakitTerbanyak as $item)
|
||||||
|
<div style="margin-bottom:0.85rem;">
|
||||||
|
<div style="display:flex; justify-content:space-between; margin-bottom:4px;">
|
||||||
|
<span style="font-size:0.82rem; font-weight:500;">{{ $item->penyakit->nama ?? '-' }}</span>
|
||||||
|
<span style="font-size:0.78rem; color:var(--text-2);">{{ $item->total }}x</span>
|
||||||
|
</div>
|
||||||
|
@php $persen = $penyakitTerbanyak->first()->total > 0 ? ($item->total / $penyakitTerbanyak->first()->total * 100) : 0; @endphp
|
||||||
|
<div style="height:6px; background:var(--border); border-radius:3px; overflow:hidden;">
|
||||||
|
<div style="height:100%; width:{{ $persen }}%; background:var(--red); border-radius:3px; transition: width 0.8s ease;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<p style="color:var(--text-3); font-size:0.83rem; text-align:center; padding:1rem 0;">Belum ada data konsultasi.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- KONSULTASI TERBARU --}}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">Konsultasi Terbaru</div>
|
||||||
|
<a href="{{ route('admin.konsultasi.index') }}" class="btn btn-sm">Lihat Semua</a>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Kode</th><th>Tanggal</th><th>Hasil</th><th>CF</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($konsultasiTerbaru as $k)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{{ route('admin.konsultasi.show', $k->id) }}" style="font-family:monospace; font-size:0.75rem; color:var(--red); text-decoration:none;">
|
||||||
|
{{ $k->kode_konsultasi }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td style="font-size:0.78rem; color:var(--text-2);">
|
||||||
|
{{ \Carbon\Carbon::parse($k->tanggal)->format('d M, H:i') }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($k->hasilUtama && $k->hasilUtama->penyakit)
|
||||||
|
<span class="badge {{ $k->hasilUtama->penyakit->jenis === 'Hama' ? 'badge-hama' : 'badge-penyakit' }}">
|
||||||
|
{{ $k->hasilUtama->penyakit->nama }}
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span style="color:var(--text-3); font-size:0.78rem;">—</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($k->hasilUtama)
|
||||||
|
<span class="badge badge-mono">{{ number_format($k->hasilUtama->persentase, 1) }}%</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="4" style="text-align:center; color:var(--text-3); padding:2rem;">Belum ada konsultasi.</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- GRAFIK --}}
|
||||||
|
<div style="display:grid; grid-template-columns:2fr 1fr; gap:1.25rem;">
|
||||||
|
|
||||||
|
{{-- Grafik Konsultasi per Bulan --}}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">Grafik Konsultasi per Bulan</div>
|
||||||
|
<span style="font-size:0.72rem; color:var(--text-3);">{{ \Carbon\Carbon::now()->year }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="chartKonsultasi"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Grafik Donut Hama vs Penyakit --}}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">Komposisi Diagnosa</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="display:flex; flex-direction:column; align-items:center; gap:16px;">
|
||||||
|
<div style="position:relative; width:160px; height:160px;">
|
||||||
|
<canvas id="chartDonut"></canvas>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; flex-direction:column; gap:6px; width:100%;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; font-size:0.78rem;">
|
||||||
|
<span style="width:12px; height:12px; border-radius:3px; background:#C0392B; display:inline-block;"></span>
|
||||||
|
<span style="color:var(--text-2); flex:1;">Penyakit</span>
|
||||||
|
<span style="font-weight:600;">{{ $totalPenyakitOnly }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; font-size:0.78rem;">
|
||||||
|
<span style="width:12px; height:12px; border-radius:3px; background:#F39C12; display:inline-block;"></span>
|
||||||
|
<span style="color:var(--text-2); flex:1;">Hama</span>
|
||||||
|
<span style="font-weight:600;">{{ $totalHama }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; font-size:0.78rem;">
|
||||||
|
<span style="width:12px; height:12px; border-radius:3px; background:#27AE60; display:inline-block;"></span>
|
||||||
|
<span style="color:var(--text-2); flex:1;">Terdeteksi</span>
|
||||||
|
<span style="font-weight:600;">{{ $totalTerdeteksi }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// Data konsultasi per bulan dari controller
|
||||||
|
// Pastikan $konsultasiPerBulan dikirim dari DashboardController
|
||||||
|
const bulanLabels = ['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des'];
|
||||||
|
const konsultasiData = @json($konsultasiPerBulan ?? array_fill(0, 12, 0));
|
||||||
|
|
||||||
|
// Grafik garis konsultasi per bulan
|
||||||
|
const ctx1 = document.getElementById('chartKonsultasi').getContext('2d');
|
||||||
|
new Chart(ctx1, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: bulanLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Konsultasi',
|
||||||
|
data: konsultasiData,
|
||||||
|
borderColor: '#C0392B',
|
||||||
|
backgroundColor: 'rgba(192,57,43,0.08)',
|
||||||
|
borderWidth: 2.5,
|
||||||
|
pointBackgroundColor: '#C0392B',
|
||||||
|
pointRadius: 4,
|
||||||
|
pointHoverRadius: 6,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: '#1A1A2E',
|
||||||
|
titleColor: '#fff',
|
||||||
|
bodyColor: '#ccc',
|
||||||
|
padding: 10,
|
||||||
|
cornerRadius: 8,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { grid: { display: false }, ticks: { font: { family: 'Poppins', size: 11 }, color: '#9CA3AF' } },
|
||||||
|
y: { grid: { color: '#F0F0F0' }, ticks: { font: { family: 'Poppins', size: 11 }, color: '#9CA3AF', stepSize: 1 }, beginAtZero: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Grafik donut
|
||||||
|
const ctx2 = document.getElementById('chartDonut').getContext('2d');
|
||||||
|
new Chart(ctx2, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: ['Penyakit', 'Hama', 'Terdeteksi'],
|
||||||
|
datasets: [{
|
||||||
|
data: [{{ $totalPenyakitOnly }}, {{ $totalHama }}, {{ $totalTerdeteksi }}],
|
||||||
|
backgroundColor: ['#C0392B', '#F39C12', '#27AE60'],
|
||||||
|
borderWidth: 0,
|
||||||
|
hoverOffset: 6,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
cutout: '68%',
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: '#1A1A2E',
|
||||||
|
titleColor: '#fff',
|
||||||
|
bodyColor: '#ccc',
|
||||||
|
padding: 10,
|
||||||
|
cornerRadius: 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
{{-- resources/views/admin/gejala/form.blade.php
|
||||||
|
KEGUNAAN: Form tambah dan edit gejala. --}}
|
||||||
|
@extends('admin.layouts.app')
|
||||||
|
@section('title', isset($gejala) ? 'Edit Gejala' : 'Tambah Gejala')
|
||||||
|
@section('content')
|
||||||
|
@php $isEdit = isset($gejala); @endphp
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="breadcrumb">Admin / <a href="{{ route('admin.gejala.index') }}">Data Gejala</a> / {{ $isEdit ? 'Edit' : 'Tambah' }}</div>
|
||||||
|
<h1>{{ $isEdit ? 'Edit Gejala: '.$gejala->kode : 'Tambah Gejala Baru' }}</h1>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('admin.gejala.index') }}" class="btn">← Kembali</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="max-width:580px;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><div class="card-title">Form {{ $isEdit ? 'Edit' : 'Tambah' }} Gejala</div></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ $isEdit ? route('admin.gejala.update', $gejala->id) : route('admin.gejala.store') }}">
|
||||||
|
@csrf
|
||||||
|
@if($isEdit) @method('PUT') @endif
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Kode Gejala *</label>
|
||||||
|
<input type="text" name="kode" value="{{ old('kode', $gejala->kode ?? '') }}" placeholder="G61" maxlength="10" {{ $isEdit ? 'readonly style=background:#f8f9fa' : '' }}>
|
||||||
|
@error('kode') <div class="form-error">{{ $message }}</div> @enderror
|
||||||
|
<div class="form-hint">Format: G01–G99. Tidak bisa diubah setelah disimpan.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nama Gejala *</label>
|
||||||
|
<input type="text" name="nama" value="{{ old('nama', $gejala->nama ?? '') }}" placeholder="Contoh: Daun muda mengeriting">
|
||||||
|
@error('nama') <div class="form-error">{{ $message }}</div> @enderror
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Bagian tanaman</label>
|
||||||
|
<select name="bagian" class="form-input" required>
|
||||||
|
<option value="daun" {{ old('bagian', $gejala->bagian ?? '') == 'daun' ? 'selected' : '' }}> Daun</option>
|
||||||
|
<option value="batang" {{ old('bagian', $gejala->bagian ?? '') == 'batang' ? 'selected' : '' }}> Batang</option>
|
||||||
|
<option value="seluruh" {{ old('bagian', $gejala->bagian ?? '') == 'seluruh' ? 'selected' : '' }}> Seluruh tanaman</option>
|
||||||
|
<option value="buah" {{ old('bagian', $gejala->bagian ?? '') == 'buah' ? 'selected' : '' }}> Buah</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Deskripsi (opsional)</label>
|
||||||
|
<textarea name="deskripsi" rows="3" placeholder="Keterangan tambahan...">{{ old('deskripsi', $gejala->deskripsi ?? '') }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex; gap:10px; justify-content:flex-end;">
|
||||||
|
<a href="{{ route('admin.gejala.index') }}" class="btn">Batal</a>
|
||||||
|
<button type="submit" class="btn btn-primary">{{ $isEdit ? 'Simpan Perubahan' : 'Simpan' }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
{{-- resources/views/admin/gejala/index.blade.php
|
||||||
|
KEGUNAAN: Halaman daftar semua gejala.
|
||||||
|
Admin bisa tambah, edit, dan hapus gejala. --}}
|
||||||
|
@extends('admin.layouts.app')
|
||||||
|
@section('title', 'Data Gejala')
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="breadcrumb">Admin / Data Gejala</div>
|
||||||
|
<h1>Data Gejala</h1>
|
||||||
|
<p>Kelola daftar gejala yang digunakan dalam proses diagnosa.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('admin.gejala.create') }}" class="btn btn-primary">+ Tambah Gejala</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">Daftar Gejala ({{ $gejala->total() }})</div>
|
||||||
|
<form method="GET">
|
||||||
|
<div class="search-box">
|
||||||
|
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="color:var(--text-3)"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||||||
|
<input type="text" name="search" placeholder="Cari nama gejala..." value="{{ request('search') }}">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Kode</th><th>Nama Gejala</th><th>Dipakai di</th><th>Aksi</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($gejala as $g)
|
||||||
|
<tr>
|
||||||
|
<td><span class="badge badge-mono">{{ $g->kode }}</span></td>
|
||||||
|
<td>{{ $g->nama }}</td>
|
||||||
|
<td>
|
||||||
|
@foreach($g->rules->take(2) as $rule)
|
||||||
|
<span class="badge {{ ($rule->penyakit->jenis ?? '') === 'Hama' ? 'badge-hama' : 'badge-penyakit' }}" style="margin-right:3px;">
|
||||||
|
{{ $rule->penyakit->nama ?? '-' }}
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
@if($g->rules->count() > 2)
|
||||||
|
<span style="font-size:0.72rem; color:var(--text-3)">+{{ $g->rules->count() - 2 }}</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style="display:flex; gap:6px;">
|
||||||
|
<a href="{{ route('admin.gejala.edit', $g->id) }}" class="btn btn-sm">Edit</a>
|
||||||
|
<form method="POST" action="{{ route('admin.gejala.destroy', $g->id) }}" onsubmit="return confirm('Hapus gejala {{ $g->kode }}?')">
|
||||||
|
@csrf @method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger">Hapus</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="4" style="text-align:center; padding:2rem; color:var(--text-3);">Tidak ada data.</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="pagination-wrap">
|
||||||
|
<div class="pagination-info">Menampilkan {{ $gejala->firstItem() }}–{{ $gejala->lastItem() }} dari {{ $gejala->total() }} data</div>
|
||||||
|
{{ $gejala->links() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
{{-- resources/views/admin/konsultasi/index.blade.php
|
||||||
|
KEGUNAAN: Halaman pantau semua riwayat konsultasi user.
|
||||||
|
Admin bisa lihat siapa yang konsultasi, hasilnya apa,
|
||||||
|
dan berapa nilai CF-nya. Read-only, tidak ada edit/hapus. --}}
|
||||||
|
@extends('admin.layouts.app')
|
||||||
|
@section('title', 'Riwayat Konsultasi')
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="breadcrumb">Admin / Riwayat Konsultasi</div>
|
||||||
|
<h1>Riwayat Konsultasi</h1>
|
||||||
|
<p>Semua sesi konsultasi yang telah dilakukan pengguna.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">Semua Konsultasi ({{ $konsultasi->total() }})</div>
|
||||||
|
<form method="GET">
|
||||||
|
<div class="search-box">
|
||||||
|
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="color:var(--text-3)"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||||||
|
<input type="text" name="search" placeholder="Cari kode konsultasi..." value="{{ request('search') }}">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Kode</th><th>Pengguna</th><th>Tanggal</th><th>Hasil Diagnosa</th><th style="text-align:center">CF</th><th style="text-align:center">Status</th><th>Aksi</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($konsultasi as $k)
|
||||||
|
<tr>
|
||||||
|
<td><span style="font-family:monospace; font-size:0.75rem; color:var(--red);">{{ $k->kode_konsultasi }}</span></td>
|
||||||
|
<td style="font-size:0.83rem;">{{ $k->user->name ?? 'Guest' }}</td>
|
||||||
|
<td style="font-size:0.78rem; color:var(--text-2);">{{ \Carbon\Carbon::parse($k->tanggal)->format('d M Y, H:i') }}</td>
|
||||||
|
<td>
|
||||||
|
@if($k->hasilUtama && $k->hasilUtama->penyakit)
|
||||||
|
<span class="badge {{ $k->hasilUtama->penyakit->jenis === 'Hama' ? 'badge-hama' : 'badge-penyakit' }}">{{ $k->hasilUtama->penyakit->nama }}</span>
|
||||||
|
@if($k->hasilDiagnosa->count() > 1)
|
||||||
|
<span style="font-size:0.72rem; color:var(--text-3); margin-left:4px;">+{{ $k->hasilDiagnosa->count() - 1 }} lainnya</span>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<span style="color:var(--text-3); font-size:0.8rem;">Tidak terdeteksi</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td style="text-align:center;">
|
||||||
|
@if($k->hasilUtama)
|
||||||
|
<span class="badge badge-mono">{{ number_format($k->hasilUtama->persentase, 1) }}%</span>
|
||||||
|
@else <span style="color:var(--text-3);">—</span> @endif
|
||||||
|
</td>
|
||||||
|
<td style="text-align:center;">
|
||||||
|
@if($k->status === 'selesai')
|
||||||
|
<span class="badge badge-success">Selesai</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-warning">Tidak Terdeteksi</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td><a href="{{ route('admin.konsultasi.show', $k->id) }}" class="btn btn-sm">Detail</a></td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="7" style="text-align:center; padding:2rem; color:var(--text-3);">Belum ada data konsultasi.</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="pagination-wrap">
|
||||||
|
<div class="pagination-info">Menampilkan {{ $konsultasi->firstItem() }}–{{ $konsultasi->lastItem() }} dari {{ $konsultasi->total() }} data</div>
|
||||||
|
{{ $konsultasi->links() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
{{-- resources/views/admin/konsultasi/show.blade.php
|
||||||
|
KEGUNAAN: Halaman detail satu sesi konsultasi.
|
||||||
|
Menampilkan gejala yang dipilih, hasil diagnosa lengkap,
|
||||||
|
dan log perhitungan CF step-by-step. --}}
|
||||||
|
@extends('admin.layouts.app')
|
||||||
|
@section('title', 'Detail Konsultasi')
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="breadcrumb">Admin / <a href="{{ route('admin.konsultasi.index') }}">Riwayat Konsultasi</a> / Detail</div>
|
||||||
|
<h1>Detail Konsultasi</h1>
|
||||||
|
<p style="font-family:monospace;">{{ $konsultasi->kode_konsultasi }}</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('admin.konsultasi.index') }}" class="btn">← Kembali</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:1.25rem; margin-bottom:1.25rem;">
|
||||||
|
{{-- Info Konsultasi --}}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><div class="card-title">Informasi Konsultasi</div></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table style="font-size:0.83rem;">
|
||||||
|
<tr><td style="color:var(--text-2); padding:5px 0; width:130px;">Kode</td><td style="font-family:monospace; font-weight:600;">{{ $konsultasi->kode_konsultasi }}</td></tr>
|
||||||
|
<tr><td style="color:var(--text-2); padding:5px 0;">Pengguna</td><td>{{ $konsultasi->user->name ?? 'Guest' }}</td></tr>
|
||||||
|
<tr><td style="color:var(--text-2); padding:5px 0;">Tanggal</td><td>{{ \Carbon\Carbon::parse($konsultasi->tanggal)->format('d M Y, H:i') }}</td></tr>
|
||||||
|
<tr><td style="color:var(--text-2); padding:5px 0;">Status</td>
|
||||||
|
<td>
|
||||||
|
@if($konsultasi->status === 'selesai')
|
||||||
|
<span class="badge badge-success">Selesai</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-warning">Tidak Terdeteksi</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr><td style="color:var(--text-2); padding:5px 0;">Jumlah Gejala</td><td>{{ $konsultasi->gejalaList->count() }} gejala dipilih</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Gejala Dipilih --}}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><div class="card-title">Gejala yang Dipilih</div></div>
|
||||||
|
<div class="card-body" style="max-height:200px; overflow-y:auto;">
|
||||||
|
@foreach($konsultasi->gejalaList as $kg)
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:6px; font-size:0.82rem;">
|
||||||
|
<span class="badge badge-mono">{{ $kg->gejala->kode ?? '-' }}</span>
|
||||||
|
{{ $kg->gejala->nama ?? '-' }}
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Hasil Diagnosa --}}
|
||||||
|
<div class="card" style="margin-bottom:1.25rem;">
|
||||||
|
<div class="card-header"><div class="card-title">Hasil Diagnosa</div></div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Ranking</th><th>Penyakit</th><th>Nilai CF</th><th>Persentase</th><th>Interpretasi</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($konsultasi->hasilDiagnosa as $hasil)
|
||||||
|
<tr>
|
||||||
|
<td style="text-align:center; font-weight:700; color:{{ $hasil->ranking === 1 ? 'var(--red)' : 'var(--text-2)' }};">#{{ $hasil->ranking }}</td>
|
||||||
|
<td>
|
||||||
|
<div style="font-weight:500;">{{ $hasil->penyakit->nama ?? '-' }}</div>
|
||||||
|
<span class="badge {{ ($hasil->penyakit->jenis ?? '') === 'Hama' ? 'badge-hama' : 'badge-penyakit' }}">{{ $hasil->penyakit->jenis ?? '-' }}</span>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge badge-mono">{{ number_format($hasil->nilai_cf_akhir, 4) }}</span></td>
|
||||||
|
<td>
|
||||||
|
<div class="cf-bar-wrap">
|
||||||
|
<div class="cf-bar-bg"><div class="cf-bar-fill" style="width:{{ $hasil->persentase }}%"></div></div>
|
||||||
|
<span style="font-size:0.82rem; font-weight:600;">{{ number_format($hasil->persentase, 2) }}%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@php
|
||||||
|
$cf = $hasil->nilai_cf_akhir;
|
||||||
|
$label = $cf >= 0.81 ? 'Sangat Yakin' : ($cf >= 0.61 ? 'Yakin' : ($cf >= 0.41 ? 'Cukup Yakin' : ($cf >= 0.21 ? 'Mungkin' : 'Tidak Yakin')));
|
||||||
|
@endphp
|
||||||
|
<span class="badge badge-success">{{ $label }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="5" style="text-align:center; padding:1.5rem; color:var(--text-3);">Tidak ada penyakit terdeteksi.</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Log Perhitungan CF --}}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">Log Perhitungan CF</div>
|
||||||
|
</div>
|
||||||
|
@foreach($hasilInfer['detail_log'] ?? [] as $idP => $logs)
|
||||||
|
@php
|
||||||
|
$p = \App\Models\Penyakit::find($idP);
|
||||||
|
$cfAkhir = $hasilInfer['semua_cf'][$idP] ?? 0;
|
||||||
|
$terdeteksi = $cfAkhir >= 0.2;
|
||||||
|
@endphp
|
||||||
|
<div style="padding:1rem 1.25rem; border-bottom:1px solid var(--border); {{ $terdeteksi ? 'background:#FAFFFE;' : '' }}">
|
||||||
|
<div style="font-size:0.85rem; font-weight:600; margin-bottom:8px; color:{{ $terdeteksi ? 'var(--green)' : 'var(--text-2)' }};">
|
||||||
|
{{ $terdeteksi ? '✅' : '❌' }} {{ $p->nama ?? '-' }} — CF Akhir: <span style="font-family:monospace;">{{ number_format($cfAkhir, 4) }}</span> ({{ number_format($cfAkhir * 100, 2) }}%)
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table style="font-size:0.78rem;">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Step</th><th>Rule</th><th>Gejala</th><th>CF Pakar</th><th>CF Individu</th><th>CF Sesudah</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($logs as $log)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $log['step'] }}</td>
|
||||||
|
<td><span class="badge badge-mono">{{ $log['rule'] }}</span></td>
|
||||||
|
<td><span class="badge badge-mono" style="margin-right:4px;">{{ $log['kode_gejala'] }}</span>{{ \Illuminate\Support\Str::limit($log['nama_gejala'], 30) }}</td>
|
||||||
|
<td style="text-align:center;">{{ $log['cf_pakar'] }}</td>
|
||||||
|
<td style="text-align:center; font-family:monospace;">{{ $log['cf_individu'] }}</td>
|
||||||
|
<td style="text-align:center; font-family:monospace; font-weight:600; color:var(--red);">{{ $log['cf_sesudah'] }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
{{-- resources/views/admin/layouts/app.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', 'Admin') — RedGuard</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
* { font-family: 'Poppins', sans-serif !important; }
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
:root {
|
||||||
|
--red: #C0392B;
|
||||||
|
--red-dark: #A93226;
|
||||||
|
--red-dim: #FDEDEC;
|
||||||
|
--red-bdr: #FADBD8;
|
||||||
|
--green: #27AE60;
|
||||||
|
--green-dim: #EAFAF1;
|
||||||
|
--yellow-dim: #FEF9E7;
|
||||||
|
--yellow-txt: #B7770D;
|
||||||
|
--yellow-bdr: #F9E79F;
|
||||||
|
--bg: #F0F4F0;
|
||||||
|
--white: #FFFFFF;
|
||||||
|
--surface: #F8F9FA;
|
||||||
|
--border: #E8ECF0;
|
||||||
|
--sidebar-bg: #DCEDC8;
|
||||||
|
--sidebar-txt: #33691E;
|
||||||
|
--sidebar-txt2:#558B2F;
|
||||||
|
--sidebar-lbl: #7CB342;
|
||||||
|
--sidebar-act: #33691E;
|
||||||
|
--sidebar-act-bg: rgba(51,105,30,0.12);
|
||||||
|
--sidebar-hover: rgba(51,105,30,0.07);
|
||||||
|
--dark: #1A1A2E;
|
||||||
|
--text: #2C3038;
|
||||||
|
--text-2: #6B7280;
|
||||||
|
--text-3: #9CA3AF;
|
||||||
|
--shadow-sm: 0 2px 8px rgba(0,0,0,0.07);
|
||||||
|
--shadow-md: 0 4px 16px rgba(0,0,0,0.10);
|
||||||
|
--shadow-card: 0 2px 12px rgba(0,0,0,0.08);
|
||||||
|
--sidebar-w: 220px;
|
||||||
|
--topbar-h: 56px;
|
||||||
|
}
|
||||||
|
html, body { height: 100%; font-family: 'Poppins', sans-serif; background: var(--bg); color: var(--text); }
|
||||||
|
|
||||||
|
/* TOPBAR */
|
||||||
|
.topbar {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||||
|
height: var(--topbar-h);
|
||||||
|
background: var(--white);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
.brand { display: flex; align-items: center; gap: 8px; text-decoration: none; }
|
||||||
|
.brand-icon {
|
||||||
|
width: 32px; height: 32px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
/* No background — just the chili emoji */
|
||||||
|
}
|
||||||
|
.brand-name { font-size: 1rem; font-weight: 700; color: var(--dark); }
|
||||||
|
.brand-name span { color: var(--red); }
|
||||||
|
.brand-badge { font-size: 0.6rem; background: var(--red-dim); color: var(--red); border: 1px solid var(--red-bdr); padding: 2px 8px; border-radius: 100px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; }
|
||||||
|
.topbar-right { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.admin-name { font-size: 0.82rem; color: var(--text-2); }
|
||||||
|
.avatar { width: 32px; height: 32px; background: var(--red); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 0.78rem; font-weight: 700; box-shadow: var(--shadow-sm); }
|
||||||
|
.btn-logout { font-size: 0.78rem; padding: 5px 14px; border-radius: 100px; border: 1px solid var(--border); background: var(--white); color: var(--text-2); cursor: pointer; font-family: 'Poppins', sans-serif; text-decoration: none; transition: all 0.15s; }
|
||||||
|
.btn-logout:hover { background: var(--red-dim); color: var(--red); border-color: var(--red-bdr); }
|
||||||
|
|
||||||
|
/* SIDEBAR */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed; top: var(--topbar-h); left: 0; bottom: 0;
|
||||||
|
width: var(--sidebar-w);
|
||||||
|
background: var(--sidebar-bg);
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 2px 0 12px rgba(0,0,0,0.08);
|
||||||
|
border-right: 1px solid #C5E1A5;
|
||||||
|
}
|
||||||
|
.nav-label { font-size: 0.6rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--sidebar-lbl); padding: 0 1.25rem; margin-bottom: 0.4rem; }
|
||||||
|
.nav-section { margin-bottom: 1.5rem; }
|
||||||
|
.nav-item { display: flex; align-items: center; gap: 10px; padding: 8px 1rem; margin: 2px 10px; border-radius: 8px; cursor: pointer; font-size: 0.82rem; color: var(--sidebar-txt2); font-weight: 500; text-decoration: none; transition: all 0.15s; }
|
||||||
|
.nav-item:hover { background: var(--sidebar-hover); color: var(--sidebar-act); }
|
||||||
|
.nav-item.active { background: var(--sidebar-act-bg); color: var(--sidebar-act); font-weight: 600; box-shadow: 0 1px 4px rgba(51,105,30,0.1); }
|
||||||
|
.nav-item svg { opacity: 0.7; flex-shrink: 0; }
|
||||||
|
.nav-item.active svg { opacity: 1; }
|
||||||
|
|
||||||
|
/* MAIN */
|
||||||
|
.main { margin-left: var(--sidebar-w); margin-top: var(--topbar-h); padding: 1.75rem; min-height: calc(100vh - var(--topbar-h)); }
|
||||||
|
|
||||||
|
/* PAGE HEADER */
|
||||||
|
.page-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1.5rem; }
|
||||||
|
.page-header-left h1 { font-size: 1.2rem; font-weight: 700; color: var(--dark); letter-spacing: -0.02em; }
|
||||||
|
.page-header-left p { font-size: 0.82rem; color: var(--text-2); margin-top: 2px; }
|
||||||
|
.breadcrumb { font-size: 0.72rem; color: var(--text-3); margin-bottom: 4px; }
|
||||||
|
.breadcrumb a { color: var(--red); text-decoration: none; }
|
||||||
|
|
||||||
|
/* CARDS */
|
||||||
|
.card { background: var(--white); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; margin-bottom: 1.25rem; box-shadow: var(--shadow-card); }
|
||||||
|
.card-header { padding: 1rem 1.25rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.card-title { font-size: 0.9rem; font-weight: 600; color: var(--dark); }
|
||||||
|
.card-body { padding: 1.25rem; }
|
||||||
|
|
||||||
|
/* STATS */
|
||||||
|
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.25rem; }
|
||||||
|
.stat-card { background: var(--white); border: 1px solid var(--border); border-radius: 12px; padding: 1.1rem 1.25rem; box-shadow: var(--shadow-card); transition: box-shadow 0.2s, transform 0.2s; }
|
||||||
|
.stat-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
|
||||||
|
.stat-label { font-size: 0.75rem; color: var(--text-2); margin-bottom: 6px; font-weight: 500; }
|
||||||
|
.stat-val { font-size: 1.8rem; font-weight: 700; color: var(--dark); line-height: 1; }
|
||||||
|
.stat-val.red { color: var(--red); }
|
||||||
|
.stat-val.green { color: var(--green); }
|
||||||
|
.stat-sub { font-size: 0.72rem; color: var(--text-3); margin-top: 4px; }
|
||||||
|
|
||||||
|
/* TABLE */
|
||||||
|
.table-wrap { overflow-x: auto; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 0.83rem; }
|
||||||
|
thead th { padding: 10px 14px; text-align: left; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-2); background: var(--surface); border-bottom: 1px solid var(--border); }
|
||||||
|
tbody td { padding: 11px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
||||||
|
tbody tr:last-child td { border-bottom: none; }
|
||||||
|
tbody tr:hover td { background: #FAFBFC; }
|
||||||
|
|
||||||
|
/* BADGES */
|
||||||
|
.badge { display: inline-flex; align-items: center; font-size: 0.7rem; font-weight: 600; padding: 3px 9px; border-radius: 100px; white-space: nowrap; }
|
||||||
|
.badge-hama { background: var(--yellow-dim); color: var(--yellow-txt); border: 1px solid var(--yellow-bdr); }
|
||||||
|
.badge-penyakit { background: var(--red-dim); color: var(--red); border: 1px solid var(--red-bdr); }
|
||||||
|
.badge-success { background: var(--green-dim); color: var(--green); border: 1px solid #A9DFBF; }
|
||||||
|
.badge-warning { background: var(--yellow-dim); color: var(--yellow-txt); border: 1px solid var(--yellow-bdr); }
|
||||||
|
.badge-mono { font-family: monospace; background: var(--surface); color: var(--text-2); border: 1px solid var(--border); font-size: 0.72rem; }
|
||||||
|
|
||||||
|
/* BUTTONS */
|
||||||
|
.btn { display: inline-flex; align-items: center; gap: 6px; font-family: 'Poppins', sans-serif; font-size: 0.82rem; font-weight: 500; padding: 8px 16px; border-radius: 8px; border: 1px solid var(--border); background: var(--white); color: var(--text); cursor: pointer; text-decoration: none; transition: all 0.15s; box-shadow: var(--shadow-sm); }
|
||||||
|
.btn:hover { background: var(--surface); box-shadow: var(--shadow-md); }
|
||||||
|
.btn-primary { background: var(--red); color: #fff; border-color: var(--red); }
|
||||||
|
.btn-primary:hover { background: var(--red-dark); border-color: var(--red-dark); }
|
||||||
|
.btn-sm { font-size: 0.75rem; padding: 5px 11px; }
|
||||||
|
.btn-danger { color: var(--red); border-color: var(--red-bdr); background: var(--red-dim); }
|
||||||
|
.btn-danger:hover { background: var(--red); color: #fff; }
|
||||||
|
|
||||||
|
/* FORMS */
|
||||||
|
.form-group { margin-bottom: 1rem; }
|
||||||
|
.form-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||||
|
.form-row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; }
|
||||||
|
.form-label { display: block; font-size: 0.78rem; font-weight: 600; color: var(--dark); margin-bottom: 6px; }
|
||||||
|
input[type="text"], input[type="email"], input[type="number"], input[type="password"], select, textarea { width: 100%; padding: 9px 12px; border: 1px solid var(--border); border-radius: 8px; font-family: 'Poppins', sans-serif; font-size: 0.85rem; color: var(--dark); background: var(--white); outline: none; transition: border-color 0.15s; }
|
||||||
|
input:focus, select:focus, textarea:focus { border-color: var(--red); box-shadow: 0 0 0 3px rgba(192,57,43,0.08); }
|
||||||
|
.form-hint { font-size: 0.72rem; color: var(--text-3); margin-top: 4px; }
|
||||||
|
.form-error { font-size: 0.75rem; color: var(--red); margin-top: 4px; }
|
||||||
|
|
||||||
|
/* ALERTS */
|
||||||
|
.alert { padding: 10px 14px; border-radius: 8px; font-size: 0.83rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 8px; box-shadow: var(--shadow-sm); }
|
||||||
|
.alert-success { background: var(--green-dim); color: #1E8449; border: 1px solid #A9DFBF; }
|
||||||
|
.alert-danger { background: var(--red-dim); color: var(--red); border: 1px solid var(--red-bdr); }
|
||||||
|
|
||||||
|
/* CF BAR */
|
||||||
|
.cf-bar-wrap { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.cf-bar-bg { flex: 1; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; max-width: 80px; }
|
||||||
|
.cf-bar-fill { height: 100%; background: var(--red); border-radius: 3px; }
|
||||||
|
|
||||||
|
/* SEARCH BOX */
|
||||||
|
.search-box { display: flex; align-items: center; gap: 8px; border: 1px solid var(--border); border-radius: 8px; background: var(--white); padding: 0 12px; box-shadow: var(--shadow-sm); }
|
||||||
|
.search-box input { border: none; background: none; box-shadow: none; width: 200px; font-size: 0.82rem; padding: 7px 0; }
|
||||||
|
.search-box input:focus { box-shadow: none; border: none; }
|
||||||
|
|
||||||
|
/* PAGINATION */
|
||||||
|
.pagination-wrap { padding: 12px 14px; border-top: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||||||
|
.pagination-info { font-size: 0.78rem; color: var(--text-2); }
|
||||||
|
|
||||||
|
/* PAGINATION LINKS - Laravel default override */
|
||||||
|
nav[aria-label="Pagination Navigation"] { display: flex; align-items: center; }
|
||||||
|
nav[aria-label="Pagination Navigation"] span,
|
||||||
|
nav[aria-label="Pagination Navigation"] a {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
min-width: 32px; height: 32px; padding: 0 8px;
|
||||||
|
margin: 0 2px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.78rem; font-weight: 500;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--white);
|
||||||
|
color: var(--text-2);
|
||||||
|
transition: all 0.15s;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
nav[aria-label="Pagination Navigation"] a:hover {
|
||||||
|
background: var(--red-dim);
|
||||||
|
color: var(--red);
|
||||||
|
border-color: var(--red-bdr);
|
||||||
|
}
|
||||||
|
nav[aria-label="Pagination Navigation"] span[aria-current="page"] > span {
|
||||||
|
background: var(--red);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--red);
|
||||||
|
box-shadow: 0 2px 6px rgba(192,57,43,0.25);
|
||||||
|
}
|
||||||
|
nav[aria-label="Pagination Navigation"] span.cursor-default {
|
||||||
|
color: var(--text-3);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@stack('styles')
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{{-- TOPBAR --}}
|
||||||
|
<header class="topbar">
|
||||||
|
<a class="brand" href="{{ route('admin.dashboard') }}">
|
||||||
|
<div class="brand-icon">🌶️</div>
|
||||||
|
<div class="brand-name"><span>Red</span>Guard</div>
|
||||||
|
<span class="brand-badge">Admin</span>
|
||||||
|
</a>
|
||||||
|
<div class="topbar-right">
|
||||||
|
<span class="admin-name">{{ auth()->user()->name }}</span>
|
||||||
|
<div class="avatar">{{ strtoupper(substr(auth()->user()->name, 0, 2)) }}</div>
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="btn-logout">Keluar</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{{-- SIDEBAR --}}
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="nav-section">
|
||||||
|
<div class="nav-label">Utama</div>
|
||||||
|
<a href="{{ route('admin.dashboard') }}" class="nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||||
|
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M1 1h6v6H1zm8 0h6v6H9zM1 9h6v6H1zm8 0h6v6H9z"/></svg>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="nav-section">
|
||||||
|
<div class="nav-label">Knowledge Base</div>
|
||||||
|
<a href="{{ route('admin.penyakit.index') }}" class="nav-item {{ request()->routeIs('admin.penyakit.*') ? 'active' : '' }}">
|
||||||
|
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill-opacity="0.25"/><circle cx="8" cy="8" r="3"/></svg>
|
||||||
|
Data Penyakit
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.gejala.index') }}" class="nav-item {{ request()->routeIs('admin.gejala.*') ? 'active' : '' }}">
|
||||||
|
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M2 3h12v2H2zm0 4h12v2H2zm0 4h8v2H2z"/></svg>
|
||||||
|
Data Gejala
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.rule.index') }}" class="nav-item {{ request()->routeIs('admin.rule.*') ? 'active' : '' }}">
|
||||||
|
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M1 2h14v3H1zm2 4.5h10v2H3zm2 3.5h6v2H5z"/></svg>
|
||||||
|
Rule & Nilai CF
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="nav-section">
|
||||||
|
<div class="nav-label">Data</div>
|
||||||
|
<a href="{{ route('admin.konsultasi.index') }}" class="nav-item {{ request()->routeIs('admin.konsultasi.*') ? 'active' : '' }}">
|
||||||
|
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M2 1h12a1 1 0 011 1v9a1 1 0 01-1 1H5l-3 2V2a1 1 0 011-1z"/></svg>
|
||||||
|
Riwayat Konsultasi
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="nav-section">
|
||||||
|
<div class="nav-label">Lainnya</div>
|
||||||
|
<a href="{{ route('konsultasi.index') }}" class="nav-item" target="_blank">
|
||||||
|
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 1a7 7 0 100 14A7 7 0 008 1zm1 10H7V7h2zm0-5H7V4h2z"/></svg>
|
||||||
|
Halaman User
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{{-- MAIN --}}
|
||||||
|
<main class="main">
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="alert alert-success">✅ {{ session('success') }}</div>
|
||||||
|
@endif
|
||||||
|
@if(session('error'))
|
||||||
|
<div class="alert alert-danger">⚠️ {{ session('error') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@yield('content')
|
||||||
|
</main>
|
||||||
|
|
||||||
|
@stack('scripts')
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
{{-- resources/views/admin/rule/form.blade.php --}}
|
||||||
|
@extends('admin.layouts.app')
|
||||||
|
@section('title', isset($rule) ? 'Edit Rule' : 'Tambah Rule')
|
||||||
|
@section('content')
|
||||||
|
@php $isEdit = isset($rule); @endphp
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="breadcrumb">Admin / <a href="{{ route('admin.rule.index') }}">Rule CF</a> / {{ $isEdit ? 'Edit' : 'Tambah' }}</div>
|
||||||
|
<h1>{{ $isEdit ? 'Edit Rule: '.$rule->kode_rule : 'Tambah Rule Baru' }}</h1>
|
||||||
|
<p>Tentukan relasi penyakit–gejala dan nilai keyakinan pakar.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('admin.rule.index') }}" class="btn">← Kembali</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="max-width:620px;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><div class="card-title">Form Rule CF</div></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ $isEdit ? route('admin.rule.update', $rule->id) : route('admin.rule.store') }}">
|
||||||
|
@csrf
|
||||||
|
@if($isEdit) @method('PUT') @endif
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Kode Rule</label>
|
||||||
|
<input type="text" name="kode_rule" value="{{ old('kode_rule', $rule->kode_rule ?? '') }}" placeholder="Kosongkan untuk auto-generate" maxlength="10" {{ $isEdit ? 'readonly style=background:#f8f9fa' : '' }}>
|
||||||
|
<div class="form-hint">Contoh: R061. Biarkan kosong untuk generate otomatis.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Penyakit / Hama *</label>
|
||||||
|
<select name="id_penyakit">
|
||||||
|
<option value="">— Pilih Penyakit —</option>
|
||||||
|
@foreach($penyakit as $p)
|
||||||
|
<option value="{{ $p->id }}" {{ old('id_penyakit', $rule->id_penyakit ?? '') == $p->id ? 'selected' : '' }}>
|
||||||
|
{{ $p->kode }} — {{ $p->nama }} ({{ $p->jenis }})
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('id_penyakit') <div class="form-error">{{ $message }}</div> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Gejala *</label>
|
||||||
|
<select name="id_gejala">
|
||||||
|
<option value="">— Pilih Gejala —</option>
|
||||||
|
@foreach($gejala as $g)
|
||||||
|
<option value="{{ $g->id }}" {{ old('id_gejala', $rule->id_gejala ?? '') == $g->id ? 'selected' : '' }}>
|
||||||
|
{{ $g->kode }} — {{ $g->nama }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('id_gejala') <div class="form-error">{{ $message }}</div> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- MB dan MD --}}
|
||||||
|
<div class="form-row-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nilai MB (Measure of Belief) *</label>
|
||||||
|
<input type="number" name="nilai_mb" id="nilaiMB"
|
||||||
|
value="{{ old('nilai_mb', $rule->nilai_mb ?? '0.5') }}"
|
||||||
|
min="0" max="1" step="0.1"
|
||||||
|
oninput="hitungOtomatis()"
|
||||||
|
placeholder="0.0 – 1.0">
|
||||||
|
<div class="form-hint">Tingkat keyakinan pakar (0–1).</div>
|
||||||
|
@error('nilai_mb') <div class="form-error">{{ $message }}</div> @enderror
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nilai MD (Measure of Disbelief) *</label>
|
||||||
|
<input type="number" name="nilai_md" id="nilaiMD"
|
||||||
|
value="{{ old('nilai_md', $rule->nilai_md ?? '0.1') }}"
|
||||||
|
min="0" max="1" step="0.1"
|
||||||
|
oninput="hitungOtomatis()"
|
||||||
|
placeholder="0.0 – 1.0">
|
||||||
|
<div class="form-hint">Tingkat ketidakyakinan pakar (0–1).</div>
|
||||||
|
@error('nilai_md') <div class="form-error">{{ $message }}</div> @enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Nilai CF dan Nilai Pakar (otomatis) --}}
|
||||||
|
<div class="form-row-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nilai CF (otomatis)</label>
|
||||||
|
<input type="text" name="nilai_cf" id="nilaiCF"
|
||||||
|
value="{{ old('nilai_cf', $rule->nilai_cf ?? '0.4') }}"
|
||||||
|
readonly style="background:#f8f9fa; font-weight:700; font-size:1.1rem; color:var(--red); text-align:center;">
|
||||||
|
<div class="form-hint">CF = MB − MD (otomatis).</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nilai Pakar (otomatis)</label>
|
||||||
|
<input type="text" name="nilai_pakar" id="nilaiPakar"
|
||||||
|
value="{{ old('nilai_pakar', $rule->nilai_pakar ?? '3') }}"
|
||||||
|
readonly style="background:#f8f9fa; font-weight:700; font-size:1.1rem; color:var(--red); text-align:center;">
|
||||||
|
<div class="form-hint">Skala 1–5 dari nilai CF.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Panduan --}}
|
||||||
|
<div style="background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:0.85rem 1rem; margin-bottom:1rem;">
|
||||||
|
<div style="font-size:0.75rem; font-weight:600; margin-bottom:8px;">Panduan Nilai CF → Nilai Pakar:</div>
|
||||||
|
<div style="display:grid; grid-template-columns:repeat(5,1fr); gap:6px; text-align:center;">
|
||||||
|
<div><div style="font-family:monospace; font-weight:700; color:var(--red); font-size:0.85rem;">≤0.2 → 1</div><div style="color:var(--text-3); font-size:0.68rem; margin-top:2px;">Tidak Yakin</div></div>
|
||||||
|
<div><div style="font-family:monospace; font-weight:700; color:var(--red); font-size:0.85rem;">≤0.4 → 2</div><div style="color:var(--text-3); font-size:0.68rem; margin-top:2px;">Mungkin</div></div>
|
||||||
|
<div><div style="font-family:monospace; font-weight:700; color:var(--red); font-size:0.85rem;">≤0.6 → 3</div><div style="color:var(--text-3); font-size:0.68rem; margin-top:2px;">Cukup Yakin</div></div>
|
||||||
|
<div><div style="font-family:monospace; font-weight:700; color:var(--red); font-size:0.85rem;">≤0.8 → 4</div><div style="color:var(--text-3); font-size:0.68rem; margin-top:2px;">Yakin</div></div>
|
||||||
|
<div><div style="font-family:monospace; font-weight:700; color:var(--red); font-size:0.85rem;">>0.8 → 5</div><div style="color:var(--text-3); font-size:0.68rem; margin-top:2px;">Sangat Yakin</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex; gap:10px; justify-content:flex-end;">
|
||||||
|
<a href="{{ route('admin.rule.index') }}" class="btn">Batal</a>
|
||||||
|
<button type="submit" class="btn btn-primary">{{ $isEdit ? 'Simpan Perubahan' : 'Simpan Rule' }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
function hitungOtomatis() {
|
||||||
|
const mb = parseFloat(document.getElementById('nilaiMB').value) || 0;
|
||||||
|
const md = parseFloat(document.getElementById('nilaiMD').value) || 0;
|
||||||
|
|
||||||
|
// Hitung CF = MB - MD
|
||||||
|
let cf = mb - md;
|
||||||
|
cf = Math.round(cf * 10000) / 10000; // bulatkan 4 desimal
|
||||||
|
|
||||||
|
document.getElementById('nilaiCF').value = cf.toFixed(4);
|
||||||
|
|
||||||
|
// Tentukan nilai pakar dari CF
|
||||||
|
let pakar;
|
||||||
|
if (cf <= 0.2) pakar = 1;
|
||||||
|
else if (cf <= 0.4) pakar = 2;
|
||||||
|
else if (cf <= 0.6) pakar = 3;
|
||||||
|
else if (cf <= 0.8) pakar = 4;
|
||||||
|
else pakar = 5;
|
||||||
|
|
||||||
|
document.getElementById('nilaiPakar').value = pakar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jalankan saat halaman load
|
||||||
|
hitungOtomatis();
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
{{-- resources/views/admin/rule/index.blade.php --}}
|
||||||
|
@extends('admin.layouts.app')
|
||||||
|
@section('title', 'Rule & Nilai CF')
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-left">
|
||||||
|
<div class="breadcrumb">Admin / Rule & Nilai CF</div>
|
||||||
|
<h1>Rule & Nilai CF</h1>
|
||||||
|
<p>Kelola relasi penyakit–gejala dan nilai Certainty Factor pakar.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('admin.rule.create') }}" class="btn btn-primary">+ Tambah Rule</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Filter --}}
|
||||||
|
<div class="card" style="padding:0.85rem 1.25rem; margin-bottom:1rem;">
|
||||||
|
<form method="GET" style="display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
|
||||||
|
<select name="penyakit_id" onchange="this.form.submit()" style="width:220px;">
|
||||||
|
<option value="">— Semua Penyakit —</option>
|
||||||
|
@foreach($semuaPenyakit as $p)
|
||||||
|
<option value="{{ $p->id }}" {{ request('penyakit_id') == $p->id ? 'selected' : '' }}>
|
||||||
|
{{ $p->kode }} — {{ $p->nama }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<div class="search-box">
|
||||||
|
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="color:var(--text-3)"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||||||
|
<input type="text" name="search" placeholder="Cari kode rule..." value="{{ request('search') }}">
|
||||||
|
</div>
|
||||||
|
@if(request('penyakit_id') || request('search'))
|
||||||
|
<a href="{{ route('admin.rule.index') }}" class="btn btn-sm">Reset</a>
|
||||||
|
@endif
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">Daftar Rule ({{ $rules->total() }})</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Kode Rule</th>
|
||||||
|
<th>Penyakit</th>
|
||||||
|
<th>Gejala</th>
|
||||||
|
<th style="text-align:center">Nilai Pakar</th>
|
||||||
|
<th style="text-align:center">MB</th>
|
||||||
|
<th style="text-align:center">MD</th>
|
||||||
|
<th>Nilai CF</th>
|
||||||
|
<th>Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($rules as $rule)
|
||||||
|
<tr>
|
||||||
|
<td><span class="badge badge-mono">{{ $rule->kode_rule }}</span></td>
|
||||||
|
<td>
|
||||||
|
<div style="font-weight:500; font-size:0.82rem;">{{ $rule->penyakit->nama ?? '-' }}</div>
|
||||||
|
<span class="badge {{ ($rule->penyakit->jenis ?? '') === 'Hama' ? 'badge-hama' : 'badge-penyakit' }}">{{ $rule->penyakit->jenis ?? '-' }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-mono" style="margin-right:6px;">{{ $rule->gejala->kode ?? '-' }}</span>
|
||||||
|
<span style="font-size:0.82rem;">{{ $rule->gejala->nama ?? '-' }}</span>
|
||||||
|
</td>
|
||||||
|
<td style="text-align:center;">
|
||||||
|
<span style="font-size:1rem; font-weight:600;">{{ $rule->nilai_pakar }}</span>
|
||||||
|
<span style="font-size:0.72rem; color:var(--text-3);">/5</span>
|
||||||
|
</td>
|
||||||
|
<td style="text-align:center;">
|
||||||
|
<span style="font-size:0.85rem; font-weight:600; color:var(--green, #2d7a4f);">{{ number_format($rule->nilai_mb, 2) }}</span>
|
||||||
|
</td>
|
||||||
|
<td style="text-align:center;">
|
||||||
|
<span style="font-size:0.85rem; font-weight:600; color:var(--red);">{{ number_format($rule->nilai_md, 2) }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="cf-bar-wrap">
|
||||||
|
<div class="cf-bar-bg"><div class="cf-bar-fill" style="width:{{ $rule->nilai_cf * 100 }}%"></div></div>
|
||||||
|
<span class="badge badge-mono">{{ number_format($rule->nilai_cf, 1) }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style="display:flex; gap:6px;">
|
||||||
|
<a href="{{ route('admin.rule.edit', $rule->id) }}" class="btn btn-sm">Edit</a>
|
||||||
|
<form method="POST" action="{{ route('admin.rule.destroy', $rule->id) }}" onsubmit="return confirm('Hapus rule {{ $rule->kode_rule }}?')">
|
||||||
|
@csrf @method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger">Hapus</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="8" style="text-align:center; padding:2rem; color:var(--text-3);">Tidak ada rule.</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="pagination-wrap">
|
||||||
|
<div class="pagination-info">Menampilkan {{ $rules->firstItem() }}–{{ $rules->lastItem() }} dari {{ $rules->total() }} rule</div>
|
||||||
|
{{ $rules->appends(request()->query())->links() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue