penambahan fitur

This commit is contained in:
WahyuTegarP 2026-04-23 11:19:37 +07:00
parent 3564fc3fb8
commit 6ad6bb79f4
9 changed files with 993 additions and 296 deletions

View File

@ -10,6 +10,9 @@
use Carbon\CarbonPeriod;
use App\Models\Ulasan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\StreamedResponse;
class AdminController extends Controller
{
@ -53,18 +56,29 @@ public function dashboard()
$yesterday = Biodata::whereDate('created_at', Carbon::yesterday())->count();
$diff = $todayDiagnosis - $yesterday;
// total user
$totalUsers = Biodata::count();
// user list
// user list + diagnosis list
$sort = request('sort');
if ($sort == 'oldest') {
$data = Biodata::orderBy('created_at', 'asc')->get();
} else {
$data = Biodata::orderBy('created_at', 'desc')->get();
}
// Data pengguna unik (hindari duplikasi input yang sama)
$userData = $data->unique(function ($item) {
$phone = trim((string)($item->no_telepon ?? ''));
if ($phone !== '') {
return 'phone:' . $phone;
}
return 'fallback:' . Str::lower(trim((string)($item->nama_pemilik ?? ''))) . '|' .
Str::lower(trim((string)($item->nama_kucing ?? ''))) . '|' .
Str::lower(trim((string)($item->alamat ?? '')));
})->values();
// total user = jumlah data unik
$totalUsers = $userData->count();
// penyakit paling umum
$mostCommon = Biodata::select('hasil_diagnosis')
->whereNotNull('hasil_diagnosis')
@ -142,7 +156,7 @@ public function dashboard()
$stats['rating_labels'] = $ratingChart->pluck('rating');
$stats['rating_data'] = $ratingChart->pluck('total');
return view('admin.dashboard', compact('stats', 'data'));
return view('admin.dashboard', compact('stats', 'data', 'userData'));
}
public function logout(Request $request)
@ -196,4 +210,237 @@ public function sortDiagnosis(Request $request)
return response()->json($data);
}
public function exportDiagnosisExcel(): StreamedResponse
{
$rows = Biodata::orderBy('created_at', 'desc')->get();
$filename = 'diagnosis-pawmedic-' . now()->format('Ymd-His') . '.xls';
$headers = [
'Content-Type' => 'application/vnd.ms-excel; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
'Cache-Control' => 'max-age=0',
];
return response()->streamDownload(function () use ($rows) {
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>Tanggal</th>';
echo '<th>Nama Pemilik</th>';
echo '<th>Nama Kucing</th>';
echo '<th>Umur Kucing</th>';
echo '<th>Jenis Kelamin</th>';
echo '<th>Berat Badan</th>';
echo '<th>Ras Kucing</th>';
echo '<th>Alamat</th>';
echo '<th>No Telepon</th>';
echo '<th>Hasil Diagnosis</th>';
echo '<th>Jenis</th>';
echo '</tr>';
foreach ($rows as $row) {
echo '<tr>';
echo '<td>' . e(optional($row->created_at)->format('d-m-Y H:i')) . '</td>';
echo '<td>' . e($row->nama_pemilik ?? '-') . '</td>';
echo '<td>' . e($row->nama_kucing ?? '-') . '</td>';
echo '<td>' . e($row->umur_kucing ?? '-') . '</td>';
echo '<td>' . e($row->jenis_kelamin ?? '-') . '</td>';
echo '<td>' . e($row->berat_badan ?? '-') . '</td>';
echo '<td>' . e($row->ras_kucing ?? '-') . '</td>';
echo '<td>' . e($row->alamat ?? '-') . '</td>';
echo '<td style="mso-number-format:\'\\@\';">' . e($row->no_telepon ?? '-') . '</td>';
echo '<td>' . e($row->hasil_diagnosis ?? '-') . '</td>';
echo '<td>' . e($row->jenis ?? '-') . '</td>';
echo '</tr>';
}
echo '</table></body></html>';
}, $filename, $headers);
}
public function diseaseSettings()
{
$existing = $this->loadDiseaseExplanations();
$diseasesFromData = Biodata::query()
->whereNotNull('hasil_diagnosis')
->where('hasil_diagnosis', '!=', '')
->pluck('hasil_diagnosis')
->map(fn ($d) => trim((string) $d))
->filter()
->unique()
->values()
->all();
$diseases = collect(array_merge($diseasesFromData, array_keys($existing)))
->map(fn ($d) => trim((string) $d))
->filter()
->unique()
->sort()
->values()
->all();
return view('admin.disease-settings', [
'diseases' => $diseases,
'descriptions' => $existing,
]);
}
public function saveDiseaseSettings(Request $request)
{
$items = $request->input('descriptions', []);
$normalized = [];
if (is_array($items)) {
foreach ($items as $name => $description) {
$diseaseName = trim((string) $name);
if ($diseaseName === '') {
continue;
}
$desc = trim((string) $description);
if ($desc === '') {
continue;
}
$normalized[$diseaseName] = $desc;
}
}
$path = $this->diseaseExplanationPath();
$dir = dirname($path);
if (!is_dir($dir)) {
File::makeDirectory($dir, 0755, true);
}
file_put_contents(
$path,
json_encode($normalized, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);
return redirect()
->route('admin.disease.settings')
->with('success', 'Penjelasan penyakit berhasil disimpan.');
}
private function diseaseExplanationPath(): string
{
return storage_path('app/disease_explanations.json');
}
private function loadDiseaseExplanations(): array
{
$path = $this->diseaseExplanationPath();
if (!file_exists($path)) {
return [];
}
$decoded = json_decode((string) file_get_contents($path), true);
return is_array($decoded) ? $decoded : [];
}
public function faqSettings()
{
return view('admin.faq-settings', [
'faqs' => $this->loadFaqItems(),
]);
}
public function saveFaqSettings(Request $request)
{
$questions = $request->input('questions', []);
$answers = $request->input('answers', []);
$faqs = [];
if (is_array($questions) && is_array($answers)) {
$count = max(count($questions), count($answers));
for ($i = 0; $i < $count; $i++) {
$q = trim((string)($questions[$i] ?? ''));
$a = trim((string)($answers[$i] ?? ''));
if ($q === '' || $a === '') {
continue;
}
$faqs[] = [
'question' => $q,
'answer' => $a,
];
}
}
$path = $this->faqPath();
$dir = dirname($path);
if (!is_dir($dir)) {
File::makeDirectory($dir, 0755, true);
}
file_put_contents(
$path,
json_encode($faqs, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);
return redirect()->route('admin.faq.settings')->with('success', 'FAQ berhasil disimpan.');
}
public function faqPage()
{
return view('faq', [
'faqs' => $this->loadFaqItems(),
]);
}
private function faqPath(): string
{
return storage_path('app/faqs.json');
}
private function defaultFaqItems(): array
{
return [
[
'question' => 'Apa itu PawMedic?',
'answer' => 'PawMedic adalah aplikasi sistem pakar yang membantu pemilik kucing memahami gejala dan mendapatkan rekomendasi perawatan awal. Aplikasi ini menggunakan metode sistem pakar untuk menganalisis gejala yang dipilih dan memberikan diagnosis kemungkinan penyakit.',
],
[
'question' => 'Bagaimana cara menggunakan PawMedic?',
'answer' => "Cara menggunakan PawMedic sangat mudah:\n1. Isi biodata kucing Anda\n2. Pilih gejala yang Anda amati pada kucing\n3. Sistem akan menganalisis dan memberikan hasil diagnosis\n4. Baca rekomendasi perawatan yang diberikan",
],
[
'question' => 'Apakah hasil diagnosis akurat?',
'answer' => 'Hasil diagnosis dari PawMedic adalah sebagai panduan awal berdasarkan gejala yang Anda pilih. Untuk diagnosis yang akurat dan penanganan yang tepat, sangat disarankan untuk berkonsultasi langsung dengan dokter hewan profesional. PawMedic tidak menggantikan konsultasi medis profesional.',
],
[
'question' => 'Apakah data saya aman?',
'answer' => 'Ya, data yang Anda masukkan hanya digunakan untuk keperluan diagnosis dan tidak dibagikan kepada pihak ketiga.',
],
[
'question' => 'Berapa banyak gejala yang harus dipilih?',
'answer' => 'Pilih gejala yang benar-benar Anda amati. Semakin relevan gejala yang dipilih, semakin baik hasil analisis.',
],
];
}
private function loadFaqItems(): array
{
$path = $this->faqPath();
if (!file_exists($path)) {
return $this->defaultFaqItems();
}
$decoded = json_decode((string) file_get_contents($path), true);
if (!is_array($decoded) || empty($decoded)) {
return $this->defaultFaqItems();
}
$faqs = [];
foreach ($decoded as $item) {
$q = trim((string)($item['question'] ?? ''));
$a = trim((string)($item['answer'] ?? ''));
if ($q === '' || $a === '') {
continue;
}
$faqs[] = ['question' => $q, 'answer' => $a];
}
return !empty($faqs) ? $faqs : $this->defaultFaqItems();
}
}

View File

@ -71,7 +71,29 @@ public function prosesDiagnosis(Request $request)
// 🔥 halaman hasil
public function hasil()
{
return view('hasil-diagnosis');
$diagnosis = session('diagnosis', []);
$diseaseName = trim((string)($diagnosis['nama'] ?? ''));
$description = $this->getDiseaseDescription($diseaseName);
$history = collect();
$biodataId = session('biodata_id');
if ($biodataId) {
$current = Biodata::find($biodataId);
$phone = trim((string)($current->no_telepon ?? ''));
if ($phone !== '') {
$history = Biodata::query()
->where('no_telepon', $phone)
->whereNotNull('hasil_diagnosis')
->orderByDesc('created_at')
->take(10)
->get(['nama_kucing', 'hasil_diagnosis', 'created_at']);
}
}
return view('hasil-diagnosis', [
'diseaseDescription' => $description,
'diagnosisHistory' => $history,
]);
}
public function simpanBiodata(Request $request)
@ -99,4 +121,29 @@ public function simpanBiodata(Request $request)
return redirect()->route('gejala');
}
private function getDiseaseDescription(string $diseaseName): string
{
if ($diseaseName === '') {
return '';
}
$path = storage_path('app/disease_explanations.json');
if (!file_exists($path)) {
return '';
}
$decoded = json_decode((string)file_get_contents($path), true);
if (!is_array($decoded)) {
return '';
}
foreach ($decoded as $name => $description) {
if (trim((string)$name) === $diseaseName) {
return trim((string)$description);
}
}
return '';
}
}

View File

@ -140,6 +140,24 @@
letter-spacing:-0.02em;
}
.admin-shortcuts{
display:flex;
gap:10px;
flex-wrap:wrap;
margin-bottom:22px;
}
.admin-shortcut{
text-decoration:none;
padding:9px 14px;
border-radius:999px;
border:1px solid rgba(111,207,151,0.35);
background:#fff;
color:var(--text-dark);
font-weight:600;
font-size:13px;
}
/* ===== STATS GRID ===== */
.stats-grid {
display: grid;
@ -238,6 +256,41 @@
gap:12px;
}
.table-wrap{
width:100%;
max-height:420px;
overflow:auto;
border:1px solid #e2e8f0;
border-radius:14px;
}
.table-controls{
display:flex;
gap:10px;
margin-bottom:12px;
flex-wrap:wrap;
}
.form-control{
padding:10px 12px;
border:1px solid #cbd5e1;
border-radius:10px;
background:#fff;
font-size:14px;
min-width:180px;
}
.btn-export{
padding:10px 14px;
border-radius:10px;
border:1px solid var(--primary);
background:var(--primary-light);
color:var(--text-dark);
text-decoration:none;
font-weight:600;
font-size:14px;
}
.table{
width:100%;
border-collapse:collapse;
@ -342,6 +395,12 @@
<!-- MAIN CONTENT -->
<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>
</div>
<!-- Statistics -->
<div class="stats-grid">
@ -443,45 +502,44 @@
<div class="data-section">
<div class="section-title">📋 Data Diagnosis</div>
<div style="display:flex; gap:10px; margin-bottom:10px; flex-wrap:wrap;">
<input type="text" id="searchDiagnosis"
class="form-control"
placeholder="🔍 Cari..." style="max-width:200px;">
<select id="filterDiagnosis" class="form-control" style="max-width:200px;">
<div class="table-controls">
<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)
<option value="{{ strtolower($penyakit) }}">{{ $penyakit }}</option>
@endforeach
</select>
<form method="GET">
<select id="sortDiagnosis">
<select id="sortDiagnosis" class="form-control">
<option value="latest">Terbaru</option>
<option value="oldest">Terlama</option>
<option value="name_asc">Nama Pemilik A-Z</option>
<option value="name_desc">Nama Pemilik Z-A</option>
</select>
</form>
<a href="{{ route('admin.export.diagnosis') }}" class="btn-export"> Export Excel</a>
</div>
<div style="max-height:400px; overflow-y:auto;">
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>Nama Pemilik</th>
<th>Nama Kucing</th>
<th>Umur Kucing</th>
<th>Jenis Kelamin</th>
<th>Penyakit</th>
<th>Tanggal</th>
</tr>
</thead>
<tbody id="diagnosisTable">
@foreach($data as $user)
<tr data-date="{{ $user->created_at }}">
<td>{{ $user->nama_pemilik }}</td>
<td>{{ $user->nama_kucing }}</td>
<td>{{ $user->hasil_diagnosis ?? '-' }}</td>
<td>{{ \Carbon\Carbon::parse($user->created_at)->format('d M Y') }}</td>
@foreach($data as $item)
<tr data-date="{{ $item->created_at }}" data-name="{{ strtolower($item->nama_pemilik ?? '') }}" data-disease="{{ strtolower($item->hasil_diagnosis ?? '') }}">
<td>{{ $item->nama_pemilik }}</td>
<td>{{ $item->nama_kucing }}</td>
<td>{{ $item->umur_kucing ?? '-' }}</td>
<td>{{ $item->jenis_kelamin ?? '-' }}</td>
<td>{{ $item->hasil_diagnosis ?? '-' }}</td>
<td>{{ \Carbon\Carbon::parse($item->created_at)->format('d M Y') }}</td>
</tr>
@endforeach
</tbody>
@ -493,28 +551,41 @@ class="form-control"
<div id="userBox" style="display:none; margin-top:20px;">
<div class="data-section">
<div class="section-title">👥 Data Pengguna</div>
<div class="table-controls">
<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>
<option value="name_asc">Nama Pemilik A-Z</option>
<option value="name_desc">Nama Pemilik Z-A</option>
</select>
</div>
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>Tanggal</th>
<th>Nama Pemilik</th>
<th>No Telepon</th>
<th>Alamat</th>
<th>Nama Kucing</th>
</tr>
</thead>
<tbody>
@foreach($data ?? [] as $user)
<tr>
<td>{{ $user->nama_pemilik }}</td>
<td>{{ $user->no_telepon }}</td>
<td>{{ $user->alamat ?? 'Tidak tersedia'}}</td>
<td>{{ $user->nama_kucing }}</td>
<tbody id="userTable">
@foreach(($userData ?? collect()) as $item)
<tr data-date="{{ $item->created_at }}" data-name="{{ strtolower($item->nama_pemilik ?? '') }}">
<td>{{ \Carbon\Carbon::parse($item->created_at)->format('d M Y') }}</td>
<td>{{ $item->nama_pemilik }}</td>
<td>{{ $item->no_telepon }}</td>
<td>{{ $item->alamat ?? 'Tidak tersedia'}}</td>
<td>{{ $item->nama_kucing }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
<!-- Recent Diagnosis -->
<div class="data-section">
@ -560,6 +631,12 @@ class="form-control"
<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
</a>
</div>
</div>
</div>
@ -567,182 +644,158 @@ class="form-control"
@include('components.scroll-top')
<script>
let currentPage = 1;
const rowsPerPage = 20;
let allRows = [];
const diagnosisBox = document.getElementById('diagnosisBox');
const userBox = document.getElementById('userBox');
const chartBox = document.getElementById('chartBox');
const diagnosisTableBody = document.getElementById('diagnosisTable');
const diagnosisRows = Array.from(diagnosisTableBody.querySelectorAll('tr'));
// SEARCH + FILTER + SORT
function applyFilters() {
let search = document.getElementById("searchDiagnosis").value.toLowerCase();
let filter = document.getElementById("filterDiagnosis").value;
const userTableBody = document.getElementById('userTable');
const userRows = Array.from(userTableBody.querySelectorAll('tr'));
let rows = Array.from(document.querySelectorAll("#diagnosisTable tr"));
function sortRows(rows, sortType, nameAttr = 'data-name') {
const cloned = [...rows];
cloned.sort((a, b) => {
if (sortType === 'oldest') {
return new Date(a.getAttribute('data-date')) - new Date(b.getAttribute('data-date'));
}
if (sortType === 'name_asc') {
return (a.getAttribute(nameAttr) || '').localeCompare((b.getAttribute(nameAttr) || ''));
}
if (sortType === 'name_desc') {
return (b.getAttribute(nameAttr) || '').localeCompare((a.getAttribute(nameAttr) || ''));
}
return new Date(b.getAttribute('data-date')) - new Date(a.getAttribute('data-date'));
});
return cloned;
}
let filtered = rows.filter(row => {
let text = row.innerText.toLowerCase();
let penyakit = row.children[2].innerText.toLowerCase();
function applyDiagnosisFilters() {
const search = (document.getElementById('searchDiagnosis').value || '').toLowerCase();
const diseaseFilter = document.getElementById('filterDiagnosis').value;
const sort = document.getElementById('sortDiagnosis').value;
return text.includes(search) &&
(filter === "" || penyakit === filter);
const filtered = diagnosisRows.filter((row) => {
const text = row.innerText.toLowerCase();
const disease = row.getAttribute('data-disease') || '';
return text.includes(search) && (diseaseFilter === '' || disease === diseaseFilter);
});
// SEMUA DIHIDE DULU
rows.forEach(row => row.style.display = "none");
// TAMPILKAN HASIL
filtered.forEach(row => row.style.display = "");
}
// TAMPILKAN DATA
function displayRows(rows) {
let start = (currentPage - 1) * rowsPerPage;
let end = start + rowsPerPage;
let visible = rows.slice(start, end);
document.getElementById("tableBody").innerHTML = "";
visible.forEach(row => {
document.getElementById("tableBody").appendChild(row);
});
document.getElementById("pageInfo").innerText =
`Page ${currentPage}`;
const sorted = sortRows(filtered, sort);
diagnosisTableBody.innerHTML = '';
sorted.forEach((row) => diagnosisTableBody.appendChild(row));
}
// PAGINATION
function nextPage() {
currentPage++;
applyFilters();
function applyUserFilters() {
const search = (document.getElementById('searchUser').value || '').toLowerCase();
const sort = document.getElementById('sortUser').value;
const filtered = userRows.filter((row) => row.innerText.toLowerCase().includes(search));
const sorted = sortRows(filtered, sort);
userTableBody.innerHTML = '';
sorted.forEach((row) => userTableBody.appendChild(row));
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
applyFilters();
function toggleChart() {
const isHidden = window.getComputedStyle(chartBox).display === 'none';
if (isHidden) {
chartBox.style.display = 'block';
chartBox.scrollIntoView({ behavior: 'smooth' });
loadMainChart();
loadRatingChart();
} else {
chartBox.style.display = 'none';
}
}
function toggleUsers() {
chartBox.style.display = 'none';
const hidden = window.getComputedStyle(userBox).display === 'none';
userBox.style.display = hidden ? 'block' : 'none';
if (hidden) userBox.scrollIntoView({ behavior: 'smooth' });
}
function toggleDiagnosis() {
chartBox.style.display = 'none';
userBox.style.display = 'none';
const hidden = window.getComputedStyle(diagnosisBox).display === 'none';
diagnosisBox.style.display = hidden ? 'block' : 'none';
if (hidden) diagnosisBox.scrollIntoView({ behavior: 'smooth' });
}
</script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels"></script>
<script>
window.onload = function () {
allRows = Array.from(document.querySelectorAll("#tableBody tr"));
applyFilters();
let diseaseChart = null;
let ratingChart = null;
document.getElementById("searchDiagnosis").addEventListener("keyup", () => {
currentPage = 1;
applyFilters();
function loadMainChart() {
if (diseaseChart) return;
const ctx = document.getElementById('chartPenyakit');
diseaseChart = new Chart(ctx, {
type: 'bar',
data: {
labels: {!! json_encode($stats['chart_labels']) !!},
datasets: [{
label: 'Jumlah Kasus',
data: {!! json_encode($stats['chart_data']) !!},
backgroundColor: '#6fcf97',
borderColor: '#4bb66f',
borderWidth: 1.5,
borderRadius: 8
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: { mode: 'index', intersect: false }
},
scales: {
y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: '#e5e7eb' } },
x: { grid: { display: false } }
}
}
});
}
document.getElementById("filterDiagnosis").addEventListener("change", () => {
currentPage = 1;
applyFilters();
});
document.getElementById("sortDiagnosis").addEventListener("change", () => {
currentPage = 1;
applyFilters();
});
};
const ctx2 = document.getElementById('chartHarian');
new Chart(ctx2, {
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.4,
fill: false,
tension: 0.35,
fill: true,
backgroundColor: 'rgba(111, 207, 151, 0.2)',
borderColor: '#4bb66f',
borderWidth: 3,
pointRadius: 5
}]
}
});
let chart = null;
function toggleChart() {
const chartBox = document.getElementById('chartBox');
const isHidden = window.getComputedStyle(chartBox).display === "none";
if (isHidden) {
chartBox.style.display = "block";
chartBox.scrollIntoView({ behavior: 'smooth' });
if (!chart) {
const ctx = document.getElementById('chartPenyakit');
chart = new Chart(ctx, {
type: 'bar',
data: {
labels: {!! json_encode($stats['chart_labels']) !!},
datasets: [{
label: 'Jumlah Kasus',
data: {!! json_encode($stats['chart_data']) !!}
pointRadius: 4,
pointBackgroundColor: '#4bb66f'
}]
},
options: {
plugins: {
legend: { display: false }
responsive: true,
plugins: { legend: { display: true } },
scales: {
y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: '#e5e7eb' } },
x: { grid: { display: false } }
}
}
});
}
loadRatingChart();
} else {
chartBox.style.display = "none";
}
}
function toggleUsers() {
const userBox = document.getElementById('userBox');
const chartBox = document.getElementById('chartBox');
chartBox.style.display = "none"; // tutup chart
if (userBox.style.display === "none") {
userBox.style.display = "block";
userBox.scrollIntoView({ behavior: 'smooth' });
} else {
userBox.style.display = "none";
}
}
function toggleDiagnosis() {
const diagnosisBox = document.getElementById('diagnosisBox');
const chartBox = document.getElementById('chartBox');
const userBox = document.getElementById('userBox');
// tutup yang lain biar rapi
chartBox.style.display = "none";
userBox.style.display = "none";
if (diagnosisBox.style.display === "none") {
diagnosisBox.style.display = "block";
diagnosisBox.scrollIntoView({ behavior: 'smooth' });
} else {
diagnosisBox.style.display = "none";
}
}
let ratingChart = null;
function loadRatingChart() {
if (ratingChart) return;
const ctx = document.getElementById('chartRating');
ratingChart = new Chart(ctx, {
ratingChart = new Chart(document.getElementById('chartRating'), {
type: 'pie',
data: {
labels: {!! json_encode($stats['rating_labels']) !!}.map(r => 'Bintang ' + r),
datasets: [{
data: {!! json_encode($stats['rating_data']) !!}
data: {!! json_encode($stats['rating_data']) !!},
backgroundColor: ['#22c55e', '#84cc16', '#f59e0b', '#f97316', '#ef4444']
}]
},
options: {
@ -750,53 +803,27 @@ function loadRatingChart() {
datalabels: {
color: '#fff',
formatter: (value, context) => {
let total = context.dataset.data.reduce((a, b) => a + b, 0);
let percent = (value / total * 100).toFixed(1);
return percent + '%';
const total = context.dataset.data.reduce((a, b) => a + b, 0);
if (!total) return '0%';
return ((value / total) * 100).toFixed(1) + '%';
},
font: {
weight: 'bold'
}
font: { weight: 'bold' }
},
legend: {
position: 'bottom'
}
legend: { position: 'bottom' }
}
},
plugins: [ChartDataLabels]
});}
</script>
<script>
document.getElementById('sortDiagnosis').addEventListener('change', function() {
let sort = this.value;
let table = document.getElementById('diagnosisTable');
// loading dulu
table.innerHTML = "<tr><td colspan='4'>Loading...</td></tr>";
fetch(`/admin/sort-diagnosis?sort=${sort}`)
.then(response => response.json())
.then(data => {
table.innerHTML = '';
data.forEach(item => {
table.innerHTML += `
<tr>
<td>${item.nama_pemilik}</td>
<td>${item.nama_kucing}</td>
<td>${item.hasil_diagnosis ?? '-'}</td>
<td>${new Date(item.created_at).toLocaleDateString()}</td>
</tr>
`;
});
}
})
.catch(error => {
table.innerHTML = "<tr><td colspan='4'>Error load data</td></tr>";
console.error(error);
});
});
document.getElementById('searchDiagnosis').addEventListener('input', applyDiagnosisFilters);
document.getElementById('filterDiagnosis').addEventListener('change', applyDiagnosisFilters);
document.getElementById('sortDiagnosis').addEventListener('change', applyDiagnosisFilters);
document.getElementById('searchUser').addEventListener('input', applyUserFilters);
document.getElementById('sortUser').addEventListener('change', applyUserFilters);
applyDiagnosisFilters();
applyUserFilters();
</script>
</body>
</html>

View File

@ -0,0 +1,106 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pengaturan Penjelasan Penyakit - 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">
<style>
:root{
--ff-heading:'Poppins',system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial;
--ff-body:'Inter',system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial;
--primary:#6fcf97;
--primary-dark:#4bb66f;
--primary-light:#e8f7ef;
--text-dark:#114d3a;
--text-muted:#64748b;
}
*{box-sizing:border-box;}
body{
margin:0;
font-family:var(--ff-body);
background:linear-gradient(135deg,#f0fdf4 0%,#eaf7f0 50%,#f0f9ff 100%);
color:#333;
}
.container{max-width:1200px;margin:0 auto;padding:32px 20px;}
.topbar{
display:flex;justify-content:space-between;align-items:center;gap:12px;
margin-bottom:18px;
}
.title{font-family:var(--ff-heading);color:var(--text-dark);font-size:30px;font-weight:800;margin:0;}
.muted{color:var(--text-muted);margin:6px 0 0;}
.back{
text-decoration:none;padding:10px 14px;border-radius:10px;border:1px solid var(--primary);
background:#fff;color:var(--text-dark);font-weight:600;
}
.card{
background:rgba(255,255,255,.95);border:1px solid rgba(111,207,151,.2);border-radius:18px;
box-shadow:0 8px 24px rgba(17,77,58,.1);padding:20px;
}
.notice{
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;
}
.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);}
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;
}
.actions{margin-top:16px;display:flex;justify-content:flex-end;}
.btn{
border:none;border-radius:12px;padding:12px 18px;font-weight:700;cursor:pointer;
background:linear-gradient(135deg,var(--primary),var(--primary-dark));color:#fff;
}
</style>
</head>
<body>
<div class="container">
<div class="topbar">
<div>
<h1 class="title">Pengaturan Penjelasan Penyakit</h1>
<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>
</div>
<div class="card">
@if(session('success'))
<div class="notice">{{ session('success') }}</div>
@endif
<form method="POST" action="{{ route('admin.disease.settings.save') }}">
@csrf
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:32%;">Nama Penyakit</th>
<th>Penjelasan</th>
</tr>
</thead>
<tbody>
@forelse($diseases as $disease)
<tr>
<td><strong>{{ $disease }}</strong></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>
@endforelse
</tbody>
</table>
</div>
<div class="actions">
<button type="submit" class="btn">Simpan Penjelasan</button>
</div>
</form>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,71 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<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">
<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{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}
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}
.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}}
</style>
</head>
<body>
<div class="container">
<div class="head">
<h1 class="title">Kelola FAQ</h1>
<a class="btn secondary" href="{{ route('admin.dashboard') }}"> Dashboard</a>
</div>
<div class="card">
@if(session('success'))
<div class="notice">{{ session('success') }}</div>
@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>
<div class="actions">
<button type="button" class="btn secondary" onclick="addFaqRow()">+ Tambah FAQ</button>
<button type="submit" class="btn">Simpan FAQ</button>
</div>
</form>
</div>
</div>
<script>
function addFaqRow() {
const wrap = document.getElementById('faqRows');
const row = document.createElement('div');
row.className = 'row';
row.innerHTML = `
<input type="text" name="questions[]" placeholder="Pertanyaan">
<textarea name="answers[]" placeholder="Jawaban"></textarea>
`;
wrap.appendChild(row);
}
</script>
</body>
</html>

View File

@ -226,60 +226,17 @@
</div>
<div class="faq-list">
@forelse(($faqs ?? []) as $faq)
<div class="faq-card">
<div class="faq-question">Apa itu PawMedic?</div>
<div class="faq-answer">
PawMedic adalah aplikasi sistem pakar yang membantu pemilik kucing memahami gejala dan mendapatkan rekomendasi perawatan awal. Aplikasi ini menggunakan metode sistem pakar untuk menganalisis gejala yang dipilih dan memberikan diagnosis kemungkinan penyakit.
<div class="faq-question">{{ $faq['question'] ?? '-' }}</div>
<div class="faq-answer" style="white-space: pre-line;">{{ $faq['answer'] ?? '-' }}</div>
</div>
</div>
@empty
<div class="faq-card">
<div class="faq-question">Bagaimana cara menggunakan PawMedic?</div>
<div class="faq-answer">
Cara menggunakan PawMedic sangat mudah:
<ol style="margin:12px 0; padding-left:20px;">
<li>Isi biodata kucing Anda</li>
<li>Pilih gejala yang Anda amati pada kucing</li>
<li>Sistem akan menganalisis dan memberikan hasil diagnosis</li>
<li>Baca rekomendasi perawatan yang diberikan</li>
</ol>
</div>
</div>
<div class="faq-card">
<div class="faq-question">Apakah hasil diagnosis akurat?</div>
<div class="faq-answer">
Hasil diagnosis dari PawMedic adalah sebagai panduan awal berdasarkan gejala yang Anda pilih. Untuk diagnosis yang akurat dan penanganan yang tepat, sangat disarankan untuk berkonsultasi langsung dengan dokter hewan profesional. PawMedic tidak menggantikan konsultasi medis profesional.
</div>
</div>
<div class="faq-card">
<div class="faq-question">Apakah data saya aman?</div>
<div class="faq-answer">
Ya, data yang Anda masukkan hanya digunakan untuk keperluan diagnosis dan tidak dibagikan kepada pihak ketiga. Semua data disimpan secara lokal di browser Anda (sessionStorage) dan tidak dikirim ke server kecuali untuk keperluan analisis diagnosis.
</div>
</div>
<div class="faq-card">
<div class="faq-question">Berapa banyak gejala yang harus dipilih?</div>
<div class="faq-answer">
Anda dapat memilih sebanyak mungkin gejala yang sesuai dengan kondisi kucing Anda. Semakin banyak gejala yang dipilih, semakin akurat diagnosis yang akan diberikan. Namun, pastikan gejala yang dipilih benar-benar Anda amati pada kucing.
</div>
</div>
<div class="faq-card">
<div class="faq-question">Apakah aplikasi ini gratis?</div>
<div class="faq-answer">
Ya, PawMedic sepenuhnya gratis untuk digunakan. Anda dapat melakukan diagnosis tanpa batas dan mengakses semua fitur yang tersedia tanpa biaya apapun.
</div>
</div>
<div class="faq-card">
<div class="faq-question">Bagaimana jika kucing saya dalam kondisi darurat?</div>
<div class="faq-answer">
Jika kucing Anda menunjukkan tanda-tanda darurat seperti kesulitan bernapas, kejang, tidak sadar, atau luka parah, segera bawa ke dokter hewan terdekat atau klinik hewan darurat. Jangan menunggu diagnosis dari aplikasi ini.
</div>
<div class="faq-question">Belum ada FAQ</div>
<div class="faq-answer">Konten FAQ belum tersedia.</div>
</div>
@endforelse
</div>
</div>

View File

@ -203,6 +203,17 @@
font-weight:600;
}
.disease-explanation{
margin-top:14px;
background:#fff;
border:1px solid #d1fae5;
border-radius:12px;
padding:14px 16px;
color:#0f5132;
font-size:14px;
line-height:1.7;
}
/* ===== GEJALA LIST ===== */
.gejala-list-section{
margin-bottom:32px;
@ -327,6 +338,32 @@
line-height:1.7;
}
.history-section{
margin-bottom:32px;
}
.history-table{
width:100%;
border-collapse:collapse;
background:#fff;
border:1px solid #d1fae5;
border-radius:12px;
overflow:hidden;
}
.history-table th,
.history-table td{
padding:10px 12px;
border-bottom:1px solid #e2e8f0;
text-align:left;
font-size:14px;
}
.history-table th{
background:#f0fdf4;
color:#0f5132;
}
/* ===== BUTTONS ===== */
.action-buttons{
display:flex;
@ -450,6 +487,12 @@
<div class="diagnosis-category">
Jenis: {{ $diagnosis['kategori'] ?? '-' }}
</div>
@if(!empty($diseaseDescription))
<div class="disease-explanation">
<strong>Penjelasan penyakit:</strong><br>
{{ $diseaseDescription }}
</div>
@endif
<!-- Gejala yang Dipilih -->
@ -516,13 +559,85 @@
</div>
</div>
@if(isset($diagnosisHistory) && $diagnosisHistory->count() > 0)
<div class="result-card history-section">
<div class="section-title">
<span>🕘 Riwayat Diagnosis (Nomor yang sama)</span>
</div>
<table class="history-table">
<thead>
<tr>
<th>Tanggal</th>
<th>Nama Kucing</th>
<th>Hasil Diagnosis</th>
</tr>
</thead>
<tbody>
@foreach($diagnosisHistory as $row)
<tr>
<td>{{ \Carbon\Carbon::parse($row->created_at)->format('d M Y H:i') }}</td>
<td>{{ $row->nama_kucing ?? '-' }}</td>
<td>{{ $row->hasil_diagnosis ?? '-' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
@include('components.scroll-top')
<script>
// Print function
function printDiagnosis() {
window.print();
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();
}
// Share function

View File

@ -83,6 +83,16 @@
gap:18px;
align-items:center;
}
.menu-toggle{
display:none;
border:1px solid #d1d5db;
background:#fff;
border-radius:10px;
padding:8px 10px;
font-size:20px;
cursor:pointer;
color:#114d3a;
}
.nav-menu a{
text-decoration:none;
color:#555;
@ -471,6 +481,26 @@
/* ===== RESPONSIVE ===== */
@media(max-width:900px){
.container{
padding:24px;
}
.navbar{
flex-direction:column;
align-items:flex-start;
gap:14px;
margin-bottom:28px;
}
.nav-menu{
width:100%;
flex-wrap:wrap;
gap:10px;
}
.nav-menu a{
font-size:14px;
}
.nav-menu .btn{
margin-left:auto;
}
.hero{
flex-direction:column;
text-align:center;
@ -503,9 +533,56 @@
.features{
grid-template-columns:repeat(2,1fr);
}
section{
margin-top:72px;
}
#diagnosa{
padding:24px;
}
}
@media(max-width:500px){
.container{
padding:16px;
}
.logo-text{
font-size:18px;
}
.navbar{
padding:12px 0;
align-items:stretch;
}
.menu-toggle{
display:inline-flex;
align-items:center;
justify-content:center;
align-self:flex-end;
}
.nav-menu{
display:none;
grid-template-columns:1fr 1fr;
width:100%;
}
.nav-menu.open{
display:grid;
}
.nav-menu a{
text-align:center;
padding:8px 6px;
border-radius:8px;
background:#fff;
border:1px solid #eef5f3;
}
.nav-menu .btn{
grid-column:1 / -1;
width:100%;
margin-left:0;
}
.hero{
padding:28px 18px 32px;
gap:20px;
border-radius:18px;
}
.features{
grid-template-columns:1fr;
}
@ -516,6 +593,33 @@
max-width:320px;
width:100%;
}
.hero-actions{
width:100%;
flex-direction:column;
}
.hero-actions .btn{
width:100%;
min-width:unset;
}
section{
margin-top:56px;
}
section > p{
font-size:15px;
margin-bottom:24px;
}
.card.feature{
padding:22px 18px;
min-height:unset;
}
footer{
margin-top:44px;
padding-bottom:42px;
}
.admin-login-link{
bottom:2px;
right:2px;
}
}
</style>
</head>
@ -529,7 +633,8 @@
<div class="logo-icon">🐾</div>
<div class="logo-text">PawMedic</div>
</div>
<div class="nav-menu">
<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>
@ -649,6 +754,15 @@ function scrollToSection(id){
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>
</body>

View File

@ -8,6 +8,21 @@
use App\Models\Ulasan;
Route::get('/admin/sort-diagnosis', [AdminController::class, 'sortDiagnosis']);
Route::get('/admin/export-diagnosis', [AdminController::class, 'exportDiagnosisExcel'])
->name('admin.export.diagnosis')
->middleware('auth');
Route::get('/admin/disease-settings', [AdminController::class, 'diseaseSettings'])
->name('admin.disease.settings')
->middleware('auth');
Route::post('/admin/disease-settings', [AdminController::class, 'saveDiseaseSettings'])
->name('admin.disease.settings.save')
->middleware('auth');
Route::get('/admin/faq-settings', [AdminController::class, 'faqSettings'])
->name('admin.faq.settings')
->middleware('auth');
Route::post('/admin/faq-settings', [AdminController::class, 'saveFaqSettings'])
->name('admin.faq.settings.save')
->middleware('auth');
Route::delete('/ulasan/{id}', [UlasanController::class, 'destroy'])->name('ulasan.delete');
@ -36,9 +51,7 @@
Route::get('/hasil-diagnosis', [DiagnosisController::class, 'hasil'])->name('hasil-diagnosis');
Route::get('/faq', function () {
return view('faq');
})->name('faq');
Route::get('/faq', [AdminController::class, 'faqPage'])->name('faq');
// Admin Routes
Route::get('/admin/login', [AdminController::class, 'login'])->name('admin.login');