From 920e6caf3da29bff9879d31ed1a6d091ca4e0697 Mon Sep 17 00:00:00 2001 From: WahyuTegarP <158023677+WahyuTegarP@users.noreply.github.com> Date: Fri, 8 May 2026 17:47:40 +0700 Subject: [PATCH] update besar besaran --- app/Http/Controllers/AdminController.php | 537 +++++++++++++++++- app/Http/Controllers/DiagnosisController.php | 43 +- app/Http/Controllers/GejalaController.php | 2 +- app/Http/Controllers/UlasanController.php | 23 +- app/Models/Biodata.php | 5 +- app/Models/Ulasan.php | 3 +- composer.json | 1 + composer.lock | 530 ++++++++++++++++- ...11_add_gejala_dipilih_to_biodata_table.php | 32 ++ ..._074540_add_is_hidden_to_ulasans_table.php | 32 ++ public/favicon.svg | 15 + resources/views/admin/dashboard.blade.php | 240 ++++++-- .../views/admin/disease-settings.blade.php | 57 +- resources/views/admin/faq-settings.blade.php | 126 +++- .../views/admin/forgot-password.blade.php | 47 ++ resources/views/admin/login.blade.php | 117 +++- .../views/admin/reset-password.blade.php | 37 ++ .../admin/training-data-settings.blade.php | 195 +++++++ .../training-symptoms-settings.blade.php | 77 +++ resources/views/biodata.blade.php | 20 +- resources/views/components/toast.blade.php | 8 +- resources/views/faq.blade.php | 22 +- resources/views/gejala.blade.php | 49 +- resources/views/hasil-diagnosis.blade.php | 162 +++--- resources/views/landing.blade.php | 366 ++++++++---- resources/views/loading.blade.php | 182 ++++-- .../views/pdf/hasil-diagnosis-pdf.blade.php | 71 +++ resources/views/ulasan.blade.php | 156 ++++- routes/web.php | 37 +- 29 files changed, 2797 insertions(+), 395 deletions(-) create mode 100644 database/migrations/2026_04_29_141911_add_gejala_dipilih_to_biodata_table.php create mode 100644 database/migrations/2026_05_08_074540_add_is_hidden_to_ulasans_table.php create mode 100644 public/favicon.svg create mode 100644 resources/views/admin/forgot-password.blade.php create mode 100644 resources/views/admin/reset-password.blade.php create mode 100644 resources/views/admin/training-data-settings.blade.php create mode 100644 resources/views/admin/training-symptoms-settings.blade.php create mode 100644 resources/views/pdf/hasil-diagnosis-pdf.blade.php diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index f81cedd..24ff3ce 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -4,7 +4,9 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Mail; use App\Models\Biodata; use Carbon\Carbon; use Carbon\CarbonPeriod; @@ -12,6 +14,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\File; use Illuminate\Support\Str; +use App\Models\User; use Symfony\Component\HttpFoundation\StreamedResponse; class AdminController extends Controller @@ -32,6 +35,12 @@ public function authenticate(Request $request) 'password' => 'required', ]); + if (!str_ends_with(strtolower($credentials['email']), '@pawmedic.app')) { + return back()->withErrors([ + 'email' => 'Login admin hanya untuk email @pawmedic.app.', + ])->onlyInput('email'); + } + if (Auth::attempt($credentials, $request->boolean('remember'))) { $request->session()->regenerate(); @@ -43,6 +52,171 @@ public function authenticate(Request $request) ])->onlyInput('email'); } + public function forgotPasswordPage() + { + return view('admin.forgot-password'); + } + + public function sendForgotOtp(Request $request) + { + $data = $request->validate([ + 'otp_email' => 'required|email', + ]); + + $otpEmail = strtolower((string)$data['otp_email']); + $otp = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); + $cacheKey = 'admin_password_recovery_otp_' . sha1($otpEmail); + + Cache::put($cacheKey, [ + 'otp_hash' => Hash::make($otp), + 'otp_email' => $otpEmail, + ], now()->addMinutes(10)); + + Mail::raw( + "Kode OTP reset password PawMedic Anda adalah: {$otp}\n\nKode berlaku 10 menit. Jangan bagikan kode ini kepada siapa pun.", + function ($message) use ($otpEmail) { + $message->to($otpEmail)->subject('OTP Reset Password PawMedic'); + } + ); + + return back() + ->with('success', 'Kode OTP berhasil dikirim. Cek email Anda.') + ->with('otp_email', $otpEmail); + } + + public function verifyForgotOtp(Request $request) + { + $data = $request->validate([ + 'otp_email' => 'required|email', + 'otp' => 'required|string|size:6', + ]); + + $otpEmail = strtolower((string)$data['otp_email']); + $cacheKey = 'admin_password_recovery_otp_' . sha1($otpEmail); + $cached = Cache::get($cacheKey); + + if (!is_array($cached) || empty($cached['otp_hash'])) { + return back()->with('error', 'Kode OTP tidak ditemukan atau sudah kedaluwarsa.')->with('otp_email', $otpEmail); + } + + if (!Hash::check((string)$data['otp'], (string)$cached['otp_hash'])) { + return back()->with('error', 'Kode tidak valid.')->with('otp_email', $otpEmail); + } + + session([ + 'forgot_otp_verified' => true, + 'forgot_otp_email' => $otpEmail, + ]); + + return redirect()->route('admin.forgot.reset.form'); + } + + public function resetPasswordPage() + { + if (!session('forgot_otp_verified')) { + return redirect()->route('admin.forgot.password')->with('error', 'Silakan verifikasi OTP terlebih dahulu.'); + } + + return view('admin.reset-password'); + } + + public function resetPasswordSubmit(Request $request) + { + if (!session('forgot_otp_verified')) { + return redirect()->route('admin.forgot.password')->with('error', 'Silakan verifikasi OTP terlebih dahulu.'); + } + + $data = $request->validate([ + 'new_password' => 'required|string|min:6|confirmed', + ]); + + $admin = User::query() + ->where('email', 'like', '%@pawmedic.app') + ->orderBy('id') + ->first(); + + if (!$admin) { + return redirect()->route('admin.forgot.password')->with('error', 'Akun admin @pawmedic.app tidak ditemukan.'); + } + + $admin->password = Hash::make((string)$data['new_password']); + $admin->save(); + + $otpEmail = (string)session('forgot_otp_email', ''); + if ($otpEmail !== '') { + Cache::forget('admin_password_recovery_otp_' . sha1($otpEmail)); + } + + $request->session()->forget(['forgot_otp_verified', 'forgot_otp_email']); + + return redirect()->route('admin.login')->with('success', 'Password admin berhasil diganti. Silakan login.'); + } + + public function sendRecoveryOtp(Request $request) + { + $data = $request->validate([ + 'otp_email' => 'required|email', + ]); + + $otp = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); + $otpEmail = strtolower($data['otp_email']); + $cacheKey = 'admin_password_recovery_otp_' . sha1($otpEmail); + Cache::put($cacheKey, [ + 'otp_hash' => Hash::make($otp), + 'otp_email' => $otpEmail, + ], now()->addMinutes(10)); + + Mail::raw( + "Kode OTP reset password PawMedic Anda adalah: {$otp}\n\nKode berlaku 10 menit. Jangan berikan kode ini ke siapa pun.", + function ($message) use ($otpEmail) { + $message->to($otpEmail)->subject('OTP Reset Password PawMedic'); + } + ); + + return back()->with('success', 'OTP berhasil dikirim ke email aplikasi. Cek inbox (atau log mail jika mode lokal).'); + } + + public function recoverPassword(Request $request) + { + $data = $request->validate([ + 'otp_email' => 'required|email', + 'email' => 'required|email', + 'otp' => 'required|string|size:6', + 'new_password' => 'required|string|min:6|confirmed', + ]); + + $otpEmail = strtolower($data['otp_email']); + $email = strtolower($data['email']); + if (!str_ends_with($email, '@pawmedic.app')) { + return back()->with('error', 'Reset password admin hanya untuk email @pawmedic.app.'); + } + + $cacheKey = 'admin_password_recovery_otp_' . sha1($otpEmail); + $cached = Cache::get($cacheKey); + if (!is_array($cached) || empty($cached['otp_hash'])) { + return back()->with('error', 'OTP tidak ditemukan atau sudah kedaluwarsa. Silakan kirim OTP ulang.'); + } + + if (!hash_equals((string)($cached['otp_email'] ?? ''), $otpEmail)) { + return back()->with('error', 'Email aplikasi tidak sesuai dengan OTP.'); + } + + if (!Hash::check((string) $data['otp'], (string) $cached['otp_hash'])) { + return back()->with('error', 'Kode OTP tidak valid.'); + } + + $admin = User::where('email', $data['email'])->first(); + if (!$admin) { + return back()->with('error', 'Email admin tidak ditemukan.'); + } + + $admin->password = Hash::make($data['new_password']); + $admin->save(); + Cache::forget($cacheKey); + + return redirect()->route('admin.login')->with('success', 'Password berhasil direset. Silakan login dengan password baru.'); + } + public function dashboard() { @@ -120,19 +294,51 @@ public function dashboard() $chartLabels = $diseaseStats->keys()->values(); $chartData = $diseaseStats->values(); - // ๐Ÿ”ฅ 7 hari terakhir + // Data tren: 7 hari terakhir $period = CarbonPeriod::create(Carbon::now()->subDays(6), Carbon::now()); - - $dailyLabels = []; - $dailyData = []; + $trend7Labels = []; + $trend7Data = []; foreach ($period as $date) { $count = Biodata::whereDate('created_at', $date)->count(); - - $dailyLabels[] = $date->format('d M'); - $dailyData[] = $count; + $trend7Labels[] = $date->format('d M'); + $trend7Data[] = $count; } + // Data tren: per bulan (tahun berjalan) + $monthNames = [ + 1 => 'Jan', 2 => 'Feb', 3 => 'Mar', 4 => 'Apr', 5 => 'Mei', 6 => 'Jun', + 7 => 'Jul', 8 => 'Agu', 9 => 'Sep', 10 => 'Okt', 11 => 'Nov', 12 => 'Des', + ]; + $monthlyRaw = Biodata::query() + ->selectRaw('MONTH(created_at) as m, COUNT(*) as total') + ->whereYear('created_at', Carbon::now()->year) + ->groupBy('m') + ->orderBy('m') + ->get() + ->keyBy('m'); + + $trendMonthLabels = []; + $trendMonthData = []; + for ($m = 1; $m <= 12; $m++) { + $trendMonthLabels[] = $monthNames[$m]; + $trendMonthData[] = (int)($monthlyRaw[$m]->total ?? 0); + } + + // Data tren: seluruh periode (agregasi per bulan-tahun) + $allRaw = Biodata::query() + ->selectRaw("DATE_FORMAT(created_at, '%Y-%m') as ym, COUNT(*) as total") + ->groupBy('ym') + ->orderBy('ym') + ->get(); + + $trendAllLabels = $allRaw->map(function ($row) use ($monthNames) { + [$y, $m] = explode('-', (string)$row->ym); + $month = (int)$m; + return ($monthNames[$month] ?? $m) . ' ' . $y; + })->values()->all(); + $trendAllData = $allRaw->pluck('total')->map(fn ($n) => (int)$n)->values()->all(); + // kirim ke blade $stats = [ 'total_diagnosis' => $totalDiagnosis, @@ -144,8 +350,12 @@ public function dashboard() 'chart_data' => $chartData, 'diagnosis_diff' => $diff, 'today_top_disease' => $todayDisease, - 'daily_labels' => $dailyLabels, - 'daily_data' => $dailyData + 'trend_7_labels' => $trend7Labels, + 'trend_7_data' => $trend7Data, + 'trend_month_labels' => $trendMonthLabels, + 'trend_month_data' => $trendMonthData, + 'trend_all_labels' => $trendAllLabels, + 'trend_all_data' => $trendAllData, ]; // ๐Ÿ”ฅ STAT $ratingChart = Ulasan::select('rating', DB::raw('count(*) as total')) @@ -388,6 +598,315 @@ public function faqPage() ]); } +public function trainingDataSettings() +{ + $featureCols = $this->loadFeatureCols(); + $items = collect($this->loadTrainingItems()) + ->map(fn ($item) => $this->normalizeTrainingItem($item, $featureCols)) + ->values() + ->all(); + + return view('admin.training-data-settings', [ + 'items' => $items, + 'featureCols' => $featureCols, + 'featureCount' => count($featureCols), + ]); +} + +public function saveTrainingDataSettings(Request $request) +{ + $ids = $request->input('id', []); + $diseases = $request->input('disease', []); + $categories = $request->input('category', []); + $featureCols = $this->loadFeatureCols(); + $existingById = collect($this->loadTrainingItems()) + ->keyBy(fn ($it) => (string)($it['id'] ?? '')); + + $items = []; + $count = max(count($ids), count($diseases), count($categories)); + for ($i = 0; $i < $count; $i++) { + $id = trim((string)($ids[$i] ?? '')); + $disease = trim((string)($diseases[$i] ?? '')); + $category = trim((string)($categories[$i] ?? '')); + + if ($disease === '' && $category === '') { + continue; + } + + $existing = $id !== '' ? ($existingById->get($id) ?? []) : []; + if ($id === '') { + $id = (string) Str::uuid(); + } + $samples = is_array($existing['symptom_samples'] ?? null) ? $existing['symptom_samples'] : []; + $samples = $this->normalizeSymptomSamples($samples, $featureCols); + $filledSamples = $this->countFilledSamples($samples, $featureCols); + + $items[] = [ + 'id' => $id, + 'disease' => $disease, + 'category' => $category, + 'symptom_samples' => $samples, + 'samples' => $filledSamples, + 'status' => $filledSamples >= 10 ? 'ready-train' : 'need-samples', + 'updated_at' => now()->toDateTimeString(), + ]; + } + + $path = $this->trainingItemsPath(); + $dir = dirname($path); + if (!is_dir($dir)) { + File::makeDirectory($dir, 0755, true); + } + file_put_contents( + $path, + json_encode($items, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); + + return redirect()->route('admin.training.settings')->with('success', 'Data calon training berhasil disimpan.'); +} + +public function editTrainingSymptoms(string $id) +{ + $featureCols = $this->loadFeatureCols(); + $items = collect($this->loadTrainingItems()) + ->map(fn ($item) => $this->normalizeTrainingItem($item, $featureCols)); + $item = $items->first(fn ($it) => (string)($it['id'] ?? '') === $id); + + if (!$item) { + return redirect()->route('admin.training.settings')->with('error', 'Data penyakit tidak ditemukan.'); + } + + return view('admin.training-symptoms-settings', [ + 'item' => $item, + 'featureCols' => $featureCols, + ]); +} + +public function saveTrainingSymptoms(Request $request, string $id) +{ + $featureCols = $this->loadFeatureCols(); + $all = collect($this->loadTrainingItems()) + ->map(fn ($item) => $this->normalizeTrainingItem($item, $featureCols)) + ->values(); + + $idx = $all->search(fn ($it) => (string)($it['id'] ?? '') === $id); + if ($idx === false) { + return redirect()->route('admin.training.settings')->with('error', 'Data penyakit tidak ditemukan.'); + } + + $rowsInput = $request->input('sample_rows', []); + $samples = []; + for ($r = 0; $r < 10; $r++) { + $row = (isset($rowsInput[$r]) && is_array($rowsInput[$r])) ? $rowsInput[$r] : []; + $sample = []; + foreach ($featureCols as $col) { + $sample[$col] = isset($row[$col]) && (string)$row[$col] === '1' ? 'Ya' : 'Tidak'; + } + $samples[] = $sample; + } + + $item = $all[$idx]; + $filled = $this->countFilledSamples($samples, $featureCols); + $item['symptom_samples'] = $samples; + $item['samples'] = $filled; + $item['status'] = $filled >= 10 ? 'ready-train' : 'need-samples'; + $item['updated_at'] = now()->toDateTimeString(); + $all[$idx] = $item; + + $this->storeTrainingItems($all->all()); + + return redirect()->route('admin.training.settings')->with('success', 'Data gejala per sample berhasil disimpan.'); +} + +public function downloadTrainingData() +{ + $featureCols = $this->loadFeatureCols(); + $items = collect($this->loadTrainingItems()) + ->map(fn ($item) => $this->normalizeTrainingItem($item, $featureCols)) + ->values() + ->all(); + + $filename = 'data-calon-training-pawmedic-' . now()->format('Ymd-His') . '.xls'; + $headers = [ + 'Content-Type' => 'application/vnd.ms-excel; charset=UTF-8', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + ]; + + return response()->streamDownload(function () use ($items, $featureCols) { + echo ''; + echo ''; + echo ''; + echo ''; + foreach ($featureCols as $col) { + echo ''; + } + echo ''; + + $no = 1; + foreach ($items as $item) { + $sampleRows = $this->normalizeSymptomSamples($item['symptom_samples'] ?? [], $featureCols); + foreach ($sampleRows as $sampleIndex => $flags) { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + foreach ($featureCols as $col) { + $v = (string)($flags[$col] ?? 'Tidak'); + echo ''; + } + echo ''; + } + } + echo '
NoPenyakitGolonganSample Ke' . e($col) . '
' . $no++ . '' . e((string)($item['disease'] ?? '')) . '' . e((string)($item['category'] ?? '')) . '' . e((string)($sampleIndex + 1)) . '' . e(strcasecmp($v, 'Ya') === 0 ? 'Ya' : 'Tidak') . '
'; + }, $filename, $headers); +} + +private function trainingItemsPath(): string +{ + return storage_path('app/training_items.json'); +} + +private function loadTrainingItems(): array +{ + $path = $this->trainingItemsPath(); + if (!file_exists($path)) { + return []; + } + $decoded = json_decode((string) file_get_contents($path), true); + return is_array($decoded) ? $decoded : []; +} + +private function storeTrainingItems(array $items): void +{ + $path = $this->trainingItemsPath(); + $dir = dirname($path); + if (!is_dir($dir)) { + File::makeDirectory($dir, 0755, true); + } + file_put_contents( + $path, + json_encode($items, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); +} + +private function normalizeTrainingItem($item, array $featureCols): array +{ + $base = is_array($item) ? $item : []; + $id = trim((string)($base['id'] ?? '')); + if ($id === '') { + $id = (string) Str::uuid(); + } + + $samples = $this->normalizeSymptomSamples($base['symptom_samples'] ?? [], $featureCols); + $filled = $this->countFilledSamples($samples, $featureCols); + $status = $filled >= 10 ? 'ready-train' : 'need-samples'; + + // Migrasi data lama (symptom_flags tunggal) ke baris sample pertama. + $legacyFlags = is_array($base['symptom_flags'] ?? null) ? $base['symptom_flags'] : []; + if (!empty($legacyFlags) && $filled === 0) { + $first = []; + foreach ($featureCols as $col) { + $v = (string)($legacyFlags[$col] ?? 'Tidak'); + $first[$col] = strcasecmp($v, 'Ya') === 0 ? 'Ya' : 'Tidak'; + } + $samples[0] = $first; + $filled = $this->countFilledSamples($samples, $featureCols); + $status = $filled >= 10 ? 'ready-train' : 'need-samples'; + } + + return array_merge($base, [ + 'id' => $id, + 'symptom_samples' => $samples, + 'samples' => $filled, + 'status' => $status, + ]); +} + +private function normalizeSymptomSamples($rows, array $featureCols): array +{ + $list = is_array($rows) ? array_values($rows) : []; + $normalized = []; + + for ($r = 0; $r < 10; $r++) { + $row = (isset($list[$r]) && is_array($list[$r])) ? $list[$r] : []; + $sample = []; + foreach ($featureCols as $col) { + $v = (string)($row[$col] ?? 'Tidak'); + $sample[$col] = strcasecmp($v, 'Ya') === 0 ? 'Ya' : 'Tidak'; + } + $normalized[] = $sample; + } + + return $normalized; +} + +private function countFilledSamples(array $samples, array $featureCols): int +{ + $count = 0; + foreach ($samples as $row) { + $hasYes = false; + foreach ($featureCols as $col) { + if (strcasecmp((string)($row[$col] ?? 'Tidak'), 'Ya') === 0) { + $hasYes = true; + break; + } + } + if ($hasYes) { + $count++; + } + } + return $count; +} + +public function downloadTrainingTemplate() +{ + $featureCols = $this->loadFeatureCols(); + $filename = 'template-training-pawmedic-' . now()->format('Ymd-His') . '.csv'; + + $headers = [ + 'Content-Type' => 'text/csv; charset=UTF-8', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + ]; + + return response()->streamDownload(function () use ($featureCols) { + $handle = fopen('php://output', 'w'); + fwrite($handle, chr(0xEF) . chr(0xBB) . chr(0xBF)); + + $cols = array_values(array_filter($featureCols, fn($c) => trim((string)$c) !== '')); + $header = array_merge($cols, ['Golongan', 'Penyakit']); + fputcsv($handle, $header, ';'); + + // Dua baris contoh isi sebagai panduan format ya/tidak. + $rowYa = array_fill(0, count($cols), 'Ya'); + $rowTidak = array_fill(0, count($cols), 'Tidak'); + fputcsv($handle, array_merge($rowYa, ['ContohKategori', 'Contoh Penyakit A']), ';'); + fputcsv($handle, array_merge($rowTidak, ['ContohKategori', 'Contoh Penyakit B']), ';'); + + fclose($handle); + }, $filename, $headers); +} + +private function loadFeatureCols(): array +{ + $path = base_path('python_artifacts/feature_cols.json'); + if (!file_exists($path)) { + return []; + } + + $decoded = json_decode((string) file_get_contents($path), true); + if (!is_array($decoded)) { + return []; + } + + return array_values(array_filter($decoded, function ($col) { + $name = trim((string)$col); + if ($name === '') return false; + if (stripos($name, 'Unnamed:') === 0) return false; + return true; + })); +} + private function faqPath(): string { return storage_path('app/faqs.json'); diff --git a/app/Http/Controllers/DiagnosisController.php b/app/Http/Controllers/DiagnosisController.php index 333f150..04c60e7 100644 --- a/app/Http/Controllers/DiagnosisController.php +++ b/app/Http/Controllers/DiagnosisController.php @@ -4,23 +4,36 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; +use Barryvdh\DomPDF\Facade\Pdf; use App\Models\Biodata; class DiagnosisController extends Controller { public function prosesDiagnosis(Request $request) { - $input = $request->input('gejala', []); + $rawInput = $request->input('gejala', []); + $input = $rawInput; + if (is_string($rawInput)) { + $decoded = json_decode($rawInput, true); + if (is_array($decoded)) { + $input = $decoded; + } else { + $input = array_filter(array_map('trim', explode(',', $rawInput))); + } + } + if (!is_array($input)) { + $input = []; + } // validasi minimal 3 gejala if (count($input) < 3) { return redirect()->route('gejala') - ->with('error', 'Pilih minimal 5 dan maksimal 7 gejala!'); + ->with('error', 'Pilih minimal 3 gejala!'); } $inputNama = $input; // ambil fitur dari Python - $response = Http::get('http://127.0.0.1:5000/gejala'); + $response = Http::get(env('API_MODEL') . '/gejala'); if (!$response->successful()) { return redirect()->route('gejala') @@ -37,7 +50,7 @@ public function prosesDiagnosis(Request $request) } // kirim ke Python API - $response = Http::post('http://127.0.0.1:5000/predict', $fiturAssoc); + $response = Http::post(env('API_MODEL') . '/predict', $fiturAssoc); if (!$response->successful()) { return redirect()->route('gejala') @@ -58,7 +71,8 @@ public function prosesDiagnosis(Request $request) if ($biodataId) { \App\Models\Biodata::where('id', $biodataId)->update([ 'hasil_diagnosis' => $diagnosis['nama'], - 'jenis' => $diagnosis['kategori'] + 'jenis' => $diagnosis['kategori'], + 'gejala_dipilih' => json_encode(array_values($inputNama), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ]); } @@ -96,6 +110,25 @@ public function hasil() ]); } + public function downloadPdf() + { + $diagnosis = session('diagnosis', []); + $gejala = session('gejala', []); + $diseaseName = trim((string)($diagnosis['nama'] ?? '')); + $description = $this->getDiseaseDescription($diseaseName); + $generatedAt = now()->format('d M Y H:i'); + + $pdf = Pdf::loadView('pdf.hasil-diagnosis-pdf', [ + 'diagnosis' => $diagnosis, + 'gejala' => is_array($gejala) ? $gejala : [], + 'diseaseDescription' => $description, + 'generatedAt' => $generatedAt, + ])->setPaper('a4', 'portrait'); + + $filename = 'hasil-diagnosis-pawmedic-' . now()->format('Ymd-His') . '.pdf'; + return $pdf->download($filename); + } + public function simpanBiodata(Request $request) { $request->validate([ diff --git a/app/Http/Controllers/GejalaController.php b/app/Http/Controllers/GejalaController.php index 1e39f0b..5dfd9b0 100644 --- a/app/Http/Controllers/GejalaController.php +++ b/app/Http/Controllers/GejalaController.php @@ -8,7 +8,7 @@ class GejalaController extends Controller { public function index() { - $response = Http::get('http://127.0.0.1:5000/gejala'); + $response = Http::get(env('API_MODEL') . '/gejala'); if (!$response->successful()) { return back()->with('error', 'Tidak bisa mengambil data gejala dari API'); } diff --git a/app/Http/Controllers/UlasanController.php b/app/Http/Controllers/UlasanController.php index 5243638..87ef503 100644 --- a/app/Http/Controllers/UlasanController.php +++ b/app/Http/Controllers/UlasanController.php @@ -10,7 +10,12 @@ class UlasanController extends Controller { public function index() { - $ulasan = Ulasan::latest()->get(); + $query = Ulasan::query()->latest(); + $isAdmin = Auth::check() && Auth::user()->email === 'admin@pawmedic.app'; + if (!$isAdmin) { + $query->where('is_hidden', false); + } + $ulasan = $query->get(); // ๐Ÿ”ฅ TOTAL ULASAN $total = $ulasan->count(); // ๐Ÿ”ฅ RATING RATA-RATA @@ -39,5 +44,21 @@ public function destroy($id) return redirect()->back()->with('success', 'Ulasan berhasil dihapus'); } + + public function toggleHide($id) + { + if (!Auth::check() || Auth::user()->email !== 'admin@pawmedic.app') { + abort(403); + } + + $ulasan = Ulasan::findOrFail($id); + $ulasan->is_hidden = !$ulasan->is_hidden; + $ulasan->save(); + + return redirect()->back()->with( + 'success', + $ulasan->is_hidden ? 'Ulasan disembunyikan.' : 'Ulasan ditampilkan kembali.' + ); + } } diff --git a/app/Models/Biodata.php b/app/Models/Biodata.php index ab65b11..81a9172 100644 --- a/app/Models/Biodata.php +++ b/app/Models/Biodata.php @@ -16,6 +16,9 @@ class Biodata extends Model 'berat_badan', 'ras_kucing', 'alamat', - 'no_telepon' + 'no_telepon', + 'hasil_diagnosis', + 'jenis', + 'gejala_dipilih', ]; } diff --git a/app/Models/Ulasan.php b/app/Models/Ulasan.php index 1f7dd3c..df2baa9 100644 --- a/app/Models/Ulasan.php +++ b/app/Models/Ulasan.php @@ -12,6 +12,7 @@ class Ulasan extends Model 'nama_kucing', 'hasil_diagnosis', 'rating', - 'komentar' + 'komentar', + 'is_hidden', ]; } diff --git a/composer.json b/composer.json index 44c6054..eef6f76 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "license": "MIT", "require": { "php": "^8.2", + "barryvdh/laravel-dompdf": "^3.1", "laravel/framework": "^12.0", "laravel/tinker": "^2.10.1" }, diff --git a/composer.lock b/composer.lock index 1659f4d..a12c3a8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,85 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c514d8f7b9fc5970bdd94287905ef584", + "content-hash": "398ac677a9c6311454725e156f8de59c", "packages": [ + { + "name": "barryvdh/laravel-dompdf", + "version": "v3.1.2", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-dompdf.git", + "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc", + "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc", + "shasum": "" + }, + "require": { + "dompdf/dompdf": "^3.0", + "illuminate/support": "^9|^10|^11|^12|^13.0", + "php": "^8.1" + }, + "require-dev": { + "larastan/larastan": "^2.7|^3.0", + "orchestra/testbench": "^7|^8|^9.16|^10|^11.0", + "phpro/grumphp": "^2.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "PDF": "Barryvdh\\DomPDF\\Facade\\Pdf", + "Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf" + }, + "providers": [ + "Barryvdh\\DomPDF\\ServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\DomPDF\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "A DOMPDF Wrapper for Laravel", + "keywords": [ + "dompdf", + "laravel", + "pdf" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-dompdf/issues", + "source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.2" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2026-02-21T08:51:10+00:00" + }, { "name": "brick/math", "version": "0.14.1", @@ -377,6 +454,161 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "dompdf/dompdf", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "shasum": "" + }, + "require": { + "dompdf/php-font-lib": "^1.0.0", + "dompdf/php-svg-lib": "^1.0.0", + "ext-dom": "*", + "ext-mbstring": "*", + "masterminds/html5": "^2.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ext-gd": "*", + "ext-json": "*", + "ext-zip": "*", + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0" + }, + "suggest": { + "ext-gd": "Needed to process images", + "ext-gmagick": "Improves image processing performance", + "ext-imagick": "Improves image processing performance", + "ext-zlib": "Needed for pdf stream compression" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dompdf\\": "src/" + }, + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" + } + ], + "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", + "homepage": "https://github.com/dompdf/dompdf", + "support": { + "issues": "https://github.com/dompdf/dompdf/issues", + "source": "https://github.com/dompdf/dompdf/tree/v3.1.5" + }, + "time": "2026-03-03T13:54:37+00:00" + }, + { + "name": "dompdf/php-font-lib", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12" + }, + "type": "library", + "autoload": { + "psr-4": { + "FontLib\\": "src/FontLib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "The FontLib Community", + "homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse, export and make subsets of different types of font files.", + "homepage": "https://github.com/dompdf/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/1.0.2" + }, + "time": "2026-01-20T14:10:26+00:00" + }, + { + "name": "dompdf/php-svg-lib", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4 || ^9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Svg\\": "src/Svg" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "The SvgLib Community", + "homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/dompdf/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2" + }, + "time": "2026-01-02T16:01:13+00:00" + }, { "name": "dragonmantank/cron-expression", "version": "v3.6.0", @@ -2019,6 +2251,73 @@ ], "time": "2025-12-07T16:03:21+00:00" }, + { + "name": "masterminds/html5", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "fcf91eb64359852f00d921887b219479b4f21251" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" + }, + "time": "2025-07-25T09:04:22+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", @@ -3290,6 +3589,86 @@ }, "time": "2025-12-14T04:43:48+00:00" }, + { + "name": "sabberworm/php-css-parser", + "version": "v9.3.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", + "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.32 || 2.1.32", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7", + "phpunit/phpunit": "8.5.52", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.2.8", + "rector/type-perfect": "1.0.0 || 2.1.0", + "squizlabs/php_codesniffer": "4.0.1", + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.4.x-dev" + } + }, + "autoload": { + "files": [ + "src/Rule/Rule.php", + "src/RuleSet/RuleContainer.php" + ], + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" + } + ], + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0" + }, + "time": "2026-03-03T17:31:43+00:00" + }, { "name": "symfony/clock", "version": "v7.4.0", @@ -5791,6 +6170,149 @@ ], "time": "2025-10-27T20:36:44+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+00:00" + }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "v2.4.0", @@ -8371,12 +8893,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.2" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/database/migrations/2026_04_29_141911_add_gejala_dipilih_to_biodata_table.php b/database/migrations/2026_04_29_141911_add_gejala_dipilih_to_biodata_table.php new file mode 100644 index 0000000..c0ddd02 --- /dev/null +++ b/database/migrations/2026_04_29_141911_add_gejala_dipilih_to_biodata_table.php @@ -0,0 +1,32 @@ +longText('gejala_dipilih')->nullable()->after('jenis'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('biodata', function (Blueprint $table) { + if (Schema::hasColumn('biodata', 'gejala_dipilih')) { + $table->dropColumn('gejala_dipilih'); + } + }); + } +}; diff --git a/database/migrations/2026_05_08_074540_add_is_hidden_to_ulasans_table.php b/database/migrations/2026_05_08_074540_add_is_hidden_to_ulasans_table.php new file mode 100644 index 0000000..2b64045 --- /dev/null +++ b/database/migrations/2026_05_08_074540_add_is_hidden_to_ulasans_table.php @@ -0,0 +1,32 @@ +boolean('is_hidden')->default(false)->after('komentar'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('ulasans', function (Blueprint $table) { + if (Schema::hasColumn('ulasans', 'is_hidden')) { + $table->dropColumn('is_hidden'); + } + }); + } +}; diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..69b4adb --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index 81aa888..7285230 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -5,6 +5,7 @@ Dashboard Admin - PawMedic + + + diff --git a/resources/views/admin/disease-settings.blade.php b/resources/views/admin/disease-settings.blade.php index 6362a59..1b21ade 100644 --- a/resources/views/admin/disease-settings.blade.php +++ b/resources/views/admin/disease-settings.blade.php @@ -28,6 +28,8 @@ margin-bottom:18px; } .title{font-family:var(--ff-heading);color:var(--text-dark);font-size:30px;font-weight:800;margin:0;} +.title-wrap{display:flex;align-items:center;gap:10px} +.logo-icon{width:40px;height:40px;border-radius:10px;background:linear-gradient(135deg,#6fcf97,#4bb66f);display:flex;align-items:center;justify-content:center;color:#fff} .muted{color:var(--text-muted);margin:6px 0 0;} .back{ text-decoration:none;padding:10px 14px;border-radius:10px;border:1px solid var(--primary); @@ -41,10 +43,16 @@ padding:10px 14px;border-radius:10px;background:var(--primary-light);color:var(--text-dark); border:1px solid rgba(111,207,151,.3);margin-bottom:14px;font-weight:600; } +.search-wrap{margin-bottom:12px} +.search-input{ + width:100%;max-width:320px;padding:10px 12px;border:1px solid #cbd5e1;border-radius:10px;font:inherit +} .table-wrap{max-height:70vh;overflow:auto;border:1px solid #e2e8f0;border-radius:12px;} table{width:100%;border-collapse:collapse;background:#fff;} th,td{padding:12px 10px;border-bottom:1px solid #e2e8f0;vertical-align:top;} th{background:var(--primary-light);text-align:left;color:var(--text-dark);} +.disease-name{font-weight:700;color:#14532d} +.count{font-size:12px;color:#64748b;margin-top:4px} textarea{ width:100%;min-height:90px;resize:vertical;padding:10px;border:1px solid #cbd5e1;border-radius:10px; font-family:var(--ff-body);font-size:14px;line-height:1.5; @@ -60,7 +68,17 @@
-

Pengaturan Penjelasan Penyakit

+
+ +

Pengaturan Penjelasan Penyakit

+

Atur deskripsi penyakit yang tampil di halaman hasil diagnosis.

โ† Kembali ke Dashboard @@ -73,24 +91,32 @@
@csrf +
+ +
+ @forelse($diseases as $disease) - - + + + @empty - + @endforelse
No Nama Penyakit Penjelasan
{{ $disease }}
+
{{ $disease }}
+
Kunci: {{ \Illuminate\Support\Str::slug($disease, '-') }}
+
Belum ada data penyakit.
Belum ada data penyakit.
@@ -101,6 +127,29 @@
+ diff --git a/resources/views/admin/faq-settings.blade.php b/resources/views/admin/faq-settings.blade.php index 5374a0b..f0086da 100644 --- a/resources/views/admin/faq-settings.blade.php +++ b/resources/views/admin/faq-settings.blade.php @@ -5,26 +5,42 @@ Kelola FAQ - PawMedic Admin +
-

Kelola FAQ

+
+ +

Kelola FAQ

+
โ† Dashboard
@@ -34,18 +50,34 @@ @endif
@csrf -
- @forelse($faqs as $faq) -
- - -
- @empty -
- - -
- @endforelse +
+ + + + + + + + + + + @forelse($faqs as $faq) + + + + + + + @empty + + + + + + + @endforelse + +
NoPertanyaanJawabanAksi
@@ -54,17 +86,73 @@
+ + diff --git a/resources/views/admin/forgot-password.blade.php b/resources/views/admin/forgot-password.blade.php new file mode 100644 index 0000000..6981633 --- /dev/null +++ b/resources/views/admin/forgot-password.blade.php @@ -0,0 +1,47 @@ + + + + + +Lupa Password Admin - PawMedic + + + +
+
+
+

Lupa Password Admin

+

Masukkan email aktif untuk menerima OTP, lalu verifikasi kode.

+ + @if(session('success')) +
{{ session('success') }}
+ @endif + @if(session('error')) +
{{ session('error') }}
+ @endif + @if($errors->any()) +
{{ $errors->first() }}
+ @endif + +
+ @csrf + + + +
+ +
+ @csrf + + + + + +
+ + Kembali ke login +
+
+
+ + diff --git a/resources/views/admin/login.blade.php b/resources/views/admin/login.blade.php index 6ea902e..f8dd8f9 100644 --- a/resources/views/admin/login.blade.php +++ b/resources/views/admin/login.blade.php @@ -5,6 +5,9 @@ Login Admin - PawMedic + + + + + +
+
+
+
+ +

Calon Data Training Penyakit

+
+
Tahap awal hanya isi nama penyakit dan kategori.
+
+ โ† Dashboard +
+ +
+ @if(session('success')) +
{{ session('success') }}
+ @endif + @if(session('error')) +
{{ session('error') }}
+ @endif +
+ Isi data penyakit dulu, lalu klik Atur Gejala. Status Belum Siap jika sample terisi belum 10, dan Siap jika sudah 10/10. +
+ +
+ @csrf +
+ + + + + + + + + + + + + @forelse($items as $it) + + + + + + + + + + @empty + + + + + + + + + + @endforelse + +
NoPenyakitKategoriSample TerisiStatusAksi
{{ (int)($it['samples'] ?? 0) }}/10 + @if(($it['status'] ?? '') === 'ready-train') + Siap + @else + Belum Siap + @endif + + Atur Gejala + +
0/10Belum SiapSimpan dulu
+
+
+ + Export Data Kandidat + +
+
+
+
+ +
+ Minimal harus ada 1 baris data. +
+ + + + diff --git a/resources/views/admin/training-symptoms-settings.blade.php b/resources/views/admin/training-symptoms-settings.blade.php new file mode 100644 index 0000000..ce83d15 --- /dev/null +++ b/resources/views/admin/training-symptoms-settings.blade.php @@ -0,0 +1,77 @@ + + + + + +Atur Gejala Sample - PawMedic Admin + + + + +
+
+
+

Atur Gejala: {{ $item['disease'] ?? '-' }}

+
Kategori: {{ $item['category'] ?? '-' }} | Isi 10 baris sample
+
+ โ† Kembali +
+ +
+
Centang gejala yang bernilai Ya. Tidak dicentang berarti Tidak.
+
+ @csrf +
+ + + + + @foreach($featureCols as $col) + + @endforeach + + + + @for($r = 0; $r < 10; $r++) + + + @foreach($featureCols as $col) + @php + $isYes = strcasecmp((string)($item['symptom_samples'][$r][$col] ?? 'Tidak'), 'Ya') === 0; + @endphp + + @endforeach + + @endfor + +
Sample{{ $col }}
{{ $r + 1 }} + +
+
+
+ Kembali + +
+
+
+
+ + diff --git a/resources/views/biodata.blade.php b/resources/views/biodata.blade.php index a3c61a8..2b879b6 100644 --- a/resources/views/biodata.blade.php +++ b/resources/views/biodata.blade.php @@ -5,6 +5,7 @@ Input Biodata - PawMedic + @@ -309,7 +320,14 @@
-
๐Ÿพ
+
PawMedic
@php diff --git a/resources/views/components/toast.blade.php b/resources/views/components/toast.blade.php index 326c6b2..b8ff352 100644 --- a/resources/views/components/toast.blade.php +++ b/resources/views/components/toast.blade.php @@ -135,10 +135,10 @@ function showToast(message, type = 'info', title = null) { toast.className = `toast ${type}`; const icons = { - success: 'โœ…', - error: 'โŒ', - warning: 'โš ๏ธ', - info: 'โ„น๏ธ' + success: 'โœ“', + error: 'โœ•', + warning: '!', + info: 'i' }; toast.innerHTML = ` diff --git a/resources/views/faq.blade.php b/resources/views/faq.blade.php index ada86d5..65dd099 100644 --- a/resources/views/faq.blade.php +++ b/resources/views/faq.blade.php @@ -5,6 +5,7 @@ FAQ - PawMedic + @@ -218,7 +229,14 @@
-
๐Ÿพ
+
PawMedic

Pertanyaan Umum

diff --git a/resources/views/gejala.blade.php b/resources/views/gejala.blade.php index 84adb6e..c8b2d90 100644 --- a/resources/views/gejala.blade.php +++ b/resources/views/gejala.blade.php @@ -5,6 +5,8 @@ Pilih Gejala - PawMedic + + @@ -757,7 +769,14 @@
-
๐Ÿพ
+
PawMedic
@php @@ -780,19 +799,19 @@
-
+ @csrf
-
๐Ÿ’ก
+

Pilih minimal 4 dan maksimal 7 gejala yang terjadi pada kucing anda

- ๐Ÿ” Gejala yang Ditemukan + Gejala yang Ditemukan 0 dipilih
@@ -801,7 +820,7 @@ @@ -874,19 +893,29 @@ function updateSelectedCount() { // Form submission form.addEventListener('submit', function(e) { - const checked = document.querySelectorAll('.gejala-checkbox:checked').length; + e.preventDefault(); - if (checked < 4) { - e.preventDefault(); + const checked = document.querySelectorAll('.gejala-checkbox:checked'); + + if (checked.length < 4) { alert("Minimal pilih 4 gejala!"); return; } - if (checked > 7) { - e.preventDefault(); + if (checked.length > 7) { alert("Maksimal hanya 7 gejala!"); return; } + + // ambil gejala + let gejala = []; + checked.forEach(c => gejala.push(c.value)); + + // simpan ke localStorage (AMAN ๐Ÿ”ฅ) + localStorage.setItem('gejala', JSON.stringify(gejala)); + + // pindah ke loading + window.location.href = "/loading-diagnosis"; }); // Search functionality diff --git a/resources/views/hasil-diagnosis.blade.php b/resources/views/hasil-diagnosis.blade.php index 332bd87..f50a53a 100644 --- a/resources/views/hasil-diagnosis.blade.php +++ b/resources/views/hasil-diagnosis.blade.php @@ -5,6 +5,8 @@ Hasil Diagnosis - PawMedic + + @php $diagnosis = session('diagnosis')?? []; @endphp @@ -178,6 +180,7 @@ padding:24px; border-radius:16px; margin-bottom:32px; + overflow-wrap:anywhere; } .diagnosis-label{ @@ -212,6 +215,7 @@ color:#0f5132; font-size:14px; line-height:1.7; + overflow-wrap:anywhere; } /* ===== GEJALA LIST ===== */ @@ -448,6 +452,27 @@ width:100%; } } + +@media (max-width:576px) and (orientation:portrait){ + body{padding:10px;} + .container{padding:10px 0;} + .header h1{font-size:1.35rem;} + .logo-icon{width:38px;height:38px;} + .result-card{padding:16px 12px;border-radius:16px;} + .result-title{font-size:1.1rem;} + .result-subtitle,.diagnosis-description,.diagnosis-list li{font-size:13px;} + .diagnosis-result,.recommendation,.warning-box{padding:14px 12px;border-radius:12px;margin-bottom:16px;} + .diagnosis-name{font-size:20px;line-height:1.35;} + .diagnosis-category{font-size:14px;} + .disease-explanation{font-size:13px;line-height:1.65;padding:10px 12px;} + .section-title{font-size:18px;margin-bottom:12px;} + .gejala-list{gap:8px;} + .gejala-badge{font-size:12px;padding:8px 10px;border-radius:12px;} + .gejala-badge::before{width:16px;height:16px;font-size:10px;} + .action-buttons{gap:10px;margin-top:20px;} + .btn{font-size:14px;padding:10px 12px;min-width:unset;} + .history-table th,.history-table td{font-size:12px;padding:8px 6px;} +} @@ -455,7 +480,14 @@
-
๐Ÿพ
+
PawMedic
@php @@ -472,7 +504,7 @@
-
๐Ÿฉบ
+
Diagnosis Selesai
Berikut adalah hasil analisis gejala kucing Anda
@@ -498,7 +530,7 @@
- ๐Ÿ” Gejala yang Dipilih + Gejala yang Dipilih
@foreach(session('gejala', []) as $g) @@ -510,7 +542,7 @@
- ๐Ÿ’ก Rekomendasi Perawatan + Rekomendasi Perawatan
    @foreach($diagnosis['pertolongan'] ?? [] as $item) @@ -521,7 +553,7 @@
    - ๐Ÿ›ก๏ธ Pencegahan + Pencegahan
      @foreach($diagnosis['pencegahan'] ?? [] as $item) @@ -533,7 +565,7 @@
      - โš ๏ธ Peringatan Penting + Peringatan Penting
      Hasil diagnosis ini hanya sebagai panduan awal. Untuk diagnosis yang akurat dan penanganan yang tepat, @@ -544,16 +576,13 @@
      @@ -562,7 +591,7 @@ @if(isset($diagnosisHistory) && $diagnosisHistory->count() > 0)
      - ๐Ÿ•˜ Riwayat Diagnosis (Nomor yang sama) + Riwayat Diagnosis (Nomor yang sama)
      @@ -575,7 +604,11 @@ @foreach($diagnosisHistory as $row) - + @@ -588,76 +621,37 @@ @include('components.scroll-top') @@ -628,23 +714,34 @@
      -
      {{ \Carbon\Carbon::parse($row->created_at)->format('d M Y H:i') }} + + {{ \Carbon\Carbon::parse($row->created_at)->format('d M Y H:i') }} + + {{ $row->nama_kucing ?? '-' }} {{ $row->hasil_diagnosis ?? '-' }}