From 9e1a910c2f8a8f67bb7af9719115e2f1194a9269 Mon Sep 17 00:00:00 2001 From: WahyuTegarP <158023677+WahyuTegarP@users.noreply.github.com> Date: Thu, 18 Jun 2026 10:10:52 +0700 Subject: [PATCH] update revisi --- app/Http/Controllers/DiagnosisController.php | 13 +- app/Http/Controllers/LandingController.php | 239 ++++++++ database/seeders/BiodataTrendSeeder.php | 139 +++++ database/seeders/DatabaseSeeder.php | 17 +- public/data/Bissmilah lagi.xlsx | Bin 11049 -> 11893 bytes python_api/app.py | 6 +- resources/views/biodata.blade.php | 216 +++++-- resources/views/gejala.blade.php | 329 +++++++++-- resources/views/landing.blade.php | 585 +++++++++++++++++++ routes/web.php | 9 +- 10 files changed, 1444 insertions(+), 109 deletions(-) create mode 100644 app/Http/Controllers/LandingController.php create mode 100644 database/seeders/BiodataTrendSeeder.php diff --git a/app/Http/Controllers/DiagnosisController.php b/app/Http/Controllers/DiagnosisController.php index 04c60e7..1ab3b32 100644 --- a/app/Http/Controllers/DiagnosisController.php +++ b/app/Http/Controllers/DiagnosisController.php @@ -76,10 +76,12 @@ public function prosesDiagnosis(Request $request) ]); } -return redirect()->route('hasil-diagnosis') -->with('diagnosis', $diagnosis) -->with('gejala', $inputNama); -$biodataId = session('biodata_id'); +session([ + 'diagnosis' => $diagnosis, + 'gejala' => $inputNama, +]); + +return redirect()->route('hasil-diagnosis'); } // 🔥 halaman hasil @@ -137,6 +139,7 @@ public function simpanBiodata(Request $request) 'umur_kucing' => 'required|numeric', 'jenis_kelamin' => 'required', '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([ @@ -179,4 +182,4 @@ private function getDiseaseDescription(string $diseaseName): string return ''; } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/LandingController.php b/app/Http/Controllers/LandingController.php new file mode 100644 index 0000000..474c926 --- /dev/null +++ b/app/Http/Controllers/LandingController.php @@ -0,0 +1,239 @@ +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))); + } +} diff --git a/database/seeders/BiodataTrendSeeder.php b/database/seeders/BiodataTrendSeeder.php new file mode 100644 index 0000000..0f493f4 --- /dev/null +++ b/database/seeders/BiodataTrendSeeder.php @@ -0,0 +1,139 @@ +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); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index ebe289c..b858cf9 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -5,6 +5,7 @@ use App\Models\User; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; class DatabaseSeeder extends Seeder { @@ -16,12 +17,16 @@ class DatabaseSeeder extends Seeder public function run(): void { // Create Admin User - User::create([ - 'name' => 'Admin PawMedic', - 'email' => 'admin@pawmedic.app', - 'password' => \Illuminate\Support\Facades\Hash::make('admin123'), - 'email_verified_at' => now(), - ]); + User::query()->updateOrCreate( + ['email' => 'admin@pawmedic.app'], + [ + 'name' => 'Admin PawMedic', + 'password' => Hash::make('admin123'), + 'email_verified_at' => now(), + ] + ); + + $this->call(BiodataTrendSeeder::class); // Optional: Create test user // User::factory()->create([ diff --git a/public/data/Bissmilah lagi.xlsx b/public/data/Bissmilah lagi.xlsx index 68752802c6178e0c36dda737b0652d1096e91b60..2710d82f70a91a9a9ff9e9cfde939291e8bc2987 100644 GIT binary patch delta 5084 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 delta 4216 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 B@(cg~ diff --git a/python_api/app.py b/python_api/app.py index 948f2a3..fea2b95 100644 --- a/python_api/app.py +++ b/python_api/app.py @@ -11,7 +11,7 @@ app = Flask(__name__) model = joblib.load("../python_artifacts/model.joblib") # ========================= -# LOAD FEATURE +# LOAD FEATURE/GEJALA # ========================= with open("../python_artifacts/feature_cols.json") as f: feature_cols = json.load(f) @@ -66,7 +66,7 @@ def predict(): print("INPUT VECTOR:", input_data) input_df = pd.DataFrame([input_data], columns=feature_cols) - +#melakukan prediksi hasil = model.predict(input_df)[0] penyakit = str(hasil).lower().strip() @@ -82,7 +82,7 @@ def predict(): "pertolongan": [], "pencegahan": [] }) - +#mengembalikan hasil prediksi ke laravel return jsonify({ "penyakit": hasil, "jenis": info["jenis"], diff --git a/resources/views/biodata.blade.php b/resources/views/biodata.blade.php index 2b879b6..3893d72 100644 --- a/resources/views/biodata.blade.php +++ b/resources/views/biodata.blade.php @@ -159,6 +159,71 @@ 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{ display:grid; grid-template-columns:1fr 1fr; @@ -343,7 +408,7 @@
-
+ @csrf @@ -441,15 +506,26 @@
- - Opsional - Untuk keperluan dokumentasi +
+
+ + +
+
+
+ Wajib memilih kecamatan dalam lingkup Kabupaten Jember.
@@ -482,45 +558,103 @@
diff --git a/resources/views/gejala.blade.php b/resources/views/gejala.blade.php index c8b2d90..2983cc3 100644 --- a/resources/views/gejala.blade.php +++ b/resources/views/gejala.blade.php @@ -5,6 +5,7 @@ Pilih Gejala - PawMedic + @@ -205,7 +206,7 @@ animation:fadeUp 0.8s cubic-bezier(0.16, 1, 0.3, 1); border:1px solid rgba(111,207,151,0.2); position:relative; - overflow:hidden; + overflow:visible; transform-style:preserve-3d; transition:transform 0.3s ease, box-shadow 0.3s ease; } @@ -237,6 +238,7 @@ } .form-card::after{ + display:none; content:''; position:absolute; top:-50%; @@ -378,6 +380,9 @@ position:relative; 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(2){animation-delay:0.1s;} @@ -409,6 +414,7 @@ .gejala-label{ display:flex; align-items:center; + flex-wrap:wrap; gap:14px; padding:20px 24px; background:linear-gradient(135deg, #ffffff 0%, #fafafa 100%); @@ -421,40 +427,20 @@ color:var(--text-dark); user-select:none; position:relative; - overflow:hidden; + overflow:visible; box-shadow: 0 4px 12px rgba(0,0,0,0.06), 0 0 0 0 rgba(111,207,151,0); 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{ background:linear-gradient(135deg, var(--primary-light) 0%, #ffffff 100%); border-color:var(--primary); - transform:translateY(-6px) scale(1.03) perspective(1000px) rotateX(-2deg); + transform:translateY(-3px); box-shadow: - 0 12px 32px rgba(111,207,151,0.25), - 0 0 0 4px rgba(111,207,151,0.1), - inset 0 1px 0 rgba(255,255,255,0.9); - border-width:3px; + 0 10px 24px rgba(111,207,151,0.18), + 0 0 0 4px rgba(111,207,151,0.08); } .gejala-checkbox:checked + .gejala-label{ @@ -466,23 +452,7 @@ 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); - transform:translateY(-4px) scale(1.02) perspective(1000px) rotateX(-1deg); - 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); - } + transform:translateY(-2px); } .gejala-checkbox:checked + .gejala-label::before{ @@ -527,6 +497,156 @@ 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{ background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); color:white; @@ -826,6 +946,65 @@ class="search-input" + @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 +
@foreach($gejala as $item)
@@ -837,7 +1016,11 @@ class="gejala-checkbox" id="gejala_{{ $loop->index }}" >
@endforeach @@ -858,6 +1041,19 @@ class="gejala-checkbox"
+ + @include('components.toast') @include('components.scroll-top') @@ -866,6 +1062,24 @@ class="gejala-checkbox" const submitBtn = document.getElementById('submitBtn'); const selectedCount = document.getElementById('selectedCount'); 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() { const checked = document.querySelectorAll('.gejala-checkbox:checked').length; @@ -875,13 +1089,13 @@ function updateSelectedCount() { selectedCount.classList.add('animate'); 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) { - submitBtn.disabled = false; submitBtn.style.opacity = '1'; + submitBtn.setAttribute('aria-disabled', 'false'); } else { - submitBtn.disabled = true; submitBtn.style.opacity = '0.6'; + submitBtn.setAttribute('aria-disabled', 'true'); } } @@ -890,6 +1104,23 @@ function 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.addEventListener('submit', function(e) { @@ -898,15 +1129,17 @@ function updateSelectedCount() { const checked = document.querySelectorAll('.gejala-checkbox:checked'); if (checked.length < 4) { - alert("Minimal pilih 4 gejala!"); + showDiagnosisAlert("Minimal pilih 4 gejala sebelum melanjutkan diagnosis."); return; } if (checked.length > 7) { - alert("Maksimal hanya 7 gejala!"); + showDiagnosisAlert("Maksimal hanya 7 gejala yang dapat dipilih."); return; } + hideDiagnosisAlert(); + // ambil gejala let gejala = []; checked.forEach(c => gejala.push(c.value)); diff --git a/resources/views/landing.blade.php b/resources/views/landing.blade.php index 5975f67..fcf8663 100644 --- a/resources/views/landing.blade.php +++ b/resources/views/landing.blade.php @@ -17,6 +17,9 @@ --ff-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; --space:28px; --muted: #6b7280; + --primary: #6fcf97; + --primary-dark: #4bb66f; + --primary-light: #e8f7ef; } body{ margin:0; @@ -292,6 +295,299 @@ 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{ display:grid; @@ -574,6 +870,11 @@ .features{ grid-template-columns:repeat(2,1fr); } + .trend-grid, + .trend-bottom-grid, + .area-donut-layout{ + grid-template-columns:1fr; + } section{ margin-top:72px; } @@ -625,6 +926,54 @@ .features{ 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{ font-size:20px; line-height:1.3; @@ -768,6 +1117,115 @@ + +
@@ -871,6 +1329,133 @@ function scrollToSection(id){ }); } + +