update besar besaran
This commit is contained in:
parent
6ad6bb79f4
commit
920e6caf3d
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ class Biodata extends Model
|
|||
'berat_badan',
|
||||
'ras_kucing',
|
||||
'alamat',
|
||||
'no_telepon'
|
||||
'no_telepon',
|
||||
'hasil_diagnosis',
|
||||
'jenis',
|
||||
'gejala_dipilih',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class Ulasan extends Model
|
|||
'nama_kucing',
|
||||
'hasil_diagnosis',
|
||||
'rating',
|
||||
'komentar'
|
||||
'komentar',
|
||||
'is_hidden',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"barryvdh/laravel-dompdf": "^3.1",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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 |
|
|
@ -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,38 +808,62 @@ 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'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: {!! json_encode($stats['daily_labels']) !!},
|
||||
datasets: [{
|
||||
label: 'Jumlah Diagnosis',
|
||||
data: {!! json_encode($stats['daily_data']) !!},
|
||||
tension: 0.35,
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(111, 207, 151, 0.2)',
|
||||
borderColor: '#4bb66f',
|
||||
borderWidth: 3,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#4bb66f'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: true } },
|
||||
scales: {
|
||||
y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: '#e5e7eb' } },
|
||||
x: { grid: { display: false } }
|
||||
}
|
||||
function renderTrendChart(mode = '7d') {
|
||||
const source = trendDatasets[mode] || trendDatasets['7d'];
|
||||
if (!trendChart) {
|
||||
trendChart = new Chart(document.getElementById('chartHarian'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: source.labels,
|
||||
datasets: [{
|
||||
label: source.label,
|
||||
data: source.data,
|
||||
tension: 0.35,
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(111, 207, 151, 0.2)',
|
||||
borderColor: '#4bb66f',
|
||||
borderWidth: 3,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#4bb66f'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: true } },
|
||||
scales: {
|
||||
y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: '#e5e7eb' } },
|
||||
x: { grid: { display: false } }
|
||||
}
|
||||
}
|
||||
});
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<h1 class="title">Pengaturan Penjelasan Penyakit</h1>
|
||||
<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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<h1 class="title">Kelola FAQ</h1>
|
||||
<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">
|
||||
@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>
|
||||
@empty
|
||||
<div class="row">
|
||||
<input type="text" name="questions[]" placeholder="Pertanyaan">
|
||||
<textarea name="answers[]" placeholder="Jawaban"></textarea>
|
||||
</div>
|
||||
@endforelse
|
||||
<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)
|
||||
<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
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,13 +369,20 @@
|
|||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Masukkan password"
|
||||
required
|
||||
>
|
||||
<div class="password-wrap">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
e.preventDefault();
|
||||
|
||||
if (checked < 4) {
|
||||
e.preventDefault();
|
||||
const checked = document.querySelectorAll('.gejala-checkbox:checked');
|
||||
|
||||
if (checked.length < 4) {
|
||||
alert("Minimal pilih 4 gejala!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (checked > 7) {
|
||||
e.preventDefault();
|
||||
if (checked.length > 7) {
|
||||
alert("Maksimal hanya 7 gejala!");
|
||||
return;
|
||||
}
|
||||
|
||||
// ambil gejala
|
||||
let gejala = [];
|
||||
checked.forEach(c => gejala.push(c.value));
|
||||
|
||||
// simpan ke localStorage (AMAN 🔥)
|
||||
localStorage.setItem('gejala', JSON.stringify(gejala));
|
||||
|
||||
// pindah ke loading
|
||||
window.location.href = "/loading-diagnosis";
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
} else {
|
||||
navigator.clipboard.writeText(text);
|
||||
alert('Hasil diagnosis disalin!');
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
updateRealtimeTimes();
|
||||
setInterval(updateRealtimeTimes, 15000);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<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>
|
||||
<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 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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
// Simulate processing and redirect
|
||||
setTimeout(() => {
|
||||
if (gejalaParam) {
|
||||
// Submit form to process diagnosis
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '{{ route("diagnosis.proses") }}';
|
||||
|
||||
const 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);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
} else {
|
||||
// Fallback: redirect to hasil
|
||||
window.location.href = '/hasil-diagnosis';
|
||||
// progress animation
|
||||
const progressTimer = setInterval(() => {
|
||||
if (progress < 92) {
|
||||
progress += Math.random() * 4;
|
||||
} else if (progress < 97) {
|
||||
progress += Math.random() * 1.2;
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
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 = '/diagnosis/proses';
|
||||
|
||||
// CSRF
|
||||
let csrf = document.createElement('input');
|
||||
csrf.type = 'hidden';
|
||||
csrf.name = '_token';
|
||||
csrf.value = '{{ csrf_token() }}';
|
||||
form.appendChild(csrf);
|
||||
|
||||
// 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();
|
||||
}, 4600);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;">
|
||||
method="POST"
|
||||
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,30 +653,76 @@
|
|||
|
||||
@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'));
|
||||
btn.classList.add('active');
|
||||
|
||||
const filter = btn.getAttribute('data-filter');
|
||||
|
||||
cards.forEach(card => {
|
||||
const rating = card.getAttribute('data-rating');
|
||||
|
||||
if (filter === 'all' || rating === filter) {
|
||||
card.style.display = 'block';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
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');
|
||||
}
|
||||
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');
|
||||
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>
|
||||
const stars = document.querySelectorAll('#rating-stars span');
|
||||
|
|
|
|||
|
|
@ -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('/admin/statistik', [AdminController::class, 'statistik'])->name('admin.statistik');
|
||||
|
||||
Route::get('/loading-diagnosis', function () {
|
||||
return view('loading');
|
||||
})->name('diagnosis.loading');
|
||||
Loading…
Reference in New Issue