update besar besaran

This commit is contained in:
WahyuTegarP 2026-05-08 17:47:40 +07:00
parent 6ad6bb79f4
commit 920e6caf3d
29 changed files with 2797 additions and 395 deletions

View File

@ -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 '<html><head><meta charset="UTF-8"></head><body>';
echo '<table border="1" cellpadding="6" cellspacing="0">';
echo '<tr style="background:#e8f7ef;font-weight:bold;">';
echo '<th>No</th><th>Penyakit</th><th>Golongan</th><th>Sample Ke</th>';
foreach ($featureCols as $col) {
echo '<th>' . e($col) . '</th>';
}
echo '</tr>';
$no = 1;
foreach ($items as $item) {
$sampleRows = $this->normalizeSymptomSamples($item['symptom_samples'] ?? [], $featureCols);
foreach ($sampleRows as $sampleIndex => $flags) {
echo '<tr>';
echo '<td>' . $no++ . '</td>';
echo '<td>' . e((string)($item['disease'] ?? '')) . '</td>';
echo '<td>' . e((string)($item['category'] ?? '')) . '</td>';
echo '<td>' . e((string)($sampleIndex + 1)) . '</td>';
foreach ($featureCols as $col) {
$v = (string)($flags[$col] ?? 'Tidak');
echo '<td>' . e(strcasecmp($v, 'Ya') === 0 ? 'Ya' : 'Tidak') . '</td>';
}
echo '</tr>';
}
}
echo '</table></body></html>';
}, $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');

View File

@ -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([

View File

@ -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');
}

View File

@ -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.'
);
}
}

View File

@ -16,6 +16,9 @@ class Biodata extends Model
'berat_badan',
'ras_kucing',
'alamat',
'no_telepon'
'no_telepon',
'hasil_diagnosis',
'jenis',
'gejala_dipilih',
];
}

View File

@ -12,6 +12,7 @@ class Ulasan extends Model
'nama_kucing',
'hasil_diagnosis',
'rating',
'komentar'
'komentar',
'is_hidden',
];
}

View File

@ -7,6 +7,7 @@
"license": "MIT",
"require": {
"php": "^8.2",
"barryvdh/laravel-dompdf": "^3.1",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1"
},

530
composer.lock generated
View File

@ -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"
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('biodata', function (Blueprint $table) {
if (!Schema::hasColumn('biodata', 'gejala_dipilih')) {
$table->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');
}
});
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('ulasans', function (Blueprint $table) {
if (!Schema::hasColumn('ulasans', 'is_hidden')) {
$table->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');
}
});
}
};

15
public/favicon.svg Normal file
View File

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
<stop offset="0%" stop-color="#6fcf97"/>
<stop offset="100%" stop-color="#4bb66f"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="14" fill="url(#g)"/>
<g fill="#ffffff">
<circle cx="21" cy="23" r="6"/>
<circle cx="32" cy="18" r="6"/>
<circle cx="43" cy="23" r="6"/>
<path d="M32 29c-8.5 0-14 6.1-14 12.2 0 5.6 4.6 9.8 10 9.8 3.3 0 4.6-1.8 6-1.8s2.7 1.8 6 1.8c5.4 0 10-4.2 10-9.8C50 35.1 40.5 29 32 29z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 596 B

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Admin - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<style>
:root{
@ -73,6 +74,7 @@
align-items:center;
justify-content:center;
font-size:22px;
color:#fff;
}
.logo-text{
@ -158,6 +160,10 @@
font-size:13px;
}
.admin-shortcut i{
margin-right:6px;
}
/* ===== STATS GRID ===== */
.stats-grid {
display: grid;
@ -371,7 +377,14 @@
<div class="admin-header">
<div class="header-content">
<div class="logo-section">
<div class="logo-icon">🐾</div>
<div class="logo-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</div>
<div class="logo-text">PawMedic Admin</div>
</div>
<div class="user-menu">
@ -396,10 +409,12 @@
<div class="container">
<h1 class="page-title">Dashboard Admin</h1>
<div class="admin-shortcuts">
<a href="#" class="admin-shortcut" onclick="toggleDiagnosis(); return false;">📋 Data Diagnosis</a>
<a href="#" class="admin-shortcut" onclick="toggleUsers(); return false;">👥 Data Pengguna</a>
<a href="#" class="admin-shortcut" onclick="toggleChart(); return false;">📊 Statistik</a>
<a href="{{ route('admin.disease.settings') }}" class="admin-shortcut">🧾 Pengaturan Penyakit</a>
<a href="#" class="admin-shortcut" onclick="toggleDiagnosis(); return false;"><i class="bi bi-card-list"></i>Data Diagnosis</a>
<a href="#" class="admin-shortcut" onclick="toggleUsers(); return false;"><i class="bi bi-people"></i>Data Pengguna</a>
<a href="#" class="admin-shortcut" onclick="toggleChart(); return false;"><i class="bi bi-bar-chart"></i>Statistik</a>
<a href="{{ route('admin.disease.settings') }}" class="admin-shortcut"><i class="bi bi-journal-medical"></i>Penjelasan Penyakit</a>
<a href="{{ route('admin.faq.settings') }}" class="admin-shortcut"><i class="bi bi-wrench-adjustable-circle-fill"></i>Kelola FAQ</a>
<a href="{{ route('admin.training.settings') }}" class="admin-shortcut"><i class="bi bi-database-add"></i>Calon Data Training</a>
</div>
<!-- Statistics -->
@ -411,7 +426,7 @@
<div class="stat-value">{{ $stats['total_diagnosis'] }}</div>
<div class="stat-label">Total Diagnosis</div>
</div>
<div class="stat-icon">📊</div>
<div class="stat-icon"><i class="bi bi-bar-chart-fill"></i></div>
</div>
<div class="stat-change">+{{ $stats['today_diagnosis'] }} hari ini</div>
<a href="#" onclick="toggleDiagnosis(); return false;"
@ -426,7 +441,7 @@
<div class="stat-value">{{ $stats['today_diagnosis'] }}</div>
<div class="stat-label">Diagnosis Hari Ini</div>
</div>
<div class="stat-icon">📈</div>
<div class="stat-icon"><i class="bi bi-graph-up-arrow"></i></div>
</div>
<div class="stat-change">Aktif hari ini</div>
<div class="stat-change">
@ -450,7 +465,7 @@
<div class="stat-value">{{ $stats['total_users'] }}</div>
<div class="stat-label">Total Pengguna</div>
</div>
<div class="stat-icon">👥</div>
<div class="stat-icon"><i class="bi bi-people-fill"></i></div>
</div>
<div class="stat-change">Pengguna aktif</div>
<a href="#" onclick="toggleUsers(); return false;"
@ -465,7 +480,7 @@
<div class="stat-value" style="font-size:24px;">{{ $stats['most_common_disease'] }}</div>
<div class="stat-label">Penyakit Paling Umum</div>
</div>
<div class="stat-icon">🩺</div>
<div class="stat-icon"><i class="bi bi-heart-pulse-fill"></i></div>
</div>
<div class="stat-change">Paling sering didiagnosis</div>
@ -493,17 +508,24 @@
</div>
<div class="data-section">
<div class="section-title">📈 Tren Diagnosis (7 Hari Terakhir)</div>
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;">
<div class="section-title">📈 Tren Diagnosis</div>
<select id="trendMode" class="form-control" style="max-width:260px;">
<option value="7d">7 Hari Terakhir</option>
<option value="month">Per Bulan (Tahun Ini)</option>
<option value="all">Seluruh Periode</option>
</select>
</div>
<canvas id="chartHarian"></canvas>
</div>
</div>
<div id="diagnosisBox" style="display:none; margin-top:20px;">
<div class="data-section">
<div class="section-title">📋 Data Diagnosis</div>
<div class="section-title"><i class="bi bi-card-list"></i> Data Diagnosis</div>
<div class="table-controls">
<input type="text" id="searchDiagnosis" class="form-control" placeholder="🔍 Cari data diagnosis...">
<input type="text" id="searchDiagnosis" class="form-control" placeholder="Cari data diagnosis...">
<select id="filterDiagnosis" class="form-control">
<option value="">Semua Penyakit</option>
@foreach($data->pluck('hasil_diagnosis')->unique() as $penyakit)
@ -528,6 +550,7 @@
<th>Umur Kucing</th>
<th>Jenis Kelamin</th>
<th>Penyakit</th>
<th>Gejala Dipilih</th>
<th>Tanggal</th>
</tr>
</thead>
@ -539,6 +562,25 @@
<td>{{ $item->umur_kucing ?? '-' }}</td>
<td>{{ $item->jenis_kelamin ?? '-' }}</td>
<td>{{ $item->hasil_diagnosis ?? '-' }}</td>
<td style="max-width:340px;text-align:center;">
@php
$sym = $item->gejala_dipilih ?? '';
$arr = json_decode((string)$sym, true);
if (!is_array($arr)) $arr = [];
$arr = array_values(array_filter(array_map('trim', $arr), fn($v) => $v !== ''));
$count = count($arr);
@endphp
@if($count === 0)
<span style="color:#64748b;">-</span>
@else
<a href="#"
class="symptoms-link"
data-symptoms='@json($arr, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)'
style="color:#2f855a;font-weight:700;text-decoration:none;">
Lihat gejala ({{ $count }})
</a>
@endif
</td>
<td>{{ \Carbon\Carbon::parse($item->created_at)->format('d M Y') }}</td>
</tr>
@endforeach
@ -550,9 +592,9 @@
<div id="userBox" style="display:none; margin-top:20px;">
<div class="data-section">
<div class="section-title">👥 Data Pengguna</div>
<div class="section-title"><i class="bi bi-people"></i> Data Pengguna</div>
<div class="table-controls">
<input type="text" id="searchUser" class="form-control" placeholder="🔍 Cari pengguna...">
<input type="text" id="searchUser" class="form-control" placeholder="Cari pengguna...">
<select id="sortUser" class="form-control">
<option value="latest">Terbaru</option>
<option value="oldest">Terlama</option>
@ -590,7 +632,7 @@
<!-- Recent Diagnosis -->
<div class="data-section">
<div class="section-title">
<span>📋</span>
<span><i class="bi bi-clock-history"></i></span>
<span>Diagnosis Terbaru</span>
</div>
<table class="table">
@ -618,24 +660,18 @@
<!-- Quick Actions -->
<div class="data-section">
<div class="section-title">
<span></span>
<span><i class="bi bi-lightning-charge-fill"></i></span>
<span>Aksi Cepat</span>
</div>
<div style="display:flex; gap:16px; flex-wrap:wrap;">
<a href="/" style="padding:12px 24px; background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); color:white; text-decoration:none; border-radius:12px; font-weight:600; transition:all 0.3s ease;">
🏠 Lihat Website
<i class="bi bi-house-door-fill"></i> Lihat Website
</a>
<a href="{{ route('ulasan') }}" style="padding:12px 24px; background:white; border:2px solid var(--primary); color:var(--primary-dark); text-decoration:none; border-radius:12px; font-weight:600; transition:all 0.3s ease;">
💬 Lihat Ulasan
<i class="bi bi-chat-dots-fill"></i> Lihat Ulasan
</a>
<a href="{{ route('faq') }}" style="padding:12px 24px; background:white; border:2px solid var(--primary); color:var(--primary-dark); text-decoration:none; border-radius:12px; font-weight:600; transition:all 0.3s ease;">
Lihat FAQ
</a>
<a href="{{ route('admin.faq.settings') }}" style="padding:12px 24px; background:white; border:2px solid var(--primary); color:var(--primary-dark); text-decoration:none; border-radius:12px; font-weight:600; transition:all 0.3s ease;">
🛠️ Kelola FAQ
</a>
<a href="{{ route('admin.disease.settings') }}" style="padding:12px 24px; background:white; border:2px solid var(--primary); color:var(--primary-dark); text-decoration:none; border-radius:12px; font-weight:600; transition:all 0.3s ease;">
🧾 Atur Penjelasan Penyakit
<i class="bi bi-question-circle-fill"></i> Lihat FAQ
</a>
</div>
</div>
@ -730,6 +766,24 @@ function toggleDiagnosis() {
<script>
let diseaseChart = null;
let ratingChart = null;
let trendChart = null;
const trendDatasets = {
'7d': {
labels: {!! json_encode($stats['trend_7_labels']) !!},
data: {!! json_encode($stats['trend_7_data']) !!},
label: 'Diagnosis (7 hari)',
},
'month': {
labels: {!! json_encode($stats['trend_month_labels']) !!},
data: {!! json_encode($stats['trend_month_data']) !!},
label: 'Diagnosis per bulan',
},
'all': {
labels: {!! json_encode($stats['trend_all_labels']) !!},
data: {!! json_encode($stats['trend_all_data']) !!},
label: 'Diagnosis seluruh periode',
}
};
function loadMainChart() {
if (diseaseChart) return;
@ -754,20 +808,36 @@ function loadMainChart() {
tooltip: { mode: 'index', intersect: false }
},
scales: {
y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: '#e5e7eb' } },
x: { grid: { display: false } }
// 🔥 TAMBAHKAN INI
x: {
ticks: {
display: false // ❗ ini yang menghilangkan nama penyakit
},
grid: {
display: false // opsional biar lebih bersih
}
},
y: {
beginAtZero: true,
ticks: { precision: 0 },
grid: { color: '#e5e7eb' }
}
}
}
});
}
new Chart(document.getElementById('chartHarian'), {
function renderTrendChart(mode = '7d') {
const source = trendDatasets[mode] || trendDatasets['7d'];
if (!trendChart) {
trendChart = new Chart(document.getElementById('chartHarian'), {
type: 'line',
data: {
labels: {!! json_encode($stats['daily_labels']) !!},
labels: source.labels,
datasets: [{
label: 'Jumlah Diagnosis',
data: {!! json_encode($stats['daily_data']) !!},
label: source.label,
data: source.data,
tension: 0.35,
fill: true,
backgroundColor: 'rgba(111, 207, 151, 0.2)',
@ -786,6 +856,14 @@ function loadMainChart() {
}
}
});
return;
}
trendChart.data.labels = source.labels;
trendChart.data.datasets[0].data = source.data;
trendChart.data.datasets[0].label = source.label;
trendChart.update();
}
function loadRatingChart() {
if (ratingChart) return;
@ -821,9 +899,69 @@ function loadRatingChart() {
document.getElementById('sortDiagnosis').addEventListener('change', applyDiagnosisFilters);
document.getElementById('searchUser').addEventListener('input', applyUserFilters);
document.getElementById('sortUser').addEventListener('change', applyUserFilters);
document.getElementById('trendMode').addEventListener('change', (e) => renderTrendChart(e.target.value));
applyDiagnosisFilters();
applyUserFilters();
renderTrendChart('7d');
</script>
<dialog id="symptomsDialog" style="border:none;border-radius:14px;max-width:520px;width:92%;padding:0;box-shadow:0 18px 50px rgba(17,77,58,.22);position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);margin:0;">
<div style="padding:16px 16px 10px;background:#fff;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;gap:12px;">
<div style="font-weight:800;color:#114d3a;">Gejala Dipilih</div>
<button id="closeSymptomsDialog" type="button" style="border:none;background:#f1f5f9;border-radius:10px;padding:8px 10px;cursor:pointer;font-weight:800;"></button>
</div>
<div style="padding:14px 16px 16px;background:#fff;">
<div id="symptomsDialogBody" style="display:flex;flex-wrap:wrap;gap:8px;"></div>
<div style="margin-top:14px;color:#64748b;font-size:12px;">Data ini tersimpan untuk evaluasi & peningkatan dataset.</div>
</div>
</dialog>
<style>
#symptomsDialog::backdrop{
background:rgba(15,23,42,.36);
}
</style>
<script>
const symptomsDialog = document.getElementById('symptomsDialog');
const symptomsDialogBody = document.getElementById('symptomsDialogBody');
const closeSymptomsDialog = document.getElementById('closeSymptomsDialog');
function openSymptomsDialog(list) {
if (!symptomsDialog || !symptomsDialogBody) return;
symptomsDialogBody.innerHTML = '';
(list || []).forEach((g) => {
const chip = document.createElement('span');
chip.textContent = g;
chip.style.cssText = 'font-size:12px;padding:6px 10px;border-radius:999px;background:#e8f7ef;border:1px solid #b7ebcf;color:#114d3a;font-weight:700;';
symptomsDialogBody.appendChild(chip);
});
if (typeof symptomsDialog.showModal === 'function') {
symptomsDialog.showModal();
}
}
document.querySelectorAll('.symptoms-link').forEach((a) => {
a.addEventListener('click', (e) => {
e.preventDefault();
let list = [];
try {
list = JSON.parse(a.getAttribute('data-symptoms') || '[]');
} catch (_) {
list = [];
}
openSymptomsDialog(list);
});
});
if (closeSymptomsDialog && symptomsDialog) {
closeSymptomsDialog.addEventListener('click', () => symptomsDialog.close());
symptomsDialog.addEventListener('click', (e) => {
const rect = symptomsDialog.getBoundingClientRect();
const inDialog = rect.top <= e.clientY && e.clientY <= rect.bottom && rect.left <= e.clientX && e.clientX <= rect.right;
if (!inDialog) symptomsDialog.close();
});
}
</script>
</body>
</html>

View File

@ -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 @@
<div class="container">
<div class="topbar">
<div>
<div class="title-wrap">
<div class="logo-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</div>
<h1 class="title">Pengaturan Penjelasan Penyakit</h1>
</div>
<p class="muted">Atur deskripsi penyakit yang tampil di halaman hasil diagnosis.</p>
</div>
<a href="{{ route('admin.dashboard') }}" class="back"> Kembali ke Dashboard</a>
@ -73,24 +91,32 @@
<form method="POST" action="{{ route('admin.disease.settings.save') }}">
@csrf
<div class="search-wrap">
<input type="text" id="searchDisease" class="search-input" placeholder="Cari nama penyakit...">
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:80px;">No</th>
<th style="width:32%;">Nama Penyakit</th>
<th>Penjelasan</th>
</tr>
</thead>
<tbody>
@forelse($diseases as $disease)
<tr>
<td><strong>{{ $disease }}</strong></td>
<tr class="disease-row">
<td class="row-no"></td>
<td>
<div class="disease-name">{{ $disease }}</div>
<div class="count">Kunci: {{ \Illuminate\Support\Str::slug($disease, '-') }}</div>
</td>
<td>
<textarea name="descriptions[{{ $disease }}]" placeholder="Tulis penjelasan singkat penyakit...">{{ $descriptions[$disease] ?? '' }}</textarea>
</td>
</tr>
@empty
<tr><td colspan="2">Belum ada data penyakit.</td></tr>
<tr><td colspan="3">Belum ada data penyakit.</td></tr>
@endforelse
</tbody>
</table>
@ -101,6 +127,29 @@
</form>
</div>
</div>
<script>
const diseaseSearch = document.getElementById('searchDisease');
if (diseaseSearch) {
diseaseSearch.addEventListener('input', () => {
const q = diseaseSearch.value.toLowerCase().trim();
document.querySelectorAll('.disease-row').forEach((row) => {
const name = row.querySelector('.disease-name')?.textContent?.toLowerCase() || '';
row.style.display = name.includes(q) ? '' : 'none';
});
renumberDiseaseRows();
});
}
function renumberDiseaseRows() {
const rows = document.querySelectorAll('.disease-row');
let i = 1;
rows.forEach((row) => {
if (row.style.display === 'none') return;
const no = row.querySelector('.row-no');
if (no) no.textContent = i++;
});
}
renumberDiseaseRows();
</script>
</body>
</html>

View File

@ -5,26 +5,42 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kelola FAQ - PawMedic Admin</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body{margin:0;font-family:'Inter',sans-serif;background:#f4faf7;color:#1f2937}
.container{max-width:1100px;margin:0 auto;padding:28px 16px}
.head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:18px}
.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;font-size:18px}
.title{font-family:'Poppins',sans-serif;font-size:30px;margin:0;color:#114d3a}
.card{background:#fff;border:1px solid #d1fae5;border-radius:16px;padding:16px;box-shadow:0 8px 20px rgba(17,77,58,.08)}
.row{display:grid;grid-template-columns:1fr 2fr;gap:10px;margin-bottom:10px}
table{width:100%;border-collapse:collapse}
th,td{padding:10px;border-bottom:1px solid #e2e8f0;vertical-align:top}
th{background:#e8f7ef;text-align:left}
input,textarea{width:100%;padding:10px;border:1px solid #cbd5e1;border-radius:10px;font:inherit}
textarea{min-height:92px;resize:vertical}
.btn{padding:10px 14px;border-radius:10px;border:1px solid #6fcf97;background:#6fcf97;color:#fff;font-weight:700;cursor:pointer}
.btn.secondary{background:#fff;color:#114d3a}
.btn.danger{background:#fff;border-color:#fca5a5;color:#b91c1c}
.actions{display:flex;justify-content:space-between;align-items:center;margin-top:12px;gap:10px}
.notice{background:#e8f7ef;border:1px solid #b7ebcf;padding:10px;border-radius:10px;color:#114d3a;margin-bottom:10px}
@media(max-width:768px){.row{grid-template-columns:1fr}.title{font-size:24px}}
@media(max-width:768px){.title{font-size:24px}}
</style>
</head>
<body>
<div class="container">
<div class="head">
<div class="title-wrap">
<div class="logo-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</div>
<h1 class="title">Kelola FAQ</h1>
</div>
<a class="btn secondary" href="{{ route('admin.dashboard') }}"> Dashboard</a>
</div>
@ -34,18 +50,34 @@
@endif
<form method="POST" action="{{ route('admin.faq.settings.save') }}">
@csrf
<div id="faqRows">
<div style="overflow:auto;border:1px solid #e2e8f0;border-radius:10px;">
<table>
<thead>
<tr>
<th style="min-width:60px;">No</th>
<th style="min-width:260px;">Pertanyaan</th>
<th style="min-width:420px;">Jawaban</th>
<th style="min-width:120px;">Aksi</th>
</tr>
</thead>
<tbody id="faqRows">
@forelse($faqs as $faq)
<div class="row">
<input type="text" name="questions[]" value="{{ $faq['question'] }}" placeholder="Pertanyaan">
<textarea name="answers[]" placeholder="Jawaban">{{ $faq['answer'] }}</textarea>
</div>
<tr class="faq-row">
<td class="faq-no"></td>
<td><input type="text" name="questions[]" value="{{ $faq['question'] }}" placeholder="Pertanyaan"></td>
<td><textarea name="answers[]" placeholder="Jawaban">{{ $faq['answer'] }}</textarea></td>
<td><button type="button" class="btn danger" onclick="askDelete(this)">Hapus</button></td>
</tr>
@empty
<div class="row">
<input type="text" name="questions[]" placeholder="Pertanyaan">
<textarea name="answers[]" placeholder="Jawaban"></textarea>
</div>
<tr class="faq-row">
<td class="faq-no"></td>
<td><input type="text" name="questions[]" placeholder="Pertanyaan"></td>
<td><textarea name="answers[]" placeholder="Jawaban"></textarea></td>
<td><button type="button" class="btn danger" onclick="askDelete(this)">Hapus</button></td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="actions">
<button type="button" class="btn secondary" onclick="addFaqRow()">+ Tambah FAQ</button>
@ -54,17 +86,73 @@
</form>
</div>
</div>
<div class="modal fade" id="deleteFaqModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Konfirmasi Hapus</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">Apakah yakin untuk dihapus?</div>
<div class="modal-footer">
<button type="button" class="btn secondary" data-bs-dismiss="modal">Batal</button>
<button type="button" class="btn danger" id="confirmDeleteBtn">Ya, Hapus</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let pendingDeleteRow = null;
function addFaqRow() {
const wrap = document.getElementById('faqRows');
const row = document.createElement('div');
row.className = 'row';
const row = document.createElement('tr');
row.className = 'faq-row';
row.innerHTML = `
<input type="text" name="questions[]" placeholder="Pertanyaan">
<textarea name="answers[]" placeholder="Jawaban"></textarea>
<td class="faq-no"></td>
<td><input type="text" name="questions[]" placeholder="Pertanyaan"></td>
<td><textarea name="answers[]" placeholder="Jawaban"></textarea></td>
<td><button type="button" class="btn danger" onclick="askDelete(this)">Hapus</button></td>
`;
wrap.appendChild(row);
renumberRows();
}
function askDelete(btn) {
const row = btn.closest('.faq-row');
if (!row) return;
const wrap = document.getElementById('faqRows');
if (wrap.querySelectorAll('.faq-row').length <= 1) {
alert('Minimal harus ada 1 baris FAQ.');
return;
}
pendingDeleteRow = row;
const modalEl = document.getElementById('deleteFaqModal');
if (window.bootstrap && modalEl) {
const modal = new bootstrap.Modal(modalEl);
modal.show();
} else if (confirm('Apakah yakin untuk dihapus?')) {
row.remove();
renumberRows();
}
}
document.getElementById('confirmDeleteBtn')?.addEventListener('click', () => {
if (pendingDeleteRow) {
pendingDeleteRow.remove();
renumberRows();
pendingDeleteRow = null;
}
const modalEl = document.getElementById('deleteFaqModal');
if (window.bootstrap && modalEl) {
bootstrap.Modal.getInstance(modalEl)?.hide();
}
});
function renumberRows() {
document.querySelectorAll('#faqRows .faq-row').forEach((row, idx) => {
const cell = row.querySelector('.faq-no');
if (cell) cell.textContent = idx + 1;
});
}
renumberRows();
</script>
</body>
</html>

View File

@ -0,0 +1,47 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lupa Password Admin - PawMedic</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-4" style="max-width:560px;">
<div class="card shadow-sm border-0 rounded-4">
<div class="card-body p-4">
<h3 class="mb-3">Lupa Password Admin</h3>
<p class="text-muted mb-3">Masukkan email aktif untuk menerima OTP, lalu verifikasi kode.</p>
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
@if($errors->any())
<div class="alert alert-danger">{{ $errors->first() }}</div>
@endif
<form method="POST" action="{{ route('admin.forgot.sendOtp') }}" class="mb-4">
@csrf
<label class="form-label">Email aktif (penerima OTP)</label>
<input type="email" class="form-control mb-3" name="otp_email" value="{{ old('otp_email', session('otp_email')) }}" placeholder="contoh@email.com" required>
<button type="submit" class="btn btn-success">Kirim kode sekarang</button>
</form>
<form method="POST" action="{{ route('admin.forgot.verifyOtp') }}">
@csrf
<label class="form-label">Email aktif (yang tadi dipakai)</label>
<input type="email" class="form-control mb-3" name="otp_email" value="{{ old('otp_email', session('otp_email')) }}" required>
<label class="form-label">Masukkan kode OTP</label>
<input type="text" class="form-control mb-3" name="otp" maxlength="6" placeholder="6 digit OTP" required>
<button type="submit" class="btn btn-primary">Verifikasi Kode</button>
</form>
<a href="{{ route('admin.login') }}" class="btn btn-link ps-0 mt-2">Kembali ke login</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Admin - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}">
<style>
:root{
@ -150,6 +153,44 @@
transition:all 0.3s ease;
background:#fafafa;
}
/* Hilangkan ikon "reveal password" bawaan browser (Windows/Edge) */
input::-ms-reveal,
input::-ms-clear{
display:none;
}
.password-wrap{
position:relative;
}
.password-wrap input{
padding-right:48px;
}
.toggle-pass{
position:absolute;
right:10px;
top:50%;
transform:translateY(-50%);
width:38px;
height:38px;
border-radius:10px;
border:1px solid rgba(100,116,139,.25);
background:#fff;
display:flex;
align-items:center;
justify-content:center;
cursor:pointer;
color:#0f3f33;
transition:all .2s ease;
}
.toggle-pass:hover{
background:var(--primary-light);
border-color:rgba(111,207,151,.55);
}
.toggle-pass svg{
width:18px;
height:18px;
display:block;
fill:currentColor;
}
.form-group input:focus{
outline:none;
@ -277,7 +318,14 @@
<div class="login-card">
<div class="logo-section">
<div class="logo">
<div class="logo-icon">🐾</div>
<div class="logo-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</div>
<div class="logo-text">PawMedic</div>
</div>
<h1 class="login-title">Admin Login</h1>
@ -286,10 +334,22 @@
@if($errors->any())
<div class="error-message">
<span>⚠️</span>
<span><i class="bi bi-exclamation-triangle-fill"></i></span>
<span>{{ $errors->first() }}</span>
</div>
@endif
@if(session('success'))
<div class="error-message" style="background:#ecfdf3;border-left-color:#22c55e;color:#166534;">
<span><i class="bi bi-check-circle-fill"></i></span>
<span>{{ session('success') }}</span>
</div>
@endif
@if(session('error'))
<div class="error-message">
<span><i class="bi bi-exclamation-triangle-fill"></i></span>
<span>{{ session('error') }}</span>
</div>
@endif
<form method="POST" action="{{ route('admin.authenticate') }}">
@csrf
@ -301,7 +361,7 @@
id="email"
name="email"
value="{{ old('email') }}"
placeholder="admin@pawmedic.app"
placeholder="Masukkan email aplikasi"
required
autofocus
>
@ -309,6 +369,7 @@
<div class="form-group">
<label for="password">Password</label>
<div class="password-wrap">
<input
type="password"
id="password"
@ -316,6 +377,12 @@
placeholder="Masukkan password"
required
>
<button type="button" class="toggle-pass" id="togglePassword" aria-label="Tampilkan password" aria-pressed="false">
<svg id="eyeIcon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 5c5.5 0 9.6 4.2 11 6.7a.6.6 0 0 1 0 .6C21.6 14.8 17.5 19 12 19S2.4 14.8 1 12.3a.6.6 0 0 1 0-.6C2.4 9.2 6.5 5 12 5zm0 2c-3.8 0-7 2.7-8.8 5 1.8 2.3 5 5 8.8 5s7-2.7 8.8-5c-1.8-2.3-5-5-8.8-5zm0 2.3A2.7 2.7 0 1 1 12 14.7a2.7 2.7 0 0 1 0-5.4z"/>
</svg>
</button>
</div>
</div>
<div class="remember-forgot">
@ -323,7 +390,7 @@
<input type="checkbox" name="remember">
<span>Ingat saya</span>
</label>
<a href="#" class="forgot-link">Lupa password?</a>
<a href="{{ route('admin.forgot.password') }}" class="forgot-link">Lupa password?</a>
</div>
<button type="submit" class="btn btn-primary">
@ -337,5 +404,33 @@
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const passInput = document.getElementById('password');
const toggleBtn = document.getElementById('togglePassword');
const eyeIcon = document.getElementById('eyeIcon');
function setEye(open) {
if (!eyeIcon) return;
eyeIcon.innerHTML = open
? '<path d="M12 5c5.5 0 9.6 4.2 11 6.7a.6.6 0 0 1 0 .6C21.6 14.8 17.5 19 12 19S2.4 14.8 1 12.3a.6.6 0 0 1 0-.6C2.4 9.2 6.5 5 12 5zm0 2c-3.8 0-7 2.7-8.8 5 1.8 2.3 5 5 8.8 5s7-2.7 8.8-5c-1.8-2.3-5-5-8.8-5zm0 2.3A2.7 2.7 0 1 1 12 14.7a2.7 2.7 0 0 1 0-5.4z"/>'
: '<path d="M2 12c1.6-2.8 5.8-7 10-7 2.1 0 4.1.8 5.9 2l1.6-1.6 1.4 1.4-18 18-1.4-1.4 2.2-2.2C3 19 1.3 14.9 1 13a1 1 0 0 1 .1-.6l.9-1.4zm10-5c-3 0-5.7 2-7.6 4.5.6.8 1.4 1.7 2.3 2.5l1.8-1.8A3.7 3.7 0 0 1 12 8.3c.5 0 1 .1 1.4.3l1.6-1.6A8.3 8.3 0 0 0 12 7zm0 10c3 0 5.7-2 7.6-4.5a15 15 0 0 0-1.9-2.1l-2 2a3.7 3.7 0 0 1-4.9 4.9l-1.7 1.7c.9.5 1.9.8 2.9.8z"/>';
}
if (toggleBtn && passInput) {
let shown = false;
setEye(false);
toggleBtn.addEventListener('click', () => {
shown = !shown;
passInput.type = shown ? 'text' : 'password';
toggleBtn.setAttribute('aria-pressed', shown ? 'true' : 'false');
toggleBtn.setAttribute('aria-label', shown ? 'Sembunyikan password' : 'Tampilkan password');
setEye(shown);
passInput.focus({ preventScroll: true });
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,37 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ganti Password Admin - PawMedic</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-4" style="max-width:560px;">
<div class="card shadow-sm border-0 rounded-4">
<div class="card-body p-4">
<h3 class="mb-3">Ganti Password Baru</h3>
<p class="text-muted mb-3">OTP valid. Silakan set password admin baru.</p>
@if(session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
@if($errors->any())
<div class="alert alert-danger">{{ $errors->first() }}</div>
@endif
<form method="POST" action="{{ route('admin.forgot.reset.submit') }}">
@csrf
<label class="form-label">Password baru</label>
<input type="password" class="form-control mb-3" name="new_password" minlength="6" required>
<label class="form-label">Konfirmasi password baru</label>
<input type="password" class="form-control mb-3" name="new_password_confirmation" minlength="6" required>
<button type="submit" class="btn btn-success">Simpan Password Baru</button>
</form>
<a href="{{ route('admin.login') }}" class="btn btn-link ps-0 mt-2">Kembali ke login</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,195 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calon Data Training - PawMedic Admin</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body{margin:0;font-family:'Inter',sans-serif;background:#f4faf7;color:#1f2937}
.container{max-width:1180px;margin:0 auto;padding:28px 16px}
.head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:18px}
.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;font-size:18px}
.title{font-family:'Poppins',sans-serif;font-size:30px;margin:0;color:#114d3a}
.subtitle{color:#64748b;margin-top:6px}
.card{background:#fff;border:1px solid #d1fae5;border-radius:16px;padding:16px;box-shadow:0 8px 20px rgba(17,77,58,.08)}
.notice{background:#e8f7ef;border:1px solid #b7ebcf;padding:10px;border-radius:10px;color:#114d3a;margin-bottom:10px}
.btn{padding:10px 14px;border-radius:10px;border:1px solid #6fcf97;background:#6fcf97;color:#fff;font-weight:700;cursor:pointer;text-decoration:none}
.btn.secondary{background:#fff;color:#114d3a}
.btn.danger{background:#fff;border-color:#fca5a5;color:#b91c1c}
.table-wrap{overflow:auto;border:1px solid #e2e8f0;border-radius:10px}
table{width:100%;border-collapse:collapse}
th,td{padding:10px;border-bottom:1px solid #e2e8f0;vertical-align:middle}
th{background:#e8f7ef;text-align:left}
input{width:100%;padding:8px;border:1px solid #cbd5e1;border-radius:8px;font:inherit}
.actions{display:flex;justify-content:space-between;align-items:center;margin-top:12px;gap:10px;flex-wrap:wrap}
.tag{display:inline-block;padding:4px 8px;border-radius:999px;font-size:12px;font-weight:700}
.tag.ready{background:#dcfce7;color:#166534}
.tag.need{background:#fee2e2;color:#991b1b}
.row-no{width:54px;text-align:center}
</style>
</head>
<body>
<div class="container">
<div class="head">
<div>
<div class="title-wrap">
<div class="logo-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</div>
<h1 class="title">Calon Data Training Penyakit</h1>
</div>
<div class="subtitle">Tahap awal hanya isi nama penyakit dan kategori.</div>
</div>
<a class="btn secondary" href="{{ route('admin.dashboard') }}"> Dashboard</a>
</div>
<div class="card">
@if(session('success'))
<div class="notice">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="notice" style="background:#fee2e2;border-color:#fca5a5;color:#991b1b">{{ session('error') }}</div>
@endif
<div class="notice">
Isi data penyakit dulu, lalu klik <strong>Atur Gejala</strong>. Status <strong>Belum Siap</strong> jika sample terisi belum 10, dan <strong>Siap</strong> jika sudah 10/10.
</div>
<form method="POST" action="{{ route('admin.training.settings.save') }}">
@csrf
<div class="table-wrap">
<table id="trainingTable">
<thead>
<tr>
<th class="row-no">No</th>
<th style="min-width:220px;">Penyakit</th>
<th style="min-width:180px;">Kategori</th>
<th style="min-width:130px;">Sample Terisi</th>
<th style="min-width:120px;">Status</th>
<th style="min-width:240px;">Aksi</th>
</tr>
</thead>
<tbody id="rows">
@forelse($items as $it)
<tr class="training-row">
<td class="row-no row-counter"></td>
<td style="display:none;"><input type="hidden" name="id[]" value="{{ $it['id'] ?? '' }}"></td>
<td><input type="text" name="disease[]" value="{{ $it['disease'] ?? '' }}"></td>
<td><input type="text" name="category[]" value="{{ $it['category'] ?? '' }}"></td>
<td>{{ (int)($it['samples'] ?? 0) }}/10</td>
<td>
@if(($it['status'] ?? '') === 'ready-train')
<span class="tag ready">Siap</span>
@else
<span class="tag need">Belum Siap</span>
@endif
</td>
<td style="display:flex;gap:8px;flex-wrap:wrap;">
<a class="btn secondary" href="{{ route('admin.training.symptoms.edit', $it['id']) }}">Atur Gejala</a>
<button type="button" class="btn danger btn-delete-row" onclick="askDeleteTrainingRow(this)">Hapus</button>
</td>
</tr>
@empty
<tr class="training-row">
<td class="row-no row-counter"></td>
<td style="display:none;"><input type="hidden" name="id[]" value=""></td>
<td><input type="text" name="disease[]" placeholder="Contoh: Feline Diabetes"></td>
<td><input type="text" name="category[]" placeholder="Contoh: Metabolik"></td>
<td>0/10</td>
<td><span class="tag need">Belum Siap</span></td>
<td><span class="btn secondary" style="opacity:.6;pointer-events:none;">Simpan dulu</span></td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="actions">
<button type="button" class="btn secondary" onclick="addRow()">+ Tambah Baris</button>
<a href="{{ route('admin.training.export') }}" class="btn secondary">Export Data Kandidat</a>
<button type="submit" class="btn">Simpan Tahap Awal</button>
</div>
</form>
</div>
</div>
<script>
function addRow() {
const tbody = document.getElementById('rows');
const tr = document.createElement('tr');
tr.className = 'training-row';
tr.innerHTML = `
<td class="row-no row-counter"></td>
<td style="display:none;"><input type="hidden" name="id[]" value=""></td>
<td><input type="text" name="disease[]" placeholder="Nama penyakit"></td>
<td><input type="text" name="category[]" placeholder="Kategori"></td>
<td>0/10</td>
<td><span class="tag need">Belum Siap</span></td>
<td><span class="btn secondary" style="opacity:.6;pointer-events:none;">Simpan dulu</span></td>
`;
tbody.appendChild(tr);
reindexRows();
}
let pendingDeleteRow = null;
function askDeleteTrainingRow(btn) {
pendingDeleteRow = btn.closest('.training-row');
const modalEl = document.getElementById('confirmDeleteTrainingModal');
if (window.bootstrap && modalEl) {
const modal = new bootstrap.Modal(modalEl);
modal.show();
return;
}
deleteTrainingRow();
}
function deleteTrainingRow() {
const row = pendingDeleteRow;
const modalEl = document.getElementById('confirmDeleteTrainingModal');
if (modalEl && window.bootstrap) {
const instance = bootstrap.Modal.getInstance(modalEl);
if (instance) instance.hide();
}
const rows = document.querySelectorAll('.training-row');
if (!row || rows.length <= 1) {
const minRowAlert = document.getElementById('minRowAlert');
if (minRowAlert) minRowAlert.classList.remove('d-none');
return;
}
row.remove();
pendingDeleteRow = null;
reindexRows();
}
function reindexRows() {
const rows = document.querySelectorAll('#rows .training-row');
rows.forEach((row, idx) => {
const no = row.querySelector('.row-counter');
if (no) no.textContent = idx + 1;
});
}
reindexRows();
</script>
<div id="minRowAlert" class="alert alert-warning d-none" style="position:fixed;right:16px;bottom:16px;z-index:1080;">
Minimal harus ada 1 baris data.
</div>
<div class="modal fade" id="confirmDeleteTrainingModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Konfirmasi Hapus</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">Yakin ingin menghapus baris data ini?</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" style="width:auto;">Batal</button>
<button type="button" class="btn btn-danger" style="width:auto;" onclick="deleteTrainingRow()">Ya, Hapus</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,77 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Atur Gejala Sample - PawMedic Admin</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
body{margin:0;font-family:'Inter',sans-serif;background:#f4faf7;color:#1f2937}
.container{max-width:98vw;margin:0 auto;padding:24px 14px}
.head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px}
.title{font-family:'Poppins',sans-serif;font-size:28px;margin:0;color:#114d3a}
.sub{color:#64748b;margin-top:4px}
.card{background:#fff;border:1px solid #d1fae5;border-radius:16px;padding:14px;box-shadow:0 8px 20px rgba(17,77,58,.08)}
.notice{background:#e8f7ef;border:1px solid #b7ebcf;padding:10px;border-radius:10px;color:#114d3a;margin-bottom:10px}
.btn{padding:10px 14px;border-radius:10px;border:1px solid #6fcf97;background:#6fcf97;color:#fff;font-weight:700;cursor:pointer;text-decoration:none}
.btn.secondary{background:#fff;color:#114d3a}
.table-wrap{overflow:auto;border:1px solid #e2e8f0;border-radius:10px}
table{width:100%;border-collapse:collapse}
th,td{padding:8px;border-bottom:1px solid #e2e8f0;vertical-align:middle}
th{background:#e8f7ef;text-align:center;font-size:12px}
td{text-align:center}
.sample-col{min-width:86px;font-weight:700;color:#14532d}
.gejala-col{min-width:115px}
.actions{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:12px}
</style>
</head>
<body>
<div class="container">
<div class="head">
<div>
<h1 class="title">Atur Gejala: {{ $item['disease'] ?? '-' }}</h1>
<div class="sub">Kategori: {{ $item['category'] ?? '-' }} | Isi 10 baris sample</div>
</div>
<a href="{{ route('admin.training.settings') }}" class="btn secondary"> Kembali</a>
</div>
<div class="card">
<div class="notice">Centang gejala yang bernilai <strong>Ya</strong>. Tidak dicentang berarti <strong>Tidak</strong>.</div>
<form method="POST" action="{{ route('admin.training.symptoms.save', $item['id']) }}">
@csrf
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sample-col">Sample</th>
@foreach($featureCols as $col)
<th class="gejala-col">{{ $col }}</th>
@endforeach
</tr>
</thead>
<tbody>
@for($r = 0; $r < 10; $r++)
<tr>
<td class="sample-col">{{ $r + 1 }}</td>
@foreach($featureCols as $col)
@php
$isYes = strcasecmp((string)($item['symptom_samples'][$r][$col] ?? 'Tidak'), 'Ya') === 0;
@endphp
<td>
<input type="checkbox" name="sample_rows[{{ $r }}][{{ $col }}]" value="1" {{ $isYes ? 'checked' : '' }}>
</td>
@endforeach
</tr>
@endfor
</tbody>
</table>
</div>
<div class="actions">
<a href="{{ route('admin.training.settings') }}" class="btn secondary">Kembali</a>
<button type="submit" class="btn">Simpan Data</button>
</div>
</form>
</div>
</div>
</body>
</html>

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Input Biodata - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}">
<style>
/* ===== GLOBAL ===== */
@ -301,6 +302,16 @@
margin-bottom:24px;
}
}
@media (max-width:576px) and (orientation:portrait){
.header h1{font-size:1.35rem;line-height:1.35;}
.header p{font-size:14px;}
.logo-icon{width:38px;height:38px;}
.form-card{padding:18px 14px;border-radius:16px;}
.form-group label{font-size:14px;}
.form-group input,.form-group select,.form-group textarea{font-size:14px;padding:10px 12px;}
.btn{font-size:14px;padding:10px 12px;}
}
</style>
</head>
@ -309,7 +320,14 @@
<!-- HEADER -->
<div class="header">
<a href="/" class="logo-link">
<div class="logo-icon">🐾</div>
<div class="logo-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</div>
<div class="logo-text">PawMedic</div>
</a>
@php

View File

@ -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 = `

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FAQ - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}">
<style>
:root{
@ -161,7 +162,7 @@
}
.faq-card.active .faq-question::before{
content:'';
content:'';
}
.back-btn{
@ -207,6 +208,16 @@
padding:28px 24px;
}
}
@media (max-width:576px) and (orientation:portrait){
.container{padding:14px;}
.header h1{font-size:1.35rem;}
.header p{font-size:14px;}
.logo-icon{width:38px;height:38px;}
.faq-card{padding:16px 12px;border-radius:14px;}
.faq-question{font-size:15px;}
.faq-answer{font-size:14px;line-height:1.6;}
}
</style>
</head>
@ -218,7 +229,14 @@
<div class="header">
<a href="/" class="logo-link">
<div class="logo-icon">🐾</div>
<div class="logo-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</div>
<div class="logo-text">PawMedic</div>
</a>
<h1>Pertanyaan Umum</h1>

View File

@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pilih Gejala - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}">
<style>
/* ===== GLOBAL ===== */
@ -749,6 +751,16 @@
width:100%;
}
}
@media (max-width:576px) and (orientation:portrait){
.header h1{font-size:1.35rem;line-height:1.35;}
.header p{font-size:14px;}
.logo-icon{width:38px;height:38px;}
.form-card{padding:18px 14px;border-radius:16px;}
.gejala-label{font-size:13.5px;padding:14px 14px;}
.btn{font-size:14px;padding:10px 12px;}
.progress-container{padding:12px 14px;}
}
</style>
</head>
@ -757,7 +769,14 @@
<!-- HEADER -->
<div class="header">
<a href="/" class="logo-link">
<div class="logo-icon">🐾</div>
<div class="logo-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</div>
<div class="logo-text">PawMedic</div>
</a>
@php
@ -780,19 +799,19 @@
<!-- FORM CARD -->
<div class="form-card">
<form id="gejalaForm" action="{{ route('diagnosis.proses') }}" method="POST">
<form id="gejalaForm" method="POST">
@csrf
<!-- Info Box -->
<div class="info-box">
<div class="icon">💡</div>
<div class="icon"><i class="bi bi-lightbulb"></i></div>
<p >Pilih minimal 4 dan maksimal 7 gejala yang terjadi pada kucing anda</p>
</div>
<!-- Gejala Section -->
<div class="gejala-section">
<div class="section-title">
<span>🔍 Gejala yang Ditemukan</span>
<span><i class="bi bi-search"></i> Gejala yang Ditemukan</span>
<span class="selected-count" id="selectedCount">0 dipilih</span>
</div>
@ -801,7 +820,7 @@
<input
type="text"
id="searchGejala"
placeholder="🔍 Cari gejala..."
placeholder="Cari gejala..."
class="search-input"
>
<button type="button" id="clearSearch" class="clear-search" style="display:none;"></button>
@ -874,19 +893,29 @@ function updateSelectedCount() {
// Form submission
form.addEventListener('submit', function(e) {
const checked = document.querySelectorAll('.gejala-checkbox:checked').length;
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

View File

@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hasil Diagnosis - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}">
@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;}
}
</style>
</head>
@ -455,7 +480,14 @@
<div class="container">
<div class="header">
<a href="/" class="logo-link">
<div class="logo-icon">🐾</div>
<div class="logo-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</div>
<div class="logo-text">PawMedic</div>
</a>
@php
@ -472,7 +504,7 @@
<div class="result-card">
<div class="result-header">
<div class="result-icon">🩺</div>
<div class="result-icon"><i class="bi bi-clipboard2-pulse"></i></div>
<div class="result-title">Diagnosis Selesai</div>
<div class="result-subtitle">Berikut adalah hasil analisis gejala kucing Anda</div>
</div>
@ -498,7 +530,7 @@
<!-- Gejala yang Dipilih -->
<div class="gejala-list-section">
<div class="section-title">
<span>🔍 Gejala yang Dipilih</span>
<span><i class="bi bi-search"></i> Gejala yang Dipilih</span>
</div>
<div class="gejala-list">
@foreach(session('gejala', []) as $g)
@ -510,7 +542,7 @@
<!-- Recommendation -->
<div class="recommendation">
<div class="recommendation-title">
<span>💡 Rekomendasi Perawatan</span>
<span><i class="bi bi-lightbulb"></i> Rekomendasi Perawatan</span>
</div>
<ul class="recommendation-list">
@foreach($diagnosis['pertolongan'] ?? [] as $item)
@ -521,7 +553,7 @@
<div class="recommendation">
<div class="recommendation-title">
<span>🛡️ Pencegahan</span>
<span><i class="bi bi-shield-check"></i> Pencegahan</span>
</div>
<ul class="recommendation-list">
@foreach($diagnosis['pencegahan'] ?? [] as $item)
@ -533,7 +565,7 @@
<!-- Warning -->
<div class="warning-box">
<div class="warning-title">
<span>⚠️ Peringatan Penting</span>
<span><i class="bi bi-exclamation-triangle"></i> Peringatan Penting</span>
</div>
<div class="warning-text">
Hasil diagnosis ini hanya sebagai panduan awal. Untuk diagnosis yang akurat dan penanganan yang tepat,
@ -544,16 +576,13 @@
<!-- Action Buttons -->
<div class="action-buttons">
<a href="{{ route('gejala') }}" class="btn btn-secondary">
Diagnosis Lagi
<i class="bi bi-arrow-left"></i> Diagnosis Lagi
</a>
<a href="{{ route('hasil-diagnosis.pdf') }}" class="btn btn-secondary">
<i class="bi bi-download"></i> Download Hasil
</a>
<button onclick="printDiagnosis()" class="btn btn-secondary">
🖨️ Cetak Hasil
</button>
<button onclick="shareDiagnosis()" class="btn btn-secondary">
📤 Bagikan
</button>
<a href="/" class="btn btn-primary">
🏠 Kembali ke Beranda
<i class="bi bi-house-door"></i> Kembali ke Beranda
</a>
</div>
</div>
@ -562,7 +591,7 @@
@if(isset($diagnosisHistory) && $diagnosisHistory->count() > 0)
<div class="result-card history-section">
<div class="section-title">
<span>🕘 Riwayat Diagnosis (Nomor yang sama)</span>
<span><i class="bi bi-clock-history"></i> Riwayat Diagnosis (Nomor yang sama)</span>
</div>
<table class="history-table">
<thead>
@ -575,7 +604,11 @@
<tbody>
@foreach($diagnosisHistory as $row)
<tr>
<td>{{ \Carbon\Carbon::parse($row->created_at)->format('d M Y H:i') }}</td>
<td>
<span class="rt-time" data-ts="{{ \Carbon\Carbon::parse($row->created_at)->toIso8601String() }}">
{{ \Carbon\Carbon::parse($row->created_at)->format('d M Y H:i') }}
</span>
</td>
<td>{{ $row->nama_kucing ?? '-' }}</td>
<td>{{ $row->hasil_diagnosis ?? '-' }}</td>
</tr>
@ -588,76 +621,37 @@
@include('components.scroll-top')
<script>
// Print function
function printDiagnosis() {
const diagnosis = @json($diagnosis);
const gejala = @json(session('gejala', []));
const penjelasan = @json($diseaseDescription ?? '');
const html = `
<html>
<head>
<meta charset="utf-8">
<title>Cetak Hasil Diagnosis</title>
<style>
body{font-family:Arial,sans-serif;padding:24px;color:#111;line-height:1.5}
h1{font-size:22px;margin-bottom:6px}
.muted{color:#666;font-size:13px;margin-bottom:18px}
.box{border:1px solid #ddd;border-radius:8px;padding:12px;margin-bottom:12px}
ul{margin:8px 0 0 18px}
</style>
</head>
<body>
<h1>Hasil Diagnosis PawMedic</h1>
<div class="muted">Dicetak pada: ${new Date().toLocaleString('id-ID')}</div>
<div class="box">
<strong>Penyakit:</strong> ${diagnosis.nama || '-'}<br>
<strong>Jenis:</strong> ${diagnosis.kategori || '-'}
${penjelasan ? `<br><strong>Penjelasan:</strong> ${penjelasan}` : ''}
</div>
<div class="box">
<strong>Gejala Dipilih:</strong>
<ul>${(gejala || []).map(g => `<li>${g}</li>`).join('') || '<li>-</li>'}</ul>
</div>
<div class="box">
<strong>Pertolongan:</strong>
<ul>${(diagnosis.pertolongan || []).map(p => `<li>${p}</li>`).join('') || '<li>-</li>'}</ul>
</div>
<div class="box">
<strong>Pencegahan:</strong>
<ul>${(diagnosis.pencegahan || []).map(p => `<li>${p}</li>`).join('') || '<li>-</li>'}</ul>
</div>
</body>
</html>`;
const w = window.open('', '_blank');
if (!w) return;
w.document.open();
w.document.write(html);
w.document.close();
w.focus();
w.print();
w.close();
function pad2(n){ return String(n).padStart(2,'0'); }
function formatAbsolute(d){
const months = ['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des'];
return `${pad2(d.getDate())} ${months[d.getMonth()]} ${d.getFullYear()} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
}
// Share function
function shareDiagnosis() {
const diagnosis = "{{ $diagnosis['nama'] ?? '-' }}";
const gejala = @json(session('gejala', []));
const text = `Hasil Diagnosis PawMedic:\n\nPenyakit: ${diagnosis}\nGejala: ${gejala.join(', ')}\n\nDapatkan diagnosis di: ${window.location.origin}`;
if (navigator.share) {
navigator.share({
title: 'Hasil Diagnosis PawMedic',
text: text,
url: window.location.href
function formatRelative(d){
const now = new Date();
const diffMs = now - d;
const diffSec = Math.floor(diffMs / 1000);
if (diffSec < 5) return 'baru saja';
if (diffSec < 60) return `${diffSec} detik lalu`;
const diffMin = Math.floor(diffSec / 60);
if (diffMin < 60) return `${diffMin} menit lalu`;
const diffHour = Math.floor(diffMin / 60);
if (diffHour < 24) return `${diffHour} jam lalu`;
const diffDay = Math.floor(diffHour / 24);
if (diffDay < 7) return `${diffDay} hari lalu`;
return formatAbsolute(d);
}
function updateRealtimeTimes(){
document.querySelectorAll('.rt-time').forEach((el) => {
const ts = el.getAttribute('data-ts');
if (!ts) return;
const d = new Date(ts);
if (isNaN(d.getTime())) return;
el.textContent = formatRelative(d);
el.title = formatAbsolute(d);
});
} else {
navigator.clipboard.writeText(text);
alert('Hasil diagnosis disalin!');
}
}
updateRealtimeTimes();
setInterval(updateRealtimeTimes, 15000);
</script>
<style>

View File

@ -2,8 +2,13 @@
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PawMedic - Sistem Pakar Kucing</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}">
<style>
/* ===== GLOBAL ===== */
@ -20,6 +25,7 @@
color:#333;
-webkit-font-smoothing:antialiased;
line-height:1.6;
overflow-x:hidden;
}
.container{
@ -36,7 +42,7 @@
}
/* ===== NAVBAR ===== */
.navbar{
.navbar-custom{
display:flex;
justify-content:space-between;
align-items:center;
@ -64,6 +70,13 @@
justify-content:center;
font-size:22px;
transition:0.4s;
color:#fff;
}
.paw-inline{
width:20px;
height:20px;
display:inline-block;
fill:currentColor;
}
.logo-text{
@ -101,66 +114,69 @@
}
.nav-menu a:hover{
color:#000;
background:#f1f5f9;
}
/* ===== BUTTON ===== */
/* ===== BUTTON (Modern Elegant) ===== */
.btn{
background:linear-gradient(135deg, #6fcf97 0%, #4bb66f 100%);
color:white;
border:none;
padding:16px 32px;
border-radius:14px;
cursor:pointer;
border-radius:12px;
font-weight:600;
transition:all .25s ease;
letter-spacing:.01em;
}
.btn-nav-cta{
background:linear-gradient(135deg,#66cf94,#42b777) !important;
color:#fff !important;
border:1px solid #46b97a;
box-shadow:0 8px 18px rgba(66,183,119,.22);
font-size:14px;
}
.btn-nav-cta:hover{
transform:translateY(-1px) scale(1.01);
box-shadow:0 10px 20px rgba(66,183,119,.3);
color:rgb(75, 75, 75) !important;
background:rgb(255, 255, 255) !important;
}
.btn-hero-primary{
background:linear-gradient(135deg,#69d29a 0%,#40b674 100%);
color:#fff !important;
border:1px solid #49bb7d;
box-shadow:0 10px 24px rgba(64,182,116,.25);
padding:12px 24px;
font-size:17px;
border-radius:13px;
}
.btn-hero-primary:hover{
transform:translateY(-2px);
box-shadow:0 14px 28px rgba(64,182,116,.31);
color:#fff !important;
}
.btn-hero-secondary{
background:rgba(255,255,255,.7);
color:#1d7a4f !important;
border:1.5px solid rgba(73,187,125,.55);
backdrop-filter:blur(4px);
box-shadow:0 6px 16px rgba(17,77,58,.08);
padding:12px 24px;
font-size:17px;
border-radius:13px;
}
.btn-hero-secondary:hover{
background:#fff;
border-color:#46b97a;
transform:translateY(-1px);
box-shadow:0 10px 20px rgba(17,77,58,.11);
}
.btn-mobile-cta{
background:linear-gradient(135deg,#67d098,#41b775);
color:#fff !important;
border:1px solid #49bb7d;
box-shadow:0 10px 24px rgba(17,77,58,.25);
font-size:16px;
transition:all 0.3s ease;
box-shadow:0 4px 16px rgba(111,207,151,0.3);
position:relative;
overflow:hidden;
text-decoration:none;
display:inline-flex;
align-items:center;
justify-content:center;
gap:8px;
letter-spacing:0.3px;
font-weight:700;
}
.btn::before{
content:'';
position:absolute;
top:0;
left:-100%;
width:100%;
height:100%;
background:linear-gradient(90deg, transparent, rgba(255,255,255,0.25), transparent);
transition:left 0.6s;
}
.btn:hover::before{
left:100%;
}
.btn:hover{
transform:translateY(-4px) scale(1.02);
box-shadow:0 12px 32px rgba(17,77,58,0.25);
background:linear-gradient(135deg, #7dd9a8 0%, #5ac77f 100%);
}
.btn:active{
transform:translateY(-2px) scale(1);
}
.btn:focus{
outline:3px solid rgba(111,207,151,0.4);
outline-offset:3px;
}
.btn.secondary{
background:#ffffff;
color:#2f855a;
border:2px solid #6fcf97;
box-shadow:0 2px 10px rgba(17,77,58,0.1);
}
.btn.secondary:hover{
background:#e8f7ef;
border-color:#4bb66f;
transform:translateY(-3px) scale(1.02);
box-shadow:0 8px 20px rgba(17,77,58,0.15);
.btn-mobile-cta:hover{
color:#fff !important;
}
/* ===== HERO ===== */
@ -299,12 +315,20 @@
margin-bottom:10px;
color:#114d3a;
font-weight:700;
display:flex;
align-items:center;
justify-content:center;
gap:8px;
}
.card.feature h3 i{
color:#4bb66f;
}
.card.feature p{
font-size:15px;
line-height:1.7;
color:#64748b;
margin:0;
word-break:break-word;
}
.hero-image{
@ -312,6 +336,7 @@
align-items:center;
justify-content:center;
flex-shrink:0;
margin:0 auto;
}
.hero-image img{
width:100%;
@ -456,6 +481,21 @@
transform:scale(1.1);
}
.mobile-cta{
display:none;
}
/* Scroll reveal fallback (tanpa library eksternal) */
[data-aos]{
opacity:0;
transform:translateY(20px);
transition:opacity .7s ease, transform .7s ease;
}
[data-aos].aos-show{
opacity:1;
transform:translateY(0);
}
/* ===== ANIMATIONS ===== */
@keyframes fadeUp{
from{
@ -484,17 +524,13 @@
.container{
padding:24px;
}
.navbar{
.navbar-custom{
flex-direction:column;
align-items:flex-start;
gap:14px;
margin-bottom:28px;
}
.nav-menu{
width:100%;
flex-wrap:wrap;
gap:10px;
}
.nav-menu{width:100%;}
.nav-menu a{
font-size:14px;
}
@ -505,8 +541,9 @@
flex-direction:column;
text-align:center;
justify-content:center;
padding:48px 32px;
padding:34px 24px;
min-height:auto;
gap:22px;
}
.hero-content{
margin-bottom:30px;
@ -530,6 +567,10 @@
flex:1;
min-width:200px;
}
.hero-image img{
max-width:360px;
width:100%;
}
.features{
grid-template-columns:repeat(2,1fr);
}
@ -543,54 +584,59 @@
@media(max-width:500px){
.container{
padding:16px;
padding:12px;
}
.logo-text{
font-size:18px;
}
.navbar{
.navbar-custom{
padding:12px 0;
align-items:stretch;
margin-bottom:20px;
}
.menu-toggle{
display:inline-flex;
align-items:center;
justify-content:center;
align-self:flex-end;
.navbar-toggler{
border-radius:10px;
border:1px solid #d1d5db;
padding:6px 10px;
}
.nav-menu{
display:none;
grid-template-columns:1fr 1fr;
width:100%;
}
.nav-menu.open{
display:grid;
padding:10px 0;
}
.nav-menu a{
text-align:center;
padding:8px 6px;
display:block;
text-align:left;
padding:10px 8px;
border-radius:8px;
background:#fff;
border:1px solid #eef5f3;
font-size:14px;
margin-bottom:8px;
}
.nav-menu .btn{
grid-column:1 / -1;
width:100%;
margin-left:0;
}
.hero{
padding:28px 18px 32px;
gap:20px;
border-radius:18px;
padding:20px 12px 18px;
gap:14px;
border-radius:14px;
}
.features{
grid-template-columns:1fr;
}
.hero-content h2{
font-size:22px;
font-size:20px;
line-height:1.3;
margin-bottom:10px;
}
.hero-content p{
font-size:14px;
line-height:1.65;
margin-bottom:16px;
}
.hero-image img{
max-width:320px;
max-width:220px;
width:100%;
}
.hero-actions{
@ -600,26 +646,66 @@
.hero-actions .btn{
width:100%;
min-width:unset;
padding-top:10px;
padding-bottom:10px;
font-size:15px;
}
section{
margin-top:56px;
margin-top:34px;
}
section > p{
font-size:15px;
margin-bottom:24px;
margin-bottom:18px;
line-height:1.6;
}
section h2{
font-size:24px;
margin-bottom:10px;
}
.card.feature{
padding:22px 18px;
padding:18px 14px;
min-height:unset;
border-radius:12px;
}
.card.feature h3{
font-size:17px;
line-height:1.35;
}
.card.feature p{
font-size:14px;
}
#diagnosa{
padding:16px 12px;
margin-top:28px;
border-radius:10px;
}
.testimonials{
gap:12px;
}
.card.testimonial{
padding:14px;
}
.quote{
font-size:14px;
}
footer{
margin-top:44px;
padding-bottom:42px;
margin-top:30px;
padding-bottom:86px;
font-size:13px;
}
.admin-login-link{
bottom:2px;
right:2px;
}
.mobile-cta{
display:none !important;
}
.mobile-cta .btn{
width:100%;
border-radius:12px;
font-size:15px;
padding:12px 14px;
}
}
</style>
</head>
@ -628,23 +714,34 @@
<div class="container">
<!-- NAVBAR -->
<div class="navbar">
<div class="navbar-custom navbar navbar-expand-md">
<div class="logo">
<div class="logo-icon">🐾</div>
<div class="logo-icon" aria-hidden="true">
<svg class="paw-inline" viewBox="0 0 24 24">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</div>
<div class="logo-text">PawMedic</div>
</div>
<button class="menu-toggle" id="menuToggle" aria-label="Buka menu"></button>
<div class="nav-menu" id="navMenu">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu" aria-controls="navMenu" aria-expanded="false" aria-label="Buka menu">
</button>
<div class="collapse navbar-collapse justify-content-end" id="navMenu">
<div class="nav-menu d-md-flex align-items-center gap-2">
<a href="#fitur">Fitur</a>
<a href="#cara">Cara Kerja</a>
<a href="{{ route('ulasan') }}">Ulasan</a>
<a href="{{ route('faq') }}">FAQ</a>
<a href="{{ route('biodata') }}" class="btn" style="padding:10px 20px; font-size:14px;">Mulai Diagnosis</a>
<a href="{{ route('biodata') }}" class="btn btn-nav-cta px-3">Mulai Diagnosis</a>
</div>
</div>
</div>
<!-- HERO -->
<section class="hero">
<section class="hero" data-aos="fade-up">
<div class="hero-content">
<h2>Bantu Jaga Kesehatan Kucing Anda</h2>
<p>
@ -652,11 +749,18 @@
memahami gejala dan mendapatkan rekomendasi perawatan awal dengan mudah dan cepat.
</p>
<div class="hero-actions">
<a href="{{ route('biodata') }}" class="btn">
<a href="{{ route('biodata') }}" class="btn btn-hero-primary btn-lg">
<span>Mulai Diagnosis</span>
<span>🐾</span>
<span>
<svg class="paw-inline" viewBox="0 0 24 24">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</span>
</a>
<button class="btn secondary" onclick="scrollToSection('fitur')">Pelajari Lebih Lanjut</button>
<button class="btn btn-hero-secondary btn-lg" onclick="scrollToSection('fitur')">Pelajari Lebih Lanjut</button>
</div>
</div>
<div class="hero-image">
@ -666,31 +770,31 @@
<!-- FITUR -->
<section id="fitur">
<section id="fitur" data-aos="fade-up">
<h2>Fitur Utama</h2>
<p>Alat yang dirancang untuk membantu pemilik kucing memahami kondisi hewan peliharaan mereka</p>
<div class="features">
<div class="card feature">
<h3>🩺 Konsultasi Cepat</h3>
<div class="card feature" data-aos="zoom-in" data-aos-delay="100">
<h3><i class="bi bi-heart-pulse"></i> Konsultasi Cepat</h3>
<p>Memberikan gambaran awal kondisi kesehatan kucing secara cepat dan mudah</p>
</div>
<div class="card feature">
<h3>🔍 Diagnosis Gejala</h3>
<div class="card feature" data-aos="zoom-in" data-aos-delay="200">
<h3><i class="bi bi-search"></i> Diagnosis Gejala</h3>
<p>Menganalisis gejala menggunakan basis pengetahuan sistem pakar</p>
</div>
<div class="card feature">
<h3>🚑 Penanganan Awal</h3>
<div class="card feature" data-aos="zoom-in" data-aos-delay="300">
<h3><i class="bi bi-shield-check"></i> Penanganan Awal</h3>
<p>Panduan langkah awal sebelum konsultasi ke dokter hewan</p>
</div>
<div class="card feature">
<h3>🐾 Tips Perawatan</h3>
<div class="card feature" data-aos="zoom-in" data-aos-delay="400">
<h3><i class="bi bi-stars"></i> Tips Perawatan</h3>
<p>Menyediakan saran perawatan dasar untuk kucing sehari-hari</p>
</div>
</div>
</section>
<!-- CARA KERJA -->
<section id="cara">
<section id="cara" data-aos="fade-up">
<h2>Cara Kerja</h2>
<div class="features">
<div class="card feature">
@ -709,7 +813,7 @@
</section>
<!-- DIAGNOSA / TESTIMONIALS -->
<section id="diagnosa">
<section id="diagnosa" data-aos="fade-up">
<h2>Ulasan Pengguna</h2>
<p>Pengalaman para pemilik kucing yang telah menggunakan PawMedic.</p>
<p style="margin-top:12px;">
@ -742,26 +846,46 @@
<!-- FOOTER -->
<footer>
<p>© 2026 PawMedic</p>
<p>Email: support@pawmedic.app</p>
<a href="{{ route('admin.login') }}" class="admin-login-link" title="Admin Login">🔐</a>
<p>Email: wahyutegar506041@gmail.com</p>
<a href="{{ route('admin.login') }}" class="admin-login-link" title="Admin Login"><i class="bi bi-lock-fill"></i></a>
</footer>
</div>
<div class="mobile-cta">
<a href="{{ route('biodata') }}" class="btn btn-mobile-cta btn-lg">
<svg class="paw-inline" viewBox="0 0 24 24">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
Mulai Diagnosis
</a>
</div>
<script>
function scrollToSection(id){
document.getElementById(id).scrollIntoView({
behavior:'smooth'
});
}
const menuToggle = document.getElementById('menuToggle');
const navMenu = document.getElementById('navMenu');
if (menuToggle && navMenu) {
menuToggle.addEventListener('click', () => {
navMenu.classList.toggle('open');
menuToggle.textContent = navMenu.classList.contains('open') ? '✕' : '☰';
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const reveals = document.querySelectorAll('[data-aos]');
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('aos-show');
obs.unobserve(entry.target);
}
});
}, { threshold: 0.12 });
reveals.forEach(el => observer.observe(el));
} else {
reveals.forEach(el => el.classList.add('aos-show'));
}
</script>

View File

@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memproses Diagnosis - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}">
<style>
:root{
@ -61,6 +63,7 @@
align-items:center;
justify-content:center;
font-size:28px;
color:#fff;
box-shadow:0 8px 24px rgba(111,207,151,0.3);
animation:logoFloat 3s ease-in-out infinite;
}
@ -91,6 +94,61 @@
animation:fadeUp 0.8s ease;
}
.loading-card::before{
content:'';
position:absolute;
inset:0;
background:linear-gradient(120deg, transparent 25%, rgba(255,255,255,0.38) 50%, transparent 75%);
transform:translateX(-120%);
animation:cardShine 2.2s ease-in-out infinite;
pointer-events:none;
}
@keyframes cardShine{
0%{transform:translateX(-120%);}
100%{transform:translateX(120%);}
}
.bootstrap-loader{
width:80px;
height:80px;
margin:0 auto 22px;
display:flex;
align-items:center;
justify-content:center;
}
.bootstrap-loader .spinner-border{
width:3.5rem;
height:3.5rem;
border:0.35em solid #d1fae5;
border-right-color:#4bb66f;
border-top-color:#6fcf97;
border-radius:50%;
animation:spin 0.8s linear infinite;
}
.progress-wrap{
margin-top:18px;
margin-bottom:10px;
}
.progress-track{
width:100%;
height:8px;
background:#e2f6ea;
border-radius:999px;
overflow:hidden;
}
.progress-bar{
width:0%;
height:100%;
background:linear-gradient(90deg,#6fcf97,#4bb66f);
border-radius:999px;
transition:width 0.45s ease;
}
h1{
font-family:var(--ff-heading);
font-size:32px;
@ -174,6 +232,13 @@
animation:pulse 2s ease infinite;
}
.progress-label{
margin-top:8px;
font-size:13px;
color:#4b5563;
font-weight:600;
}
@keyframes pulse{
0%, 100%{opacity:1;}
50%{opacity:0.6;}
@ -247,6 +312,18 @@
opacity:0;
}
}
@media (max-width:576px) and (orientation:portrait){
.container{padding:14px 10px;}
.logo{margin-bottom:14px;}
.logo-icon{width:38px;height:38px;}
.logo-text{font-size:18px;}
.loading-card{padding:18px 14px;border-radius:14px;}
h1{font-size:1.35rem;margin-bottom:10px;}
p{font-size:14px;margin-bottom:16px;}
.bootstrap-loader .spinner-border{width:2.6rem;height:2.6rem;}
.status-text,.progress-label{font-size:13px;}
}
</style>
</head>
@ -265,7 +342,14 @@
<div class="container">
<div class="logo">
<div class="logo-icon">🐾</div>
<div class="logo-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</div>
<div class="logo-text">PawMedic</div>
</div>
@ -273,8 +357,8 @@
<h1>Memproses Diagnosis</h1>
<p>Sedang menganalisis gejala yang Anda pilih...</p>
<div class="loader">
<div class="loader-circle"></div>
<div class="bootstrap-loader">
<div class="spinner-border" role="status" aria-label="Loading"></div>
</div>
<div class="loading-dots">
@ -283,6 +367,12 @@
<div class="dot"></div>
</div>
<div class="progress-wrap">
<div class="progress-track">
<div class="progress-bar" id="progressBar"></div>
</div>
</div>
<div class="progress-label" id="progressLabel">0%</div>
<div class="status-text" id="statusText">Menganalisis data gejala...</div>
</div>
</div>
@ -298,44 +388,68 @@
let currentStatus = 0;
const statusText = document.getElementById('statusText');
const progressBar = document.getElementById('progressBar');
const progressLabel = document.getElementById('progressLabel');
let progress = 0;
// Update status message
// status text
setInterval(() => {
currentStatus = (currentStatus + 1) % statusMessages.length;
statusText.textContent = statusMessages[currentStatus];
}, 2000);
// Get gejala from URL
const urlParams = new URLSearchParams(window.location.search);
const gejalaParam = urlParams.get('gejala');
// progress animation
const progressTimer = setInterval(() => {
if (progress < 92) {
progress += Math.random() * 4;
} else if (progress < 97) {
progress += Math.random() * 1.2;
}
// Simulate processing and redirect
setTimeout(() => {
if (gejalaParam) {
// Submit form to process diagnosis
progress = Math.min(progress, 97);
const rounded = Math.floor(progress);
progressBar.style.width = rounded + '%';
progressLabel.textContent = rounded + '%';
}, 220);
// 🔥 AMBIL DATA DARI LOCAL STORAGE
const gejala = JSON.parse(localStorage.getItem('gejala'));
// VALIDASI
if (!gejala || gejala.length === 0) {
window.location.href = '/cek-penyakit';
}
// KIRIM KE LARAVEL
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ route("diagnosis.proses") }}';
form.action = '/diagnosis/proses';
const csrf = document.createElement('input');
// CSRF
let csrf = document.createElement('input');
csrf.type = 'hidden';
csrf.name = '_token';
csrf.value = '{{ csrf_token() }}';
form.appendChild(csrf);
const gejalaInput = document.createElement('input');
gejalaInput.type = 'hidden';
gejalaInput.name = 'gejala';
gejalaInput.value = gejalaParam;
form.appendChild(gejalaInput);
// DATA GEJALA
gejala.forEach(g => {
let input = document.createElement('input');
input.type = 'hidden';
input.name = 'gejala[]';
input.value = g;
form.appendChild(input);
});
document.body.appendChild(form);
// SUBMIT SETELAH LOADING
setTimeout(() => {
progressBar.style.width = '100%';
progressLabel.textContent = '100%';
clearInterval(progressTimer);
form.submit();
} else {
// Fallback: redirect to hasil
window.location.href = '/hasil-diagnosis';
}
}, 3000);
}, 4600);
</script>
</body>

View File

@ -0,0 +1,71 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Hasil Diagnosis PawMedic</title>
<style>
body { font-family: DejaVu Sans, sans-serif; font-size: 12px; color: #1f2937; line-height: 1.5; }
.header { margin-bottom: 16px; border-bottom: 2px solid #16a34a; padding-bottom: 8px; }
.title { font-size: 22px; font-weight: 700; color: #065f46; margin: 0; }
.muted { color: #4b5563; font-size: 11px; margin-top: 4px; }
.box { border: 1px solid #d1d5db; border-radius: 6px; padding: 10px; margin-bottom: 10px; }
.box h3 { margin: 0 0 8px 0; font-size: 14px; color: #065f46; }
ul { margin: 0; padding-left: 18px; }
li { margin: 3px 0; }
</style>
</head>
<body>
<div class="header">
<h1 class="title">Hasil Diagnosis PawMedic</h1>
<div class="muted">Dibuat pada: {{ $generatedAt }}</div>
</div>
<div class="box">
<h3>Ringkasan Diagnosis</h3>
<div><strong>Penyakit:</strong> {{ $diagnosis['nama'] ?? '-' }}</div>
<div><strong>Jenis:</strong> {{ $diagnosis['kategori'] ?? '-' }}</div>
@if(!empty($diseaseDescription))
<div style="margin-top:6px;"><strong>Penjelasan:</strong> {{ $diseaseDescription }}</div>
@endif
</div>
<div class="box">
<h3>Gejala Dipilih</h3>
@if(!empty($gejala))
<ul>
@foreach($gejala as $item)
<li>{{ $item }}</li>
@endforeach
</ul>
@else
<div>-</div>
@endif
</div>
<div class="box">
<h3>Rekomendasi Perawatan</h3>
@if(!empty($diagnosis['pertolongan']) && is_array($diagnosis['pertolongan']))
<ul>
@foreach($diagnosis['pertolongan'] as $item)
<li>{{ $item }}</li>
@endforeach
</ul>
@else
<div>-</div>
@endif
</div>
<div class="box">
<h3>Pencegahan</h3>
@if(!empty($diagnosis['pencegahan']) && is_array($diagnosis['pencegahan']))
<ul>
@foreach($diagnosis['pencegahan'] as $item)
<li>{{ $item }}</li>
@endforeach
</ul>
@else
<div>-</div>
@endif
</div>
</body>
</html>

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ulasan Pengguna - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}">
<style>
:root{
@ -214,17 +217,19 @@
}
.star{
font-size:32px;
color:#ddd;
font-size:34px;
color:#d1d5db;
cursor:pointer;
transition:all 0.2s ease;
transition:all 0.2s ease, text-shadow 0.2s ease;
user-select:none;
padding:2px;
}
.star:hover,
.star.active{
color:var(--warning);
transform:scale(1.1);
text-shadow:0 4px 10px rgba(245,158,11,.35);
}
/* ===== REVIEWS GRID ===== */
@ -469,6 +474,18 @@
align-items:flex-start;
}
}
@media (max-width:576px) and (orientation:portrait){
.container{padding:14px;}
.header h1{font-size:1.35rem;}
.header p{font-size:14px;}
.logo-icon{width:38px;height:38px;}
.stats-card{grid-template-columns:1fr 1fr;gap:10px;}
.stat-value{font-size:1.05rem;}
.form-card{padding:18px 14px;border-radius:16px;}
.btn{font-size:14px;padding:10px 12px;}
.review-card{padding:14px;}
}
</style>
</head>
@ -480,7 +497,14 @@
<div class="header">
<a href="/" class="logo-link">
<div class="logo-icon">🐾</div>
<div class="logo-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<circle cx="6" cy="8" r="2.2"></circle>
<circle cx="10.8" cy="5.6" r="2.1"></circle>
<circle cx="15.8" cy="8" r="2.2"></circle>
<path d="M12 10.6c-3.4 0-5.9 2.4-5.9 4.9 0 2.2 1.8 3.9 4 3.9 1.4 0 1.9-.7 2-.7s.6.7 2 .7c2.2 0 4-1.7 4-3.9 0-2.6-2.6-4.9-6.1-4.9z"></path>
</svg>
</div>
<div class="logo-text">PawMedic</div>
</a>
<h1>Ulasan Pengguna</h1>
@ -506,7 +530,7 @@
<!-- Form Submit Ulasan -->
<div class="form-card">
<div class="form-title">
<span>✍️</span>
<span><i class="bi bi-pencil-square"></i></span>
<span>Tulis Ulasan Anda</span>
</div>
<form method="POST" action="{{ route('ulasan.store') }}">
@ -547,16 +571,25 @@
<div class="section-header">
<div class="section-title">Ulasan Pengguna</div>
<div class="filter-buttons">
<button class="filter-btn active" data-filter="all">Semua</button>
<button class="filter-btn" data-filter="5">5 Bintang</button>
<button class="filter-btn" data-filter="4">4 Bintang</button>
<button class="filter-btn" data-filter="3">3 Bintang</button>
<button class="filter-btn active" data-filter-rating="all">Semua Bintang</button>
<button class="filter-btn" data-filter-rating="5">5 Bintang</button>
<button class="filter-btn" data-filter-rating="4">4 Bintang</button>
<button class="filter-btn" data-filter-rating="3">3 Bintang</button>
<button class="filter-btn" data-filter-rating="2">2 Bintang</button>
<button class="filter-btn" data-filter-rating="1">1 Bintang</button>
@auth
@if(Auth::user()->email === 'admin@pawmedic.app')
<button class="filter-btn" data-filter-visibility="all">Semua Status</button>
<button class="filter-btn" data-filter-visibility="visible">Tampil</button>
<button class="filter-btn" data-filter-visibility="hidden">Hidden</button>
@endif
@endauth
</div>
</div>
<div class="reviews-grid">
@foreach($ulasan as $review)
<div class="review-card" data-rating="{{ $review->rating }}">
<div class="review-card" data-rating="{{ $review->rating }}" data-hidden="{{ ($review->is_hidden ?? false) ? '1' : '0' }}">
<div class="review-header">
<div class="avatar">{{ substr($review->nama,0,1) }}</div>
<div class="review-info">
@ -566,6 +599,9 @@
</div>
<div class="review-rating">
{{ str_repeat('★', $review->rating) }}
@if(($review->is_hidden ?? false))
<span style="margin-left:8px;font-size:12px;color:#b45309;background:#fef3c7;padding:2px 8px;border-radius:999px;">Hidden</span>
@endif
</div>
</div>
</div>
@ -576,17 +612,31 @@
<!-- HANYA ADMIN -->
@auth
@if(Auth::user()->email === 'admin@pawmedic.app')
<div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap;">
<form action="{{ route('ulasan.toggleHide', $review->id) }}" method="POST" style="display:inline;">
@csrf
<button type="submit"
style="background:#f59e0b; color:white; border:none; padding:6px 12px; border-radius:6px;">
@if(($review->is_hidden ?? false))
<i class="bi bi-eye"></i> Tampilkan
@else
<i class="bi bi-eye-slash"></i> Hide
@endif
</button>
</form>
<form action="{{ route('ulasan.delete', $review->id) }}"
method="POST"
onsubmit="return confirm('Yakin hapus ulasan ini?')"
style="margin-top:10px;">
class="delete-review-form"
data-name="{{ $review->nama }}"
style="display:inline;">
@csrf
@method('DELETE')
<button type="submit"
style="background:#ef4444; color:white; border:none; padding:6px 12px; border-radius:6px;">
🗑 Hapus
<i class="bi bi-trash"></i> Hapus
</button>
</form>
</div>
@endif
@endauth
@ -603,29 +653,75 @@
@include('components.toast')
@include('components.scroll-top')
<div class="modal fade" id="deleteReviewModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="border-radius:14px;">
<div class="modal-header">
<h5 class="modal-title">Konfirmasi Hapus Ulasan</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">Yakin hapus ulasan ini?</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" style="width:auto;">Batal</button>
<button type="button" class="btn btn-danger" id="confirmDeleteReview" style="width:auto;">Ya, Hapus</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const buttons = document.querySelectorAll('.filter-btn');
const cards = document.querySelectorAll('.review-card');
let activeRatingFilter = 'all';
let activeVisibilityFilter = 'all';
buttons.forEach(btn => {
btn.addEventListener('click', () => {
// hapus active semua
buttons.forEach(b => b.classList.remove('active'));
const ratingFilter = btn.getAttribute('data-filter-rating');
const visibilityFilter = btn.getAttribute('data-filter-visibility');
if (ratingFilter !== null) {
activeRatingFilter = ratingFilter;
document.querySelectorAll('[data-filter-rating]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const filter = btn.getAttribute('data-filter');
}
if (visibilityFilter !== null) {
activeVisibilityFilter = visibilityFilter;
document.querySelectorAll('[data-filter-visibility]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
applyReviewFilters();
});
});
function applyReviewFilters() {
cards.forEach(card => {
const rating = card.getAttribute('data-rating');
if (filter === 'all' || rating === filter) {
card.style.display = 'block';
} else {
card.style.display = 'none';
const isHidden = card.getAttribute('data-hidden') === '1';
const passRating = activeRatingFilter === 'all' || rating === activeRatingFilter;
let passVisibility = true;
if (activeVisibilityFilter === 'visible') passVisibility = !isHidden;
if (activeVisibilityFilter === 'hidden') passVisibility = isHidden;
card.style.display = (passRating && passVisibility) ? 'block' : 'none';
});
}
applyReviewFilters();
</script>
<script>
let pendingDeleteForm = null;
const deleteModalEl = document.getElementById('deleteReviewModal');
const deleteModal = deleteModalEl && window.bootstrap ? new bootstrap.Modal(deleteModalEl) : null;
document.querySelectorAll('.delete-review-form').forEach((form) => {
form.addEventListener('submit', (e) => {
e.preventDefault();
pendingDeleteForm = form;
if (deleteModal) {
deleteModal.show();
} else if (confirm('Yakin hapus ulasan ini?')) {
form.submit();
}
});
});
document.getElementById('confirmDeleteReview')?.addEventListener('click', () => {
if (pendingDeleteForm) pendingDeleteForm.submit();
});
</script>
<script>

View File

@ -23,6 +23,24 @@
Route::post('/admin/faq-settings', [AdminController::class, 'saveFaqSettings'])
->name('admin.faq.settings.save')
->middleware('auth');
Route::get('/admin/training-settings', [AdminController::class, 'trainingDataSettings'])
->name('admin.training.settings')
->middleware('auth');
Route::post('/admin/training-settings', [AdminController::class, 'saveTrainingDataSettings'])
->name('admin.training.settings.save')
->middleware('auth');
Route::get('/admin/training-settings/{id}/symptoms', [AdminController::class, 'editTrainingSymptoms'])
->name('admin.training.symptoms.edit')
->middleware('auth');
Route::post('/admin/training-settings/{id}/symptoms', [AdminController::class, 'saveTrainingSymptoms'])
->name('admin.training.symptoms.save')
->middleware('auth');
Route::get('/admin/training-template', [AdminController::class, 'downloadTrainingTemplate'])
->name('admin.training.template')
->middleware('auth');
Route::get('/admin/training-export', [AdminController::class, 'downloadTrainingData'])
->name('admin.training.export')
->middleware('auth');
Route::delete('/ulasan/{id}', [UlasanController::class, 'destroy'])->name('ulasan.delete');
@ -33,9 +51,12 @@
Route::delete('/admin/ulasan/{id}', [UlasanController::class, 'destroy'])
->name('ulasan.delete');
Route::post('/admin/ulasan/{id}/toggle-hide', [UlasanController::class, 'toggleHide'])
->name('ulasan.toggleHide')
->middleware('auth');
Route::get('/', function () {
$ulasan = Ulasan::latest()->take(3)->get();
$ulasan = Ulasan::where('is_hidden', false)->latest()->take(3)->get();
return view('landing', compact('ulasan'));
});
@ -50,13 +71,25 @@
Route::post('/diagnosis/proses', [DiagnosisController::class, 'prosesDiagnosis'])->name('diagnosis.proses');
Route::get('/hasil-diagnosis', [DiagnosisController::class, 'hasil'])->name('hasil-diagnosis');
Route::get('/hasil-diagnosis/pdf', [DiagnosisController::class, 'downloadPdf'])->name('hasil-diagnosis.pdf');
Route::get('/faq', [AdminController::class, 'faqPage'])->name('faq');
// Admin Routes
Route::get('/admin/login', [AdminController::class, 'login'])->name('admin.login');
Route::post('/admin/login', [AdminController::class, 'authenticate'])->name('admin.authenticate');
Route::get('/admin/lupa-password', [AdminController::class, 'forgotPasswordPage'])->name('admin.forgot.password');
Route::post('/admin/lupa-password/send-otp', [AdminController::class, 'sendForgotOtp'])->name('admin.forgot.sendOtp');
Route::post('/admin/lupa-password/verify-otp', [AdminController::class, 'verifyForgotOtp'])->name('admin.forgot.verifyOtp');
Route::get('/admin/lupa-password/ganti-password', [AdminController::class, 'resetPasswordPage'])->name('admin.forgot.reset.form');
Route::post('/admin/lupa-password/ganti-password', [AdminController::class, 'resetPasswordSubmit'])->name('admin.forgot.reset.submit');
Route::post('/admin/recover/send-otp', [AdminController::class, 'sendRecoveryOtp'])->name('admin.recover.sendOtp');
Route::post('/admin/recover-password', [AdminController::class, 'recoverPassword'])->name('admin.recover.password');
Route::post('/admin/logout', [AdminController::class, 'logout'])->name('admin.logout');
Route::get('/admin/dashboard', [AdminController::class, 'dashboard'])->name('admin.dashboard')->middleware('auth');
Route::post('/biodata/simpan', [DiagnosisController::class, 'simpanBiodata'])->name('biodata.simpan');
Route::get('/admin/statistik', [AdminController::class, 'statistik'])->name('admin.statistik');
Route::get('/loading-diagnosis', function () {
return view('loading');
})->name('diagnosis.loading');