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 '| No | Penyakit | Golongan | Sample Ke | ';
+ foreach ($featureCols as $col) {
+ echo '' . e($col) . ' | ';
+ }
+ echo '
';
+
+ $no = 1;
+ foreach ($items as $item) {
+ $sampleRows = $this->normalizeSymptomSamples($item['symptom_samples'] ?? [], $featureCols);
+ foreach ($sampleRows as $sampleIndex => $flags) {
+ echo '';
+ echo '| ' . $no++ . ' | ';
+ echo '' . e((string)($item['disease'] ?? '')) . ' | ';
+ echo '' . e((string)($item['category'] ?? '')) . ' | ';
+ echo '' . e((string)($sampleIndex + 1)) . ' | ';
+ foreach ($featureCols as $col) {
+ $v = (string)($flags[$col] ?? 'Tidak');
+ echo '' . e(strcasecmp($v, 'Ya') === 0 ? 'Ya' : 'Tidak') . ' | ';
+ }
+ echo '
';
+ }
+ }
+ echo '
';
+ }, $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
+
+
+