update revisi
This commit is contained in:
parent
920e6caf3d
commit
9e1a910c2f
|
|
@ -76,10 +76,12 @@ public function prosesDiagnosis(Request $request)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->route('hasil-diagnosis')
|
session([
|
||||||
->with('diagnosis', $diagnosis)
|
'diagnosis' => $diagnosis,
|
||||||
->with('gejala', $inputNama);
|
'gejala' => $inputNama,
|
||||||
$biodataId = session('biodata_id');
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('hasil-diagnosis');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 halaman hasil
|
// 🔥 halaman hasil
|
||||||
|
|
@ -137,6 +139,7 @@ public function simpanBiodata(Request $request)
|
||||||
'umur_kucing' => 'required|numeric',
|
'umur_kucing' => 'required|numeric',
|
||||||
'jenis_kelamin' => 'required',
|
'jenis_kelamin' => 'required',
|
||||||
'berat_badan' => 'required|numeric',
|
'berat_badan' => 'required|numeric',
|
||||||
|
'alamat' => 'required|in:Ajung,Ambulu,Arjasa,Balung,Bangsalsari,Gumukmas,Jelbuk,Jenggawah,Jombang,Kalisat,Kaliwates,Kencong,Ledokombo,Mayang,Mumbulsari,Pakusari,Panti,Patrang,Puger,Rambipuji,Semboro,Silo,Sukorambi,Sukowono,Sumberbaru,Sumberjambe,Sumbersari,Tanggul,Tempurejo,Umbulsari,Wuluhan',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data = \App\Models\Biodata::create([
|
$data = \App\Models\Biodata::create([
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Biodata;
|
||||||
|
use App\Models\Ulasan;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use ZipArchive;
|
||||||
|
|
||||||
|
class LandingController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$ulasan = Schema::hasTable('ulasans')
|
||||||
|
? Ulasan::where('is_hidden', false)->latest()->take(3)->get()
|
||||||
|
: collect();
|
||||||
|
$range = $request->query('range') === 'week' ? 'week' : 'month';
|
||||||
|
|
||||||
|
return view('landing', [
|
||||||
|
'ulasan' => $ulasan,
|
||||||
|
'diseaseNews' => $this->buildDiseaseNews($range),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildDiseaseNews(string $range): array
|
||||||
|
{
|
||||||
|
$startDate = $range === 'week'
|
||||||
|
? Carbon::now()->subDays(6)->startOfDay()
|
||||||
|
: Carbon::now()->subDays(29)->startOfDay();
|
||||||
|
|
||||||
|
$periodLabel = $range === 'week' ? '7 hari terakhir' : '30 hari terakhir';
|
||||||
|
|
||||||
|
if (!Schema::hasTable('biodata')) {
|
||||||
|
return $this->emptyDiseaseNews($range, $periodLabel, $startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
$diseaseStats = Biodata::query()
|
||||||
|
->select('hasil_diagnosis', DB::raw('COUNT(*) as total'))
|
||||||
|
->whereNotNull('hasil_diagnosis')
|
||||||
|
->where('hasil_diagnosis', '!=', '')
|
||||||
|
->where('created_at', '>=', $startDate)
|
||||||
|
->groupBy('hasil_diagnosis')
|
||||||
|
->orderByDesc('total')
|
||||||
|
->limit(3)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$topDisease = trim((string) ($diseaseStats->first()->hasil_diagnosis ?? ''));
|
||||||
|
|
||||||
|
$areaStats = collect();
|
||||||
|
$areaStatsByDisease = [];
|
||||||
|
if ($topDisease !== '') {
|
||||||
|
$areaStats = Biodata::query()
|
||||||
|
->where('hasil_diagnosis', $topDisease)
|
||||||
|
->where('created_at', '>=', $startDate)
|
||||||
|
->get(['alamat'])
|
||||||
|
->map(fn ($item) => $this->extractArea((string) ($item->alamat ?? '')))
|
||||||
|
->filter()
|
||||||
|
->countBy()
|
||||||
|
->sortDesc()
|
||||||
|
->take(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($diseaseStats as $row) {
|
||||||
|
$disease = trim((string) $row->hasil_diagnosis);
|
||||||
|
if ($disease === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$areas = Biodata::query()
|
||||||
|
->where('hasil_diagnosis', $disease)
|
||||||
|
->where('created_at', '>=', $startDate)
|
||||||
|
->get(['alamat'])
|
||||||
|
->map(fn ($item) => $this->extractArea((string) ($item->alamat ?? '')))
|
||||||
|
->filter()
|
||||||
|
->countBy()
|
||||||
|
->sortDesc()
|
||||||
|
->take(6);
|
||||||
|
|
||||||
|
$areaStatsByDisease[$disease] = [
|
||||||
|
'labels' => $areas->keys()->values(),
|
||||||
|
'data' => $areas->values()->map(fn ($n) => (int) $n)->values(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$knowledge = $this->getDiseaseKnowledge($topDisease);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'range' => $range,
|
||||||
|
'period_label' => $periodLabel,
|
||||||
|
'start_label' => $this->formatDateLabel($startDate),
|
||||||
|
'end_label' => $this->formatDateLabel(Carbon::now()),
|
||||||
|
'top_disease' => $topDisease,
|
||||||
|
'total_cases' => (int) ($diseaseStats->first()->total ?? 0),
|
||||||
|
'disease_labels' => $diseaseStats->pluck('hasil_diagnosis')->map(fn ($name) => trim((string) $name))->values(),
|
||||||
|
'disease_data' => $diseaseStats->pluck('total')->map(fn ($n) => (int) $n)->values(),
|
||||||
|
'area_labels' => $areaStats->keys()->values(),
|
||||||
|
'area_data' => $areaStats->values()->map(fn ($n) => (int) $n)->values(),
|
||||||
|
'area_by_disease' => $areaStatsByDisease,
|
||||||
|
'handling' => $knowledge['pertolongan'] ?? [],
|
||||||
|
'prevention' => $knowledge['pencegahan'] ?? [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function emptyDiseaseNews(string $range, string $periodLabel, Carbon $startDate): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'range' => $range,
|
||||||
|
'period_label' => $periodLabel,
|
||||||
|
'start_label' => $this->formatDateLabel($startDate),
|
||||||
|
'end_label' => $this->formatDateLabel(Carbon::now()),
|
||||||
|
'top_disease' => '',
|
||||||
|
'total_cases' => 0,
|
||||||
|
'disease_labels' => collect(),
|
||||||
|
'disease_data' => collect(),
|
||||||
|
'area_labels' => collect(),
|
||||||
|
'area_data' => collect(),
|
||||||
|
'area_by_disease' => [],
|
||||||
|
'handling' => [],
|
||||||
|
'prevention' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractArea(string $address): string
|
||||||
|
{
|
||||||
|
$address = trim($address);
|
||||||
|
if ($address === '') {
|
||||||
|
return 'Tidak diketahui';
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = array_values(array_filter(array_map('trim', preg_split('/[,;-]+/', $address))));
|
||||||
|
$selected = $parts[0] ?? $address;
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if (preg_match('/\b(kota|kabupaten|kec\.?|kecamatan|kel\.?|kelurahan|desa)\b/i', $part)) {
|
||||||
|
$selected = $part;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$selected = preg_replace('/\s+/', ' ', $selected);
|
||||||
|
|
||||||
|
return mb_convert_case($selected, MB_CASE_TITLE, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatDateLabel(Carbon $date): string
|
||||||
|
{
|
||||||
|
$months = [
|
||||||
|
1 => 'Jan', 2 => 'Feb', 3 => 'Mar', 4 => 'Apr', 5 => 'Mei', 6 => 'Jun',
|
||||||
|
7 => 'Jul', 8 => 'Agu', 9 => 'Sep', 10 => 'Okt', 11 => 'Nov', 12 => 'Des',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $date->format('d') . ' ' . $months[(int) $date->format('n')] . ' ' . $date->format('Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDiseaseKnowledge(string $diseaseName): array
|
||||||
|
{
|
||||||
|
if ($diseaseName === '') {
|
||||||
|
return ['pertolongan' => [], 'pencegahan' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $this->readXlsxRows(public_path('data/Bissmilah lagi.xlsx'));
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$name = trim((string) ($row['Penyakit'] ?? ''));
|
||||||
|
if (mb_strtolower($name) !== mb_strtolower($diseaseName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'pertolongan' => $this->splitRecommendation((string) ($row['Pertolongan'] ?? '')),
|
||||||
|
'pencegahan' => $this->splitRecommendation((string) ($row['Pencegahan'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['pertolongan' => [], 'pencegahan' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function splitRecommendation(string $value): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(array_map('trim', explode(';', $value))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readXlsxRows(string $path): array
|
||||||
|
{
|
||||||
|
if (!is_file($path) || !class_exists(ZipArchive::class)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($path) !== true) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sharedStrings = [];
|
||||||
|
$sharedXml = $zip->getFromName('xl/sharedStrings.xml');
|
||||||
|
if ($sharedXml !== false) {
|
||||||
|
$shared = simplexml_load_string($sharedXml);
|
||||||
|
foreach ($shared->si ?? [] as $item) {
|
||||||
|
$sharedStrings[] = trim((string) ($item->t ?? ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheetXml = $zip->getFromName('xl/worksheets/sheet1.xml');
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
if ($sheetXml === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet = simplexml_load_string($sheetXml);
|
||||||
|
$rows = [];
|
||||||
|
foreach ($sheet->sheetData->row ?? [] as $xmlRow) {
|
||||||
|
$cells = [];
|
||||||
|
foreach ($xmlRow->c as $cell) {
|
||||||
|
$ref = (string) $cell['r'];
|
||||||
|
$column = preg_replace('/\d+/', '', $ref);
|
||||||
|
$value = (string) ($cell->v ?? '');
|
||||||
|
if ((string) $cell['t'] === 's') {
|
||||||
|
$value = $sharedStrings[(int) $value] ?? '';
|
||||||
|
}
|
||||||
|
$cells[$column] = trim($value);
|
||||||
|
}
|
||||||
|
$rows[] = $cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = array_shift($rows) ?? [];
|
||||||
|
return array_values(array_filter(array_map(function ($row) use ($headers) {
|
||||||
|
$mapped = [];
|
||||||
|
foreach ($headers as $column => $header) {
|
||||||
|
if ($header !== '') {
|
||||||
|
$mapped[$header] = $row[$column] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $mapped;
|
||||||
|
}, $rows)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Biodata;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class BiodataTrendSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed dummy diagnosis results so disease trend charts have data.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
Biodata::query()
|
||||||
|
->where('no_telepon', 'like', '08DUMMY%')
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
$diseases = [
|
||||||
|
[
|
||||||
|
'name' => 'Scabies',
|
||||||
|
'category' => 'Parasit',
|
||||||
|
'symptoms' => ['Gatal hebat', 'Kerak pada telinga', 'Bulu rontok', 'Kulit kemerahan'],
|
||||||
|
'weight' => 5,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Feline calicivirus',
|
||||||
|
'category' => 'Virus',
|
||||||
|
'symptoms' => ['Bersin', 'Sariawan', 'Air liur berlebih', 'Nafsu makan menurun'],
|
||||||
|
'weight' => 4,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Jamur/Ringworm',
|
||||||
|
'category' => 'Parasit',
|
||||||
|
'symptoms' => ['Bulu rontok melingkar', 'Kulit bersisik', 'Gatal', 'Kerak pada kulit'],
|
||||||
|
'weight' => 4,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Cacingan',
|
||||||
|
'category' => 'Parasit',
|
||||||
|
'symptoms' => ['Perut membesar', 'Berat badan turun', 'Muntah', 'Diare'],
|
||||||
|
'weight' => 3,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'FLUTD (Feline Lower Urinary Tract Diseases)',
|
||||||
|
'category' => 'Virus / Lingkungan',
|
||||||
|
'symptoms' => ['Sulit buang air kecil', 'Sering ke litter box', 'Urin berdarah', 'Nyeri saat pipis'],
|
||||||
|
'weight' => 3,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Diare Non Spesifik',
|
||||||
|
'category' => 'Virus / Parasit',
|
||||||
|
'symptoms' => ['Diare', 'Lemas', 'Nafsu makan menurun', 'Dehidrasi ringan'],
|
||||||
|
'weight' => 2,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Earmite',
|
||||||
|
'category' => 'Parasit',
|
||||||
|
'symptoms' => ['Telinga kotor', 'Sering menggaruk telinga', 'Bau telinga', 'Kepala sering digelengkan'],
|
||||||
|
'weight' => 2,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$areas = [
|
||||||
|
'Sumbersari',
|
||||||
|
'Kaliwates',
|
||||||
|
'Patrang',
|
||||||
|
'Ajung',
|
||||||
|
'Rambipuji',
|
||||||
|
'Ambulu',
|
||||||
|
'Puger',
|
||||||
|
'Wuluhan',
|
||||||
|
'Arjasa',
|
||||||
|
'Jenggawah',
|
||||||
|
];
|
||||||
|
|
||||||
|
$cats = [
|
||||||
|
['owner' => 'Alya Pratama', 'cat' => 'Milo', 'gender' => 'Jantan', 'breed' => 'Domestik'],
|
||||||
|
['owner' => 'Bima Santoso', 'cat' => 'Luna', 'gender' => 'Betina', 'breed' => 'Persia'],
|
||||||
|
['owner' => 'Citra Dewi', 'cat' => 'Oyen', 'gender' => 'Jantan', 'breed' => 'Domestik'],
|
||||||
|
['owner' => 'Dani Kurniawan', 'cat' => 'Mochi', 'gender' => 'Betina', 'breed' => 'Anggora'],
|
||||||
|
['owner' => 'Eka Lestari', 'cat' => 'Nala', 'gender' => 'Betina', 'breed' => 'Mixdom'],
|
||||||
|
['owner' => 'Farhan Hakim', 'cat' => 'Simba', 'gender' => 'Jantan', 'breed' => 'Persia Medium'],
|
||||||
|
['owner' => 'Gita Maharani', 'cat' => 'Coco', 'gender' => 'Betina', 'breed' => 'Domestik'],
|
||||||
|
['owner' => 'Hendra Wijaya', 'cat' => 'Leo', 'gender' => 'Jantan', 'breed' => 'Maine Coon Mix'],
|
||||||
|
['owner' => 'Intan Permata', 'cat' => 'Mimi', 'gender' => 'Betina', 'breed' => 'Domestik'],
|
||||||
|
['owner' => 'Joko Saputra', 'cat' => 'Tom', 'gender' => 'Jantan', 'breed' => 'British Shorthair Mix'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$weightedDiseases = [];
|
||||||
|
foreach ($diseases as $disease) {
|
||||||
|
for ($i = 0; $i < $disease['weight']; $i++) {
|
||||||
|
$weightedDiseases[] = $disease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$today = Carbon::today();
|
||||||
|
$rowNumber = 1;
|
||||||
|
|
||||||
|
for ($dayOffset = 0; $dayOffset < 30; $dayOffset++) {
|
||||||
|
$casesForDay = 1 + ($dayOffset % 4);
|
||||||
|
|
||||||
|
if (in_array($dayOffset, [0, 1, 2, 6, 13, 20], true)) {
|
||||||
|
$casesForDay++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($case = 0; $case < $casesForDay; $case++) {
|
||||||
|
$cat = $cats[($rowNumber + $case) % count($cats)];
|
||||||
|
$disease = $weightedDiseases[($dayOffset + $case + $rowNumber) % count($weightedDiseases)];
|
||||||
|
$createdAt = $today
|
||||||
|
->copy()
|
||||||
|
->subDays($dayOffset)
|
||||||
|
->setTime(8 + (($case * 3) % 10), (17 + $rowNumber) % 60, 0);
|
||||||
|
|
||||||
|
$rows[] = [
|
||||||
|
'nama_pemilik' => $cat['owner'] . ' ' . str_pad((string) $rowNumber, 2, '0', STR_PAD_LEFT),
|
||||||
|
'nama_kucing' => $cat['cat'],
|
||||||
|
'umur_kucing' => 6 + (($rowNumber * 3) % 72),
|
||||||
|
'jenis_kelamin' => $cat['gender'],
|
||||||
|
'berat_badan' => 2.4 + (($rowNumber % 18) / 10),
|
||||||
|
'ras_kucing' => $cat['breed'],
|
||||||
|
'alamat' => $areas[($dayOffset + $case) % count($areas)],
|
||||||
|
'no_telepon' => '08DUMMY' . str_pad((string) $rowNumber, 5, '0', STR_PAD_LEFT),
|
||||||
|
'hasil_diagnosis' => $disease['name'],
|
||||||
|
'jenis' => $disease['category'],
|
||||||
|
'gejala_dipilih' => json_encode($disease['symptoms'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
|
'created_at' => $createdAt,
|
||||||
|
'updated_at' => $createdAt,
|
||||||
|
];
|
||||||
|
|
||||||
|
$rowNumber++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Biodata::query()->insert($rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
class DatabaseSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
|
@ -16,12 +17,16 @@ class DatabaseSeeder extends Seeder
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// Create Admin User
|
// Create Admin User
|
||||||
User::create([
|
User::query()->updateOrCreate(
|
||||||
'name' => 'Admin PawMedic',
|
['email' => 'admin@pawmedic.app'],
|
||||||
'email' => 'admin@pawmedic.app',
|
[
|
||||||
'password' => \Illuminate\Support\Facades\Hash::make('admin123'),
|
'name' => 'Admin PawMedic',
|
||||||
'email_verified_at' => now(),
|
'password' => Hash::make('admin123'),
|
||||||
]);
|
'email_verified_at' => now(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->call(BiodataTrendSeeder::class);
|
||||||
|
|
||||||
// Optional: Create test user
|
// Optional: Create test user
|
||||||
// User::factory()->create([
|
// User::factory()->create([
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -11,7 +11,7 @@ app = Flask(__name__)
|
||||||
model = joblib.load("../python_artifacts/model.joblib")
|
model = joblib.load("../python_artifacts/model.joblib")
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# LOAD FEATURE
|
# LOAD FEATURE/GEJALA
|
||||||
# =========================
|
# =========================
|
||||||
with open("../python_artifacts/feature_cols.json") as f:
|
with open("../python_artifacts/feature_cols.json") as f:
|
||||||
feature_cols = json.load(f)
|
feature_cols = json.load(f)
|
||||||
|
|
@ -66,7 +66,7 @@ def predict():
|
||||||
print("INPUT VECTOR:", input_data)
|
print("INPUT VECTOR:", input_data)
|
||||||
|
|
||||||
input_df = pd.DataFrame([input_data], columns=feature_cols)
|
input_df = pd.DataFrame([input_data], columns=feature_cols)
|
||||||
|
#melakukan prediksi
|
||||||
hasil = model.predict(input_df)[0]
|
hasil = model.predict(input_df)[0]
|
||||||
penyakit = str(hasil).lower().strip()
|
penyakit = str(hasil).lower().strip()
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@ def predict():
|
||||||
"pertolongan": [],
|
"pertolongan": [],
|
||||||
"pencegahan": []
|
"pencegahan": []
|
||||||
})
|
})
|
||||||
|
#mengembalikan hasil prediksi ke laravel
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"penyakit": hasil,
|
"penyakit": hasil,
|
||||||
"jenis": info["jenis"],
|
"jenis": info["jenis"],
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,71 @@
|
||||||
font-size:13px;
|
font-size:13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.combo-wrap{
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
.combo-input-wrap{
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
.combo-input-wrap input{
|
||||||
|
padding-right:46px;
|
||||||
|
}
|
||||||
|
.combo-toggle{
|
||||||
|
position:absolute;
|
||||||
|
right:14px;
|
||||||
|
top:50%;
|
||||||
|
transform:translateY(-50%);
|
||||||
|
border:none;
|
||||||
|
background:transparent;
|
||||||
|
color:#334155;
|
||||||
|
width:28px;
|
||||||
|
height:28px;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
cursor:pointer;
|
||||||
|
font-size:14px;
|
||||||
|
}
|
||||||
|
.combo-menu{
|
||||||
|
display:none;
|
||||||
|
position:absolute;
|
||||||
|
left:0;
|
||||||
|
right:0;
|
||||||
|
top:calc(100% + 8px);
|
||||||
|
z-index:20;
|
||||||
|
max-height:240px;
|
||||||
|
overflow:auto;
|
||||||
|
padding:8px;
|
||||||
|
border:1px solid #dbe6ef;
|
||||||
|
border-radius:14px;
|
||||||
|
background:#fff;
|
||||||
|
box-shadow:0 18px 44px rgba(15,23,42,0.16);
|
||||||
|
}
|
||||||
|
.combo-wrap.open .combo-menu{
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
.combo-option{
|
||||||
|
width:100%;
|
||||||
|
border:none;
|
||||||
|
background:transparent;
|
||||||
|
color:var(--text-dark);
|
||||||
|
padding:11px 12px;
|
||||||
|
border-radius:10px;
|
||||||
|
text-align:left;
|
||||||
|
font:600 14px var(--ff-body);
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
.combo-option:hover,
|
||||||
|
.combo-option.active{
|
||||||
|
background:var(--primary-light);
|
||||||
|
color:var(--primary-dark);
|
||||||
|
}
|
||||||
|
.combo-empty{
|
||||||
|
padding:12px;
|
||||||
|
color:var(--text-muted);
|
||||||
|
font-size:14px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-row{
|
.form-row{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns:1fr 1fr;
|
grid-template-columns:1fr 1fr;
|
||||||
|
|
@ -343,7 +408,7 @@
|
||||||
|
|
||||||
<!-- FORM CARD -->
|
<!-- FORM CARD -->
|
||||||
<div class="form-card">
|
<div class="form-card">
|
||||||
<form action="{{ route('biodata.simpan') }}" method="POST">
|
<form id="biodataForm" action="{{ route('biodata.simpan') }}" method="POST">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<!-- Nama Pemilik -->
|
<!-- Nama Pemilik -->
|
||||||
|
|
@ -441,15 +506,26 @@
|
||||||
<!-- Alamat -->
|
<!-- Alamat -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="alamat">
|
<label for="alamat">
|
||||||
Alamat
|
Alamat Kecamatan <span class="required">*</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<div class="combo-wrap" id="alamatCombo">
|
||||||
id="alamat"
|
<div class="combo-input-wrap">
|
||||||
name="alamat"
|
<input
|
||||||
placeholder="Masukkan alamat (opsional)"
|
type="text"
|
||||||
rows="3"
|
id="alamat"
|
||||||
></textarea>
|
name="alamat"
|
||||||
<small>Opsional - Untuk keperluan dokumentasi</small>
|
placeholder="Pilih atau cari kecamatan di Jember"
|
||||||
|
required
|
||||||
|
autocomplete="off"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="alamatOptions"
|
||||||
|
>
|
||||||
|
<button type="button" class="combo-toggle" id="alamatToggle" aria-label="Tampilkan pilihan kecamatan">▼</button>
|
||||||
|
</div>
|
||||||
|
<div class="combo-menu" id="alamatOptions" role="listbox"></div>
|
||||||
|
</div>
|
||||||
|
<small>Wajib memilih kecamatan dalam lingkup Kabupaten Jember.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Nomor Telepon -->
|
<!-- Nomor Telepon -->
|
||||||
|
|
@ -482,45 +558,103 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const kecamatanJember = [
|
||||||
|
'Ajung', 'Ambulu', 'Arjasa', 'Balung', 'Bangsalsari', 'Gumukmas', 'Jelbuk',
|
||||||
|
'Jenggawah', 'Jombang', 'Kalisat', 'Kaliwates', 'Kencong', 'Ledokombo',
|
||||||
|
'Mayang', 'Mumbulsari', 'Pakusari', 'Panti', 'Patrang', 'Puger',
|
||||||
|
'Rambipuji', 'Semboro', 'Silo', 'Sukorambi', 'Sukowono', 'Sumberbaru',
|
||||||
|
'Sumberjambe', 'Sumbersari', 'Tanggul', 'Tempurejo', 'Umbulsari', 'Wuluhan'
|
||||||
|
];
|
||||||
|
|
||||||
// Validasi sederhana
|
const biodataForm = document.getElementById('biodataForm');
|
||||||
const namaPemilik = document.getElementById('nama_pemilik').value.trim();
|
const alamatInput = document.getElementById('alamat');
|
||||||
const namaKucing = document.getElementById('nama_kucing').value.trim();
|
const alamatCombo = document.getElementById('alamatCombo');
|
||||||
const umurKucing = document.getElementById('umur_kucing').value;
|
const alamatOptions = document.getElementById('alamatOptions');
|
||||||
const jenisKelamin = document.getElementById('jenis_kelamin').value;
|
const alamatToggle = document.getElementById('alamatToggle');
|
||||||
const beratBadan = document.getElementById('berat_badan').value;
|
|
||||||
|
|
||||||
if (!namaPemilik || !namaKucing || !umurKucing || !jenisKelamin || !beratBadan) {
|
function normalizeText(value) {
|
||||||
alert('Mohon lengkapi semua field yang wajib diisi!');
|
return String(value || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAlamatOptions() {
|
||||||
|
alamatCombo.classList.add('open');
|
||||||
|
alamatInput.setAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAlamatOptions() {
|
||||||
|
alamatCombo.classList.remove('open');
|
||||||
|
alamatInput.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAlamatOptions(query = '') {
|
||||||
|
const q = normalizeText(query);
|
||||||
|
const filtered = kecamatanJember.filter((item) => normalizeText(item).includes(q));
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
alamatOptions.innerHTML = '<div class="combo-empty">Kecamatan tidak ditemukan</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alamatOptions.innerHTML = filtered.map((item) => (
|
||||||
|
`<button type="button" class="combo-option" role="option" data-value="${item}">${item}</button>`
|
||||||
|
)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAlamatOptions();
|
||||||
|
|
||||||
|
alamatInput.addEventListener('input', () => {
|
||||||
|
const selected = kecamatanJember.some((item) => normalizeText(item) === normalizeText(alamatInput.value));
|
||||||
|
alamatInput.setCustomValidity(selected || alamatInput.value.trim() === '' ? '' : 'Pilih kecamatan yang tersedia di Kabupaten Jember.');
|
||||||
|
renderAlamatOptions(alamatInput.value);
|
||||||
|
openAlamatOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
alamatInput.addEventListener('focus', () => {
|
||||||
|
renderAlamatOptions(alamatInput.value);
|
||||||
|
openAlamatOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
alamatToggle.addEventListener('click', () => {
|
||||||
|
renderAlamatOptions(alamatInput.value);
|
||||||
|
alamatCombo.classList.contains('open') ? closeAlamatOptions() : openAlamatOptions();
|
||||||
|
alamatInput.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
alamatOptions.addEventListener('click', (event) => {
|
||||||
|
const option = event.target.closest('.combo-option');
|
||||||
|
if (!option) return;
|
||||||
|
alamatInput.value = option.dataset.value;
|
||||||
|
alamatInput.setCustomValidity('');
|
||||||
|
closeAlamatOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
if (!alamatCombo.contains(event.target)) {
|
||||||
|
closeAlamatOptions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
biodataForm.addEventListener('submit', function(e) {
|
||||||
|
const selected = kecamatanJember.some((item) => normalizeText(item) === normalizeText(alamatInput.value));
|
||||||
|
if (!selected) {
|
||||||
|
e.preventDefault();
|
||||||
|
alamatInput.setCustomValidity('Pilih kecamatan yang tersedia di Kabupaten Jember.');
|
||||||
|
alamatInput.reportValidity();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simpan data ke sessionStorage untuk sementara
|
|
||||||
const formData = {
|
const formData = {
|
||||||
nama_pemilik: namaPemilik,
|
nama_pemilik: document.getElementById('nama_pemilik').value.trim(),
|
||||||
nama_kucing: namaKucing,
|
nama_kucing: document.getElementById('nama_kucing').value.trim(),
|
||||||
umur_kucing: umurKucing,
|
umur_kucing: document.getElementById('umur_kucing').value,
|
||||||
jenis_kelamin: jenisKelamin,
|
jenis_kelamin: document.getElementById('jenis_kelamin').value,
|
||||||
ras_kucing: document.getElementById('ras_kucing').value.trim(),
|
ras_kucing: document.getElementById('ras_kucing').value.trim(),
|
||||||
berat_badan: beratBadan,
|
berat_badan: document.getElementById('berat_badan').value,
|
||||||
alamat: document.getElementById('alamat').value.trim(),
|
alamat: alamatInput.value.trim(),
|
||||||
no_telepon: document.getElementById('no_telepon').value.trim()
|
no_telepon: document.getElementById('no_telepon').value.trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
sessionStorage.setItem('biodata_kucing', JSON.stringify(formData));
|
sessionStorage.setItem('biodata_kucing', JSON.stringify(formData));
|
||||||
|
|
||||||
// Simpan data ke sessionStorage
|
|
||||||
sessionStorage.setItem('biodata_kucing', JSON.stringify(formData));
|
|
||||||
|
|
||||||
// Show success message
|
|
||||||
if (window.showToast) {
|
|
||||||
showToast('Biodata berhasil disimpan!', 'success', 'Berhasil');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect ke halaman pilih gejala
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = '{{ route("gejala") }}';
|
|
||||||
}, 500);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Pilih Gejala - PawMedic</title>
|
<title>Pilih Gejala - PawMedic</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||||
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}">
|
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}">
|
||||||
|
|
||||||
|
|
@ -205,7 +206,7 @@
|
||||||
animation:fadeUp 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
animation:fadeUp 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
border:1px solid rgba(111,207,151,0.2);
|
border:1px solid rgba(111,207,151,0.2);
|
||||||
position:relative;
|
position:relative;
|
||||||
overflow:hidden;
|
overflow:visible;
|
||||||
transform-style:preserve-3d;
|
transform-style:preserve-3d;
|
||||||
transition:transform 0.3s ease, box-shadow 0.3s ease;
|
transition:transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
@ -237,6 +238,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-card::after{
|
.form-card::after{
|
||||||
|
display:none;
|
||||||
content:'';
|
content:'';
|
||||||
position:absolute;
|
position:absolute;
|
||||||
top:-50%;
|
top:-50%;
|
||||||
|
|
@ -378,6 +380,9 @@
|
||||||
position:relative;
|
position:relative;
|
||||||
animation:fadeInUp 0.5s ease backwards;
|
animation:fadeInUp 0.5s ease backwards;
|
||||||
}
|
}
|
||||||
|
.gejala-item:hover{
|
||||||
|
z-index:5;
|
||||||
|
}
|
||||||
|
|
||||||
.gejala-item:nth-child(1){animation-delay:0.05s;}
|
.gejala-item:nth-child(1){animation-delay:0.05s;}
|
||||||
.gejala-item:nth-child(2){animation-delay:0.1s;}
|
.gejala-item:nth-child(2){animation-delay:0.1s;}
|
||||||
|
|
@ -409,6 +414,7 @@
|
||||||
.gejala-label{
|
.gejala-label{
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
|
flex-wrap:wrap;
|
||||||
gap:14px;
|
gap:14px;
|
||||||
padding:20px 24px;
|
padding:20px 24px;
|
||||||
background:linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
|
background:linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
|
||||||
|
|
@ -421,40 +427,20 @@
|
||||||
color:var(--text-dark);
|
color:var(--text-dark);
|
||||||
user-select:none;
|
user-select:none;
|
||||||
position:relative;
|
position:relative;
|
||||||
overflow:hidden;
|
overflow:visible;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 4px 12px rgba(0,0,0,0.06),
|
0 4px 12px rgba(0,0,0,0.06),
|
||||||
0 0 0 0 rgba(111,207,151,0);
|
0 0 0 0 rgba(111,207,151,0);
|
||||||
transform:perspective(1000px) rotateX(0deg);
|
transform:perspective(1000px) rotateX(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gejala-label::after{
|
|
||||||
content:'';
|
|
||||||
position:absolute;
|
|
||||||
top:50%;
|
|
||||||
left:50%;
|
|
||||||
width:0;
|
|
||||||
height:0;
|
|
||||||
border-radius:50%;
|
|
||||||
background:rgba(111,207,151,0.1);
|
|
||||||
transform:translate(-50%, -50%);
|
|
||||||
transition:width 0.6s ease, height 0.6s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gejala-label:hover::after{
|
|
||||||
width:300px;
|
|
||||||
height:300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gejala-label:hover{
|
.gejala-label:hover{
|
||||||
background:linear-gradient(135deg, var(--primary-light) 0%, #ffffff 100%);
|
background:linear-gradient(135deg, var(--primary-light) 0%, #ffffff 100%);
|
||||||
border-color:var(--primary);
|
border-color:var(--primary);
|
||||||
transform:translateY(-6px) scale(1.03) perspective(1000px) rotateX(-2deg);
|
transform:translateY(-3px);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 12px 32px rgba(111,207,151,0.25),
|
0 10px 24px rgba(111,207,151,0.18),
|
||||||
0 0 0 4px rgba(111,207,151,0.1),
|
0 0 0 4px rgba(111,207,151,0.08);
|
||||||
inset 0 1px 0 rgba(255,255,255,0.9);
|
|
||||||
border-width:3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gejala-checkbox:checked + .gejala-label{
|
.gejala-checkbox:checked + .gejala-label{
|
||||||
|
|
@ -466,23 +452,7 @@
|
||||||
0 12px 36px rgba(111,207,151,0.3),
|
0 12px 36px rgba(111,207,151,0.3),
|
||||||
0 0 0 5px rgba(111,207,151,0.15),
|
0 0 0 5px rgba(111,207,151,0.15),
|
||||||
inset 0 2px 4px rgba(111,207,151,0.1);
|
inset 0 2px 4px rgba(111,207,151,0.1);
|
||||||
transform:translateY(-4px) scale(1.02) perspective(1000px) rotateX(-1deg);
|
transform:translateY(-2px);
|
||||||
animation:selectedPulse 2s ease infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes selectedPulse{
|
|
||||||
0%, 100%{
|
|
||||||
box-shadow:
|
|
||||||
0 12px 36px rgba(111,207,151,0.3),
|
|
||||||
0 0 0 5px rgba(111,207,151,0.15),
|
|
||||||
inset 0 2px 4px rgba(111,207,151,0.1);
|
|
||||||
}
|
|
||||||
50%{
|
|
||||||
box-shadow:
|
|
||||||
0 12px 36px rgba(111,207,151,0.35),
|
|
||||||
0 0 0 6px rgba(111,207,151,0.2),
|
|
||||||
inset 0 2px 4px rgba(111,207,151,0.15);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gejala-checkbox:checked + .gejala-label::before{
|
.gejala-checkbox:checked + .gejala-label::before{
|
||||||
|
|
@ -527,6 +497,156 @@
|
||||||
z-index:1;
|
z-index:1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gejala-name{
|
||||||
|
position:relative;
|
||||||
|
z-index:2;
|
||||||
|
flex:1;
|
||||||
|
min-width:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gejala-help{
|
||||||
|
position:relative;
|
||||||
|
z-index:3;
|
||||||
|
width:28px;
|
||||||
|
height:28px;
|
||||||
|
border-radius:999px;
|
||||||
|
display:inline-flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
color:var(--primary-dark);
|
||||||
|
background:#ecfdf5;
|
||||||
|
border:1px solid #bbf7d0;
|
||||||
|
flex-shrink:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gejala-help i{
|
||||||
|
transition:transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gejala-item.show-info .gejala-help i{
|
||||||
|
transform:rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gejala-description{
|
||||||
|
display:none;
|
||||||
|
width:100%;
|
||||||
|
margin-top:2px;
|
||||||
|
margin-left:42px;
|
||||||
|
padding:12px 14px;
|
||||||
|
border-radius:12px;
|
||||||
|
background:#f0fdf4;
|
||||||
|
border:1px solid #bbf7d0;
|
||||||
|
color:#14532d;
|
||||||
|
font-size:13px;
|
||||||
|
line-height:1.55;
|
||||||
|
font-weight:500;
|
||||||
|
}
|
||||||
|
.gejala-item.show-info .gejala-description{
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnosis-alert{
|
||||||
|
display:none;
|
||||||
|
position:fixed;
|
||||||
|
left:50%;
|
||||||
|
top:50%;
|
||||||
|
z-index:9999;
|
||||||
|
width:min(420px, calc(100vw - 28px));
|
||||||
|
margin:0;
|
||||||
|
padding:16px 44px 16px 16px;
|
||||||
|
border:1px solid #fde68a;
|
||||||
|
border-radius:14px;
|
||||||
|
background:#ffffff;
|
||||||
|
color:#1f2937;
|
||||||
|
box-shadow:0 22px 55px rgba(15,23,42,0.26);
|
||||||
|
transform:translate(-50%, -46%) scale(.96);
|
||||||
|
opacity:0;
|
||||||
|
transition:opacity .2s ease, transform .2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnosis-alert.show{
|
||||||
|
display:flex;
|
||||||
|
align-items:flex-start;
|
||||||
|
gap:12px;
|
||||||
|
opacity:1;
|
||||||
|
transform:translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnosis-alert-icon{
|
||||||
|
flex:0 0 auto;
|
||||||
|
width:36px;
|
||||||
|
height:36px;
|
||||||
|
border-radius:50%;
|
||||||
|
display:inline-flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
color:#b45309;
|
||||||
|
background:#fef3c7;
|
||||||
|
font-size:18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnosis-alert-content{
|
||||||
|
min-width:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnosis-alert-heading{
|
||||||
|
display:block;
|
||||||
|
margin:0 0 2px;
|
||||||
|
color:#114d3a;
|
||||||
|
font-size:14px;
|
||||||
|
font-weight:800;
|
||||||
|
line-height:1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnosis-alert-message{
|
||||||
|
margin:0;
|
||||||
|
color:#475569;
|
||||||
|
font-size:14px;
|
||||||
|
font-weight:600;
|
||||||
|
line-height:1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnosis-alert-close{
|
||||||
|
position:absolute;
|
||||||
|
top:12px;
|
||||||
|
right:10px;
|
||||||
|
width:28px;
|
||||||
|
height:28px;
|
||||||
|
border:0;
|
||||||
|
border-radius:50%;
|
||||||
|
display:inline-flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
color:#64748b;
|
||||||
|
background:transparent;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:background .2s ease, color .2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnosis-alert-close:hover{
|
||||||
|
color:#0f172a;
|
||||||
|
background:#f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:480px){
|
||||||
|
.diagnosis-alert{
|
||||||
|
width:calc(100vw - 24px);
|
||||||
|
padding:14px 40px 14px 14px;
|
||||||
|
border-radius:12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnosis-alert-icon{
|
||||||
|
width:32px;
|
||||||
|
height:32px;
|
||||||
|
font-size:16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnosis-alert-heading,
|
||||||
|
.diagnosis-alert-message{
|
||||||
|
font-size:13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.selected-count{
|
.selected-count{
|
||||||
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
||||||
color:white;
|
color:white;
|
||||||
|
|
@ -826,6 +946,65 @@ class="search-input"
|
||||||
<button type="button" id="clearSearch" class="clear-search" style="display:none;">✕</button>
|
<button type="button" id="clearSearch" class="clear-search" style="display:none;">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@php
|
||||||
|
$explainGejala = function ($name) {
|
||||||
|
$text = trim((string) $name);
|
||||||
|
$lower = \Illuminate\Support\Str::lower($text);
|
||||||
|
$rules = [
|
||||||
|
'demam tinggi' => 'Telinga, telapak kaki, atau tubuh kucing terasa lebih panas dari biasanya dan kucing tampak kurang aktif.',
|
||||||
|
'sulit kencing' => 'Cek litterbox/kotak pasir kucing, lihat apakah kencingnya sedikit atau normal ke banyak.',
|
||||||
|
'kencing' => 'Amati frekuensi, jumlah, warna, dan apakah kucing tampak mengejan saat menggunakan kotak pasir.',
|
||||||
|
'diare berdarah' => 'Periksa apakah feses encer bercampur darah. Jika terlihat darah, kondisi ini perlu lebih diwaspadai.',
|
||||||
|
'diare' => 'Periksa apakah feses lebih encer dari biasanya, lebih sering keluar, atau baunya lebih menyengat.',
|
||||||
|
'muntah' => 'Catat seberapa sering kucing muntah, isi muntahan, dan apakah terjadi setelah makan atau minum.',
|
||||||
|
'nafsu makan' => 'Bandingkan porsi makan hari ini dengan kebiasaan normalnya dan perhatikan apakah kucing menolak makanan favorit.',
|
||||||
|
'kelemahan' => 'Perhatikan apakah kucing tampak lemas, lebih banyak diam, sulit berdiri, atau tidak mau bermain.',
|
||||||
|
'lemas' => 'Perhatikan apakah kucing lebih banyak tidur, kurang responsif, atau enggan bermain dan bergerak.',
|
||||||
|
'bersin' => 'Amati apakah bersin disertai lendir hidung, mata berair, atau napas berbunyi.',
|
||||||
|
'flu' => 'Perhatikan apakah hidung berair, bersin, atau kucing terlihat sulit mencium makanan.',
|
||||||
|
'pilek' => 'Cek apakah ada cairan dari hidung, hidung tersumbat, atau suara napas menjadi berbeda.',
|
||||||
|
'sesak napas' => 'Lihat apakah napas kucing cepat, berat, mulut terbuka, atau dada terlihat naik turun kuat.',
|
||||||
|
'batuk' => 'Dengarkan apakah batuk kering atau berdahak, serta apakah muncul setelah aktivitas atau saat istirahat.',
|
||||||
|
'radang telinga' => 'Cek apakah telinga kemerahan, kotor, berbau, atau kucing sering menggaruk telinga.',
|
||||||
|
'otitis' => 'Perhatikan apakah kucing sering menggelengkan kepala, telinga berbau, atau ada kotoran berlebih.',
|
||||||
|
'gatal' => 'Cek area kulit yang sering digaruk, dijilat, atau digigit, terutama telinga, leher, punggung, dan ekor.',
|
||||||
|
'kutu' => 'Sisir atau buka bulu kucing dan lihat apakah ada kutu kecil bergerak atau bintik hitam seperti kotoran.',
|
||||||
|
'pinjal' => 'Periksa pangkal ekor, leher, dan perut untuk melihat kutu kecil atau bekas gigitan.',
|
||||||
|
'kebotakan' => 'Lihat apakah ada area bulu yang menipis atau botak, terutama jika sering digaruk atau dijilat.',
|
||||||
|
'rontok' => 'Perhatikan apakah bulu rontok lebih banyak dari biasanya atau menyisakan area kulit terlihat.',
|
||||||
|
'bulu' => 'Lihat apakah bulu rontok berlebihan, kusam, menggumpal, atau ada area botak.',
|
||||||
|
'gangguan mata' => 'Periksa apakah mata merah, berair, belekan, bengkak, atau kucing sering menyipitkan mata.',
|
||||||
|
'mata' => 'Perhatikan apakah mata terlihat keruh, merah, berair, atau ada kotoran yang tidak biasa.',
|
||||||
|
'telinga' => 'Cek apakah telinga kotor, berbau, sering digaruk, atau kepala sering digelengkan.',
|
||||||
|
'demam' => 'Rasakan telinga/telapak kaki yang lebih hangat dari biasa dan perhatikan apakah kucing tampak lesu.',
|
||||||
|
'luka pada mulut' => 'Lihat area gusi, lidah, atau bibir. Perhatikan apakah ada sariawan, luka, bau mulut, atau sulit makan.',
|
||||||
|
'luka garukan' => 'Cek bekas garukan, kemerahan, atau kerak di kulit akibat kucing sering menggaruk.',
|
||||||
|
'luka' => 'Periksa lokasi luka, kemerahan, bengkak, nanah, atau apakah kucing kesakitan saat disentuh.',
|
||||||
|
'pincang' => 'Amati cara berjalan kucing, apakah salah satu kaki diangkat, diseret, atau tidak kuat menapak.',
|
||||||
|
'selaput lendir kuning' => 'Cek gusi, bagian putih mata, atau telinga bagian dalam. Warna kuning bisa menandakan masalah serius.',
|
||||||
|
'jaundice' => 'Perhatikan warna kuning pada gusi, mata, atau kulit tipis seperti telinga.',
|
||||||
|
'perut membesar' => 'Lihat apakah perut tampak membesar tidak biasa, terasa tegang, atau kucing tidak nyaman saat disentuh.',
|
||||||
|
'buncit' => 'Bandingkan bentuk perut dengan biasanya, terutama jika disertai lemas atau nafsu makan turun.',
|
||||||
|
'anemia' => 'Cek warna gusi. Gusi yang tampak pucat bisa menjadi tanda darah atau stamina kucing sedang bermasalah.',
|
||||||
|
'infeksi kulit' => 'Periksa kulit yang merah, basah, berkerak, bernanah, atau berbau tidak biasa.',
|
||||||
|
'overgrooming' => 'Perhatikan apakah kucing menjilat satu area terus-menerus sampai bulu menipis atau kulit iritasi.',
|
||||||
|
'perut bawah keras' => 'Raba pelan area perut bawah. Jika terasa keras dan kucing kesakitan, catat sebagai gejala penting.',
|
||||||
|
'sakit perut' => 'Perhatikan apakah kucing menghindar saat perut disentuh, meringkuk, atau tampak tidak nyaman.',
|
||||||
|
'nyeri abdomen' => 'Amati tanda nyeri di perut seperti mengeong saat disentuh, gelisah, atau posisi tubuh membungkuk.',
|
||||||
|
'penurunan berat badan cepat' => 'Bandingkan berat atau bentuk tubuh dalam beberapa hari/minggu terakhir, terutama jika makan tetap normal.',
|
||||||
|
'berat' => 'Bandingkan berat badan dengan kondisi sebelumnya dan amati apakah tubuh tampak lebih kurus atau membesar.',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($rules as $keyword => $description) {
|
||||||
|
if (\Illuminate\Support\Str::contains($lower, $keyword)) {
|
||||||
|
return $description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Perhatikan gejala ' . $text . ' dengan melihat kapan mulai muncul, seberapa sering terjadi, dan apakah membuat kucing berubah perilaku.';
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
|
||||||
<div class="gejala-grid" id="gejalaGrid">
|
<div class="gejala-grid" id="gejalaGrid">
|
||||||
@foreach($gejala as $item)
|
@foreach($gejala as $item)
|
||||||
<div class="gejala-item">
|
<div class="gejala-item">
|
||||||
|
|
@ -837,7 +1016,11 @@ class="gejala-checkbox"
|
||||||
id="gejala_{{ $loop->index }}"
|
id="gejala_{{ $loop->index }}"
|
||||||
>
|
>
|
||||||
<label for="gejala_{{ $loop->index }}" class="gejala-label">
|
<label for="gejala_{{ $loop->index }}" class="gejala-label">
|
||||||
{{ $item }}
|
<span class="gejala-name">{{ $item }}</span>
|
||||||
|
<span class="gejala-help" role="button" tabindex="0" aria-label="Tampilkan penjelasan gejala {{ $item }}" aria-expanded="false">
|
||||||
|
<i class="bi bi-chevron-down"></i>
|
||||||
|
</span>
|
||||||
|
<span class="gejala-description">{{ $explainGejala($item) }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
@ -858,6 +1041,19 @@ class="gejala-checkbox"
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning diagnosis-alert" id="diagnosisAlert" role="alert" aria-live="assertive">
|
||||||
|
<span class="diagnosis-alert-icon" aria-hidden="true">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||||
|
</span>
|
||||||
|
<div class="diagnosis-alert-content">
|
||||||
|
<strong class="diagnosis-alert-heading">Perhatian</strong>
|
||||||
|
<p class="diagnosis-alert-message" id="diagnosisAlertText">Pilih minimal 4 gejala terlebih dahulu.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="diagnosis-alert-close" aria-label="Tutup alert" id="closeDiagnosisAlert">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
@include('components.toast')
|
@include('components.toast')
|
||||||
@include('components.scroll-top')
|
@include('components.scroll-top')
|
||||||
|
|
||||||
|
|
@ -866,6 +1062,24 @@ class="gejala-checkbox"
|
||||||
const submitBtn = document.getElementById('submitBtn');
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
const selectedCount = document.getElementById('selectedCount');
|
const selectedCount = document.getElementById('selectedCount');
|
||||||
const form = document.getElementById('gejalaForm');
|
const form = document.getElementById('gejalaForm');
|
||||||
|
const diagnosisAlert = document.getElementById('diagnosisAlert');
|
||||||
|
const diagnosisAlertText = document.getElementById('diagnosisAlertText');
|
||||||
|
const closeDiagnosisAlert = document.getElementById('closeDiagnosisAlert');
|
||||||
|
let diagnosisAlertTimer = null;
|
||||||
|
|
||||||
|
function showDiagnosisAlert(message) {
|
||||||
|
diagnosisAlertText.textContent = message;
|
||||||
|
diagnosisAlert.classList.add('show');
|
||||||
|
clearTimeout(diagnosisAlertTimer);
|
||||||
|
diagnosisAlertTimer = setTimeout(hideDiagnosisAlert, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDiagnosisAlert() {
|
||||||
|
clearTimeout(diagnosisAlertTimer);
|
||||||
|
diagnosisAlert.classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDiagnosisAlert.addEventListener('click', hideDiagnosisAlert);
|
||||||
|
|
||||||
function updateSelectedCount() {
|
function updateSelectedCount() {
|
||||||
const checked = document.querySelectorAll('.gejala-checkbox:checked').length;
|
const checked = document.querySelectorAll('.gejala-checkbox:checked').length;
|
||||||
|
|
@ -875,13 +1089,13 @@ function updateSelectedCount() {
|
||||||
selectedCount.classList.add('animate');
|
selectedCount.classList.add('animate');
|
||||||
setTimeout(() => selectedCount.classList.remove('animate'), 500);
|
setTimeout(() => selectedCount.classList.remove('animate'), 500);
|
||||||
|
|
||||||
// Enable/disable submit button
|
// Tombol tetap bisa diklik agar validasi menampilkan alert yang jelas.
|
||||||
if (checked >= 4 && checked <= 7) {
|
if (checked >= 4 && checked <= 7) {
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitBtn.style.opacity = '1';
|
submitBtn.style.opacity = '1';
|
||||||
|
submitBtn.setAttribute('aria-disabled', 'false');
|
||||||
} else {
|
} else {
|
||||||
submitBtn.disabled = true;
|
|
||||||
submitBtn.style.opacity = '0.6';
|
submitBtn.style.opacity = '0.6';
|
||||||
|
submitBtn.setAttribute('aria-disabled', 'true');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -890,6 +1104,23 @@ function updateSelectedCount() {
|
||||||
checkbox.addEventListener('change', updateSelectedCount);
|
checkbox.addEventListener('change', updateSelectedCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.gejala-help').forEach((help) => {
|
||||||
|
const item = help.closest('.gejala-item');
|
||||||
|
const toggleInfo = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const isOpen = item.classList.toggle('show-info');
|
||||||
|
help.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||||
|
};
|
||||||
|
|
||||||
|
help.addEventListener('click', toggleInfo);
|
||||||
|
help.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
toggleInfo(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Form submission
|
// Form submission
|
||||||
|
|
||||||
form.addEventListener('submit', function(e) {
|
form.addEventListener('submit', function(e) {
|
||||||
|
|
@ -898,15 +1129,17 @@ function updateSelectedCount() {
|
||||||
const checked = document.querySelectorAll('.gejala-checkbox:checked');
|
const checked = document.querySelectorAll('.gejala-checkbox:checked');
|
||||||
|
|
||||||
if (checked.length < 4) {
|
if (checked.length < 4) {
|
||||||
alert("Minimal pilih 4 gejala!");
|
showDiagnosisAlert("Minimal pilih 4 gejala sebelum melanjutkan diagnosis.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checked.length > 7) {
|
if (checked.length > 7) {
|
||||||
alert("Maksimal hanya 7 gejala!");
|
showDiagnosisAlert("Maksimal hanya 7 gejala yang dapat dipilih.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideDiagnosisAlert();
|
||||||
|
|
||||||
// ambil gejala
|
// ambil gejala
|
||||||
let gejala = [];
|
let gejala = [];
|
||||||
checked.forEach(c => gejala.push(c.value));
|
checked.forEach(c => gejala.push(c.value));
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@
|
||||||
--ff-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
|
--ff-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
|
||||||
--space:28px;
|
--space:28px;
|
||||||
--muted: #6b7280;
|
--muted: #6b7280;
|
||||||
|
--primary: #6fcf97;
|
||||||
|
--primary-dark: #4bb66f;
|
||||||
|
--primary-light: #e8f7ef;
|
||||||
}
|
}
|
||||||
body{
|
body{
|
||||||
margin:0;
|
margin:0;
|
||||||
|
|
@ -292,6 +295,299 @@
|
||||||
transform:translateY(-6px);
|
transform:translateY(-6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== HEALTH TRENDS ===== */
|
||||||
|
.health-trends{
|
||||||
|
background:linear-gradient(135deg,#ffffff 0%,#f8fffb 58%,#f6fbff 100%);
|
||||||
|
border:1px solid rgba(111,207,151,0.18);
|
||||||
|
border-radius:22px;
|
||||||
|
padding:26px;
|
||||||
|
box-shadow:0 16px 46px rgba(17,77,58,0.08);
|
||||||
|
}
|
||||||
|
.trend-heading{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:space-between;
|
||||||
|
gap:18px;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
margin-bottom:20px;
|
||||||
|
}
|
||||||
|
.trend-title-wrap{
|
||||||
|
display:flex;
|
||||||
|
align-items:flex-start;
|
||||||
|
gap:18px;
|
||||||
|
}
|
||||||
|
.trend-title-icon{
|
||||||
|
width:48px;
|
||||||
|
height:48px;
|
||||||
|
border-radius:14px;
|
||||||
|
background:linear-gradient(135deg,#dcfce7,#f0fdf4);
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
color:#0f5132;
|
||||||
|
font-size:22px;
|
||||||
|
box-shadow:0 10px 26px rgba(34,197,94,0.13);
|
||||||
|
}
|
||||||
|
.trend-heading h2{
|
||||||
|
text-align:left;
|
||||||
|
margin-bottom:6px;
|
||||||
|
font-size:clamp(1.7rem, 3vw, 2.35rem);
|
||||||
|
color:#064e3b;
|
||||||
|
}
|
||||||
|
.trend-heading p{
|
||||||
|
text-align:left;
|
||||||
|
margin:0;
|
||||||
|
max-width:680px;
|
||||||
|
color:#64748b;
|
||||||
|
}
|
||||||
|
.trend-filter{
|
||||||
|
display:inline-flex;
|
||||||
|
gap:6px;
|
||||||
|
background:#f8fafc;
|
||||||
|
border:1px solid #e2e8f0;
|
||||||
|
border-radius:14px;
|
||||||
|
padding:6px;
|
||||||
|
box-shadow:inset 0 1px 3px rgba(15,23,42,0.05);
|
||||||
|
}
|
||||||
|
.trend-filter a{
|
||||||
|
text-decoration:none;
|
||||||
|
color:#475569;
|
||||||
|
font-weight:700;
|
||||||
|
font-size:14px;
|
||||||
|
padding:9px 18px;
|
||||||
|
border-radius:10px;
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
|
.trend-filter a.active{
|
||||||
|
background:linear-gradient(135deg,#22c55e,#16a34a);
|
||||||
|
color:#fff;
|
||||||
|
box-shadow:0 8px 18px rgba(34,197,94,0.24);
|
||||||
|
}
|
||||||
|
.trend-grid{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:minmax(0,1fr) minmax(0,1fr);
|
||||||
|
gap:18px;
|
||||||
|
}
|
||||||
|
.trend-panel{
|
||||||
|
background:rgba(255,255,255,0.75);
|
||||||
|
border:1px solid #cfead9;
|
||||||
|
border-radius:18px;
|
||||||
|
padding:18px;
|
||||||
|
min-height:310px;
|
||||||
|
box-shadow:inset 0 1px 0 rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
.trend-panel.area-panel{
|
||||||
|
border-color:#c7daf9;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 20%, rgba(59,130,246,0.09), transparent 34%),
|
||||||
|
linear-gradient(145deg, rgba(255,255,255,0.9), rgba(248,251,255,0.78));
|
||||||
|
}
|
||||||
|
.trend-panel h3{
|
||||||
|
font-size:18px;
|
||||||
|
margin-bottom:10px;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:12px;
|
||||||
|
}
|
||||||
|
.trend-panel h3 i{
|
||||||
|
width:38px;
|
||||||
|
height:38px;
|
||||||
|
border-radius:12px;
|
||||||
|
display:inline-flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
background:#dcfce7;
|
||||||
|
color:#16a34a;
|
||||||
|
font-size:22px;
|
||||||
|
}
|
||||||
|
.trend-panel.area-panel h3 i{
|
||||||
|
background:#dbeafe;
|
||||||
|
color:#2563eb;
|
||||||
|
}
|
||||||
|
.trend-panel p{
|
||||||
|
color:#64748b;
|
||||||
|
margin:0 0 14px;
|
||||||
|
font-size:14px;
|
||||||
|
}
|
||||||
|
.trend-chart-wrap{
|
||||||
|
height:205px;
|
||||||
|
}
|
||||||
|
.disease-legend{
|
||||||
|
display:none;
|
||||||
|
grid-template-columns:1fr 1fr;
|
||||||
|
gap:8px 24px;
|
||||||
|
border:1px solid #d7eadf;
|
||||||
|
border-radius:16px;
|
||||||
|
padding:14px 18px;
|
||||||
|
margin-top:18px;
|
||||||
|
background:rgba(255,255,255,0.72);
|
||||||
|
}
|
||||||
|
.legend-row{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:auto 1fr auto;
|
||||||
|
align-items:center;
|
||||||
|
gap:10px;
|
||||||
|
color:#475569;
|
||||||
|
font-size:13px;
|
||||||
|
}
|
||||||
|
.legend-dot{
|
||||||
|
width:10px;
|
||||||
|
height:10px;
|
||||||
|
border-radius:999px;
|
||||||
|
background:#22c55e;
|
||||||
|
}
|
||||||
|
.legend-total{
|
||||||
|
color:#0f172a;
|
||||||
|
font-weight:800;
|
||||||
|
}
|
||||||
|
.area-donut-layout{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:minmax(180px, 0.95fr) minmax(190px, 1fr);
|
||||||
|
align-items:center;
|
||||||
|
gap:22px;
|
||||||
|
margin-top:10px;
|
||||||
|
}
|
||||||
|
.area-donut-wrap{
|
||||||
|
height:220px;
|
||||||
|
position:relative;
|
||||||
|
padding:8px;
|
||||||
|
border-radius:999px;
|
||||||
|
background:radial-gradient(circle at 50% 50%, #ffffff 0%, #ffffff 42%, rgba(219,234,254,0.55) 43%, rgba(255,255,255,0) 70%);
|
||||||
|
}
|
||||||
|
.area-list{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:12px;
|
||||||
|
}
|
||||||
|
.area-row{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:auto 1fr auto;
|
||||||
|
align-items:center;
|
||||||
|
gap:12px;
|
||||||
|
border:1px solid #dbe5f2;
|
||||||
|
border-radius:14px;
|
||||||
|
background:rgba(255,255,255,0.78);
|
||||||
|
padding:13px 16px;
|
||||||
|
color:#334155;
|
||||||
|
}
|
||||||
|
.area-dot{
|
||||||
|
width:18px;
|
||||||
|
height:18px;
|
||||||
|
border-radius:999px;
|
||||||
|
background:#2563eb;
|
||||||
|
}
|
||||||
|
.area-count{
|
||||||
|
color:#2563eb;
|
||||||
|
font-weight:800;
|
||||||
|
}
|
||||||
|
.trend-bottom-grid{
|
||||||
|
margin-top:18px;
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:0.8fr 1.2fr 1.2fr;
|
||||||
|
gap:16px;
|
||||||
|
}
|
||||||
|
.trend-empty{
|
||||||
|
min-height:210px;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
text-align:center;
|
||||||
|
color:#64748b;
|
||||||
|
background:#f8fafc;
|
||||||
|
border-radius:12px;
|
||||||
|
padding:20px;
|
||||||
|
}
|
||||||
|
.trend-highlight{
|
||||||
|
background:linear-gradient(135deg,#dcfce7 0%,#f7fffb 100%);
|
||||||
|
border:1px solid #74d99b;
|
||||||
|
border-radius:18px;
|
||||||
|
padding:24px;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:20px;
|
||||||
|
min-height:130px;
|
||||||
|
position:relative;
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
.trend-highlight::after{
|
||||||
|
content:'';
|
||||||
|
position:absolute;
|
||||||
|
right:22px;
|
||||||
|
bottom:14px;
|
||||||
|
width:96px;
|
||||||
|
height:96px;
|
||||||
|
opacity:0.12;
|
||||||
|
background:linear-gradient(135deg,#16a34a,#86efac);
|
||||||
|
clip-path:polygon(46% 100%,46% 54%,10% 54%,10% 34%,68% 34%,68% 0,100% 50%,68% 100%,68% 68%,58% 68%,58% 100%);
|
||||||
|
}
|
||||||
|
.trend-highlight-icon,
|
||||||
|
.care-icon{
|
||||||
|
width:52px;
|
||||||
|
height:52px;
|
||||||
|
border-radius:999px;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
flex-shrink:0;
|
||||||
|
font-size:24px;
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
|
.trend-highlight-icon{
|
||||||
|
background:linear-gradient(135deg,#fb923c,#f97316);
|
||||||
|
box-shadow:0 12px 26px rgba(249,115,22,0.22);
|
||||||
|
}
|
||||||
|
.trend-highlight span{
|
||||||
|
display:block;
|
||||||
|
color:#64748b;
|
||||||
|
font-size:13px;
|
||||||
|
font-weight:700;
|
||||||
|
text-transform:uppercase;
|
||||||
|
letter-spacing:0.04em;
|
||||||
|
margin-bottom:6px;
|
||||||
|
}
|
||||||
|
.trend-highlight strong{
|
||||||
|
display:block;
|
||||||
|
color:#114d3a;
|
||||||
|
font-family:var(--ff-heading);
|
||||||
|
font-size:19px;
|
||||||
|
line-height:1.35;
|
||||||
|
}
|
||||||
|
.care-box{
|
||||||
|
background:rgba(255,255,255,0.78);
|
||||||
|
border:1px solid #facc15;
|
||||||
|
border-radius:18px;
|
||||||
|
padding:20px;
|
||||||
|
display:flex;
|
||||||
|
gap:16px;
|
||||||
|
min-height:130px;
|
||||||
|
}
|
||||||
|
.care-box.prevention{
|
||||||
|
border-color:#c4a4f3;
|
||||||
|
}
|
||||||
|
.care-box.handling .care-icon{
|
||||||
|
background:linear-gradient(135deg,#facc15,#f59e0b);
|
||||||
|
box-shadow:0 12px 26px rgba(245,158,11,0.2);
|
||||||
|
}
|
||||||
|
.care-box.prevention .care-icon{
|
||||||
|
background:linear-gradient(135deg,#a78bfa,#7c3aed);
|
||||||
|
box-shadow:0 12px 26px rgba(124,58,237,0.18);
|
||||||
|
}
|
||||||
|
.care-box h3{
|
||||||
|
font-size:19px;
|
||||||
|
margin-bottom:10px;
|
||||||
|
}
|
||||||
|
.care-box p,
|
||||||
|
.care-box ul{
|
||||||
|
margin:0;
|
||||||
|
color:#334155;
|
||||||
|
}
|
||||||
|
.care-box ul{
|
||||||
|
padding-left:18px;
|
||||||
|
}
|
||||||
|
.care-box li + li{
|
||||||
|
margin-top:8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== FEATURES ===== */
|
/* ===== FEATURES ===== */
|
||||||
.features{
|
.features{
|
||||||
display:grid;
|
display:grid;
|
||||||
|
|
@ -574,6 +870,11 @@
|
||||||
.features{
|
.features{
|
||||||
grid-template-columns:repeat(2,1fr);
|
grid-template-columns:repeat(2,1fr);
|
||||||
}
|
}
|
||||||
|
.trend-grid,
|
||||||
|
.trend-bottom-grid,
|
||||||
|
.area-donut-layout{
|
||||||
|
grid-template-columns:1fr;
|
||||||
|
}
|
||||||
section{
|
section{
|
||||||
margin-top:72px;
|
margin-top:72px;
|
||||||
}
|
}
|
||||||
|
|
@ -625,6 +926,54 @@
|
||||||
.features{
|
.features{
|
||||||
grid-template-columns:1fr;
|
grid-template-columns:1fr;
|
||||||
}
|
}
|
||||||
|
.health-trends{
|
||||||
|
padding:16px 12px;
|
||||||
|
border-radius:12px;
|
||||||
|
}
|
||||||
|
.trend-heading h2{
|
||||||
|
font-size:22px;
|
||||||
|
}
|
||||||
|
.trend-filter{
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
.trend-filter a{
|
||||||
|
flex:1;
|
||||||
|
text-align:center;
|
||||||
|
font-size:13px;
|
||||||
|
padding:8px;
|
||||||
|
}
|
||||||
|
.trend-panel{
|
||||||
|
padding:14px;
|
||||||
|
min-height:280px;
|
||||||
|
}
|
||||||
|
.trend-title-wrap{
|
||||||
|
gap:12px;
|
||||||
|
}
|
||||||
|
.trend-title-icon{
|
||||||
|
width:46px;
|
||||||
|
height:46px;
|
||||||
|
font-size:22px;
|
||||||
|
}
|
||||||
|
.trend-chart-wrap{
|
||||||
|
height:220px;
|
||||||
|
}
|
||||||
|
.disease-legend{
|
||||||
|
grid-template-columns:1fr;
|
||||||
|
}
|
||||||
|
.trend-highlight,
|
||||||
|
.care-box{
|
||||||
|
padding:16px;
|
||||||
|
align-items:flex-start;
|
||||||
|
}
|
||||||
|
.trend-highlight-icon,
|
||||||
|
.care-icon{
|
||||||
|
width:48px;
|
||||||
|
height:48px;
|
||||||
|
font-size:22px;
|
||||||
|
}
|
||||||
|
.trend-highlight strong{
|
||||||
|
font-size:18px;
|
||||||
|
}
|
||||||
.hero-content h2{
|
.hero-content h2{
|
||||||
font-size:20px;
|
font-size:20px;
|
||||||
line-height:1.3;
|
line-height:1.3;
|
||||||
|
|
@ -768,6 +1117,115 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- TREN PENYAKIT -->
|
||||||
|
<section id="tren-penyakit" class="health-trends" data-aos="fade-up">
|
||||||
|
<div class="trend-heading">
|
||||||
|
<div class="trend-title-wrap">
|
||||||
|
<div class="trend-title-icon"><i class="bi bi-shield-check"></i></div>
|
||||||
|
<div>
|
||||||
|
<h2>Penyakit Terbanyak</h2>
|
||||||
|
<p>
|
||||||
|
Ringkasan kasus diagnosis dari data PawMedic pada periode {{ $diseaseNews['period_label'] ?? '30 hari terakhir' }}
|
||||||
|
({{ $diseaseNews['start_label'] ?? '-' }} - {{ $diseaseNews['end_label'] ?? '-' }}).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="trend-filter" aria-label="Pilih periode tren penyakit">
|
||||||
|
<a href="{{ route('landing', ['range' => 'week']) }}#tren-penyakit" class="{{ ($diseaseNews['range'] ?? 'month') === 'week' ? 'active' : '' }}">1 Minggu</a>
|
||||||
|
<a href="{{ route('landing', ['range' => 'month']) }}#tren-penyakit" class="{{ ($diseaseNews['range'] ?? 'month') === 'month' ? 'active' : '' }}">1 Bulan</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(!empty($diseaseNews['top_disease']))
|
||||||
|
<div class="trend-grid">
|
||||||
|
<div class="trend-panel">
|
||||||
|
<h3><i class="bi bi-bar-chart-fill"></i> Kasus per Penyakit</h3>
|
||||||
|
<p>Penyakit yang paling sering muncul berdasarkan hasil diagnosis pengguna.</p>
|
||||||
|
<div class="trend-chart-wrap">
|
||||||
|
<canvas id="landingDiseaseChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="disease-legend">
|
||||||
|
@foreach(($diseaseNews['disease_labels'] ?? collect()) as $index => $label)
|
||||||
|
<div class="legend-row">
|
||||||
|
<span class="legend-dot"></span>
|
||||||
|
<span>{{ $label }}</span>
|
||||||
|
<span class="legend-total">{{ ($diseaseNews['disease_data'] ?? collect())[$index] ?? 0 }}</span>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="trend-panel area-panel">
|
||||||
|
<h3><i class="bi bi-geo-alt-fill"></i> <span id="areaChartTitle">Daerah Kasus {{ $diseaseNews['top_disease'] }}</span></h3>
|
||||||
|
<p>Wilayah terbanyak untuk penyakit tertinggi pada periode yang dipilih.</p>
|
||||||
|
@if(($diseaseNews['area_data'] ?? collect())->count() > 0)
|
||||||
|
<div class="area-donut-layout">
|
||||||
|
<div class="area-donut-wrap">
|
||||||
|
<canvas id="landingAreaChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="area-list" id="areaChartList">
|
||||||
|
@foreach(($diseaseNews['area_labels'] ?? collect()) as $index => $label)
|
||||||
|
<div class="area-row">
|
||||||
|
<span class="area-dot" style="background:{{ $index === 0 ? '#2563eb' : '#93c5fd' }}"></span>
|
||||||
|
<span>{{ $label }}</span>
|
||||||
|
<span class="area-count">{{ ($diseaseNews['area_data'] ?? collect())[$index] ?? 0 }} kasus</span>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="trend-empty">Belum ada data alamat yang cukup untuk penyakit ini.</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="trend-bottom-grid">
|
||||||
|
<div class="trend-highlight">
|
||||||
|
<div class="trend-highlight-icon"><i class="bi bi-exclamation-triangle-fill"></i></div>
|
||||||
|
<div>
|
||||||
|
<span>Kasus tertinggi</span>
|
||||||
|
<strong>{{ $diseaseNews['top_disease'] }}</strong>
|
||||||
|
<p style="text-align:left;margin:8px 0 0;color:#64748b;font-size:14px;">
|
||||||
|
{{ $diseaseNews['total_cases'] ?? 0 }} kasus pada {{ $diseaseNews['period_label'] ?? 'periode ini' }}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="care-box handling">
|
||||||
|
<div class="care-icon"><i class="bi bi-lightbulb"></i></div>
|
||||||
|
<div>
|
||||||
|
<h3>Penanganan</h3>
|
||||||
|
@if(!empty($diseaseNews['handling']))
|
||||||
|
<ul>
|
||||||
|
@foreach($diseaseNews['handling'] as $item)
|
||||||
|
<li>{{ $item }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@else
|
||||||
|
<p style="text-align:left;margin:0;color:#64748b;">Data penanganan belum tersedia untuk penyakit ini.</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="care-box prevention">
|
||||||
|
<div class="care-icon"><i class="bi bi-shield"></i></div>
|
||||||
|
<div>
|
||||||
|
<h3>Pencegahan</h3>
|
||||||
|
@if(!empty($diseaseNews['prevention']))
|
||||||
|
<ul>
|
||||||
|
@foreach($diseaseNews['prevention'] as $item)
|
||||||
|
<li>{{ $item }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@else
|
||||||
|
<p style="text-align:left;margin:0;color:#64748b;">Data pencegahan belum tersedia untuk penyakit ini.</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="trend-empty">
|
||||||
|
Belum ada data diagnosis pada periode ini. Grafik akan otomatis tampil setelah data tersedia.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- FITUR -->
|
<!-- FITUR -->
|
||||||
<section id="fitur" data-aos="fade-up">
|
<section id="fitur" data-aos="fade-up">
|
||||||
|
|
@ -871,6 +1329,133 @@ function scrollToSection(id){
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
const landingDiseaseCanvas = document.getElementById('landingDiseaseChart');
|
||||||
|
const landingAreaCanvas = document.getElementById('landingAreaChart');
|
||||||
|
const landingDiseaseLabels = {!! json_encode($diseaseNews['disease_labels'] ?? []) !!};
|
||||||
|
const landingDiseaseData = {!! json_encode($diseaseNews['disease_data'] ?? []) !!};
|
||||||
|
const landingAreaLabels = {!! json_encode($diseaseNews['area_labels'] ?? []) !!};
|
||||||
|
const landingAreaData = {!! json_encode($diseaseNews['area_data'] ?? []) !!};
|
||||||
|
const landingAreaByDisease = {!! json_encode($diseaseNews['area_by_disease'] ?? []) !!};
|
||||||
|
const landingAreaColors = ['#2563eb', '#06b6d4', '#8b5cf6', '#f59e0b', '#10b981', '#ef4444'];
|
||||||
|
let landingAreaChart = null;
|
||||||
|
|
||||||
|
function renderLandingBarChart(canvas, labels, data, color, borderColor) {
|
||||||
|
if (!canvas || !labels.length || !data.length) return;
|
||||||
|
|
||||||
|
new Chart(canvas, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah Kasus',
|
||||||
|
data,
|
||||||
|
backgroundColor: color,
|
||||||
|
borderColor,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderRadius: 8,
|
||||||
|
maxBarThickness: 52
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: { mode: 'index', intersect: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { ticks: { display: false }, grid: { display: false } },
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: { precision: 0, color: '#475569' },
|
||||||
|
grid: { color: '#e5e7eb' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick(event, elements) {
|
||||||
|
if (!elements.length || canvas.id !== 'landingDiseaseChart') return;
|
||||||
|
const index = elements[0].index;
|
||||||
|
updateAreaChart(labels[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAreaList(labels, data) {
|
||||||
|
const list = document.getElementById('areaChartList');
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
if (!labels.length || !data.length) {
|
||||||
|
list.innerHTML = '<div class="trend-empty" style="min-height:120px;">Belum ada data alamat yang cukup untuk penyakit ini.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = labels.map((label, index) => `
|
||||||
|
<div class="area-row">
|
||||||
|
<span class="area-dot" style="background:${landingAreaColors[index] || '#93c5fd'}"></span>
|
||||||
|
<span>${escapeHtml(label)}</span>
|
||||||
|
<span class="area-count">${data[index] || 0} kasus</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAreaChart(diseaseName) {
|
||||||
|
const source = landingAreaByDisease[diseaseName] || { labels: [], data: [] };
|
||||||
|
const labels = source.labels || [];
|
||||||
|
const data = source.data || [];
|
||||||
|
const title = document.getElementById('areaChartTitle');
|
||||||
|
|
||||||
|
if (title) title.textContent = `Daerah Kasus ${diseaseName}`;
|
||||||
|
renderAreaList(labels, data);
|
||||||
|
|
||||||
|
if (!landingAreaChart) return;
|
||||||
|
landingAreaChart.data.labels = labels;
|
||||||
|
landingAreaChart.data.datasets[0].data = data;
|
||||||
|
landingAreaChart.data.datasets[0].backgroundColor = landingAreaColors;
|
||||||
|
landingAreaChart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLandingBarChart(landingDiseaseCanvas, landingDiseaseLabels, landingDiseaseData, '#6fcf97', '#4bb66f');
|
||||||
|
if (landingAreaCanvas && landingAreaLabels.length && landingAreaData.length) {
|
||||||
|
landingAreaChart = new Chart(landingAreaCanvas, {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: landingAreaLabels,
|
||||||
|
datasets: [{
|
||||||
|
data: landingAreaData,
|
||||||
|
backgroundColor: landingAreaColors,
|
||||||
|
borderColor: '#ffffff',
|
||||||
|
borderWidth: 5,
|
||||||
|
hoverOffset: 10
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label(context) {
|
||||||
|
return `${context.label}: ${context.raw} kasus`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const reveals = document.querySelectorAll('[data-aos]');
|
const reveals = document.querySelectorAll('[data-aos]');
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
use App\Http\Controllers\AdminController;
|
use App\Http\Controllers\AdminController;
|
||||||
use App\Http\Controllers\GejalaController;
|
use App\Http\Controllers\GejalaController;
|
||||||
use App\Http\Controllers\UlasanController;
|
use App\Http\Controllers\UlasanController;
|
||||||
use App\Models\Ulasan;
|
use App\Http\Controllers\LandingController;
|
||||||
|
|
||||||
Route::get('/admin/sort-diagnosis', [AdminController::class, 'sortDiagnosis']);
|
Route::get('/admin/sort-diagnosis', [AdminController::class, 'sortDiagnosis']);
|
||||||
Route::get('/admin/export-diagnosis', [AdminController::class, 'exportDiagnosisExcel'])
|
Route::get('/admin/export-diagnosis', [AdminController::class, 'exportDiagnosisExcel'])
|
||||||
|
|
@ -55,10 +55,7 @@
|
||||||
->name('ulasan.toggleHide')
|
->name('ulasan.toggleHide')
|
||||||
->middleware('auth');
|
->middleware('auth');
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', [LandingController::class, 'index'])->name('landing');
|
||||||
$ulasan = Ulasan::where('is_hidden', false)->latest()->take(3)->get();
|
|
||||||
return view('landing', compact('ulasan'));
|
|
||||||
});
|
|
||||||
|
|
||||||
Route::get('/biodata', function () {
|
Route::get('/biodata', function () {
|
||||||
return view('biodata');
|
return view('biodata');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue