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 6875280..2710d82 100644 Binary files a/public/data/Bissmilah lagi.xlsx and b/public/data/Bissmilah lagi.xlsx differ 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){ }); } + +