Update Dashboard penambahan fitur
This commit is contained in:
parent
ccdd44927b
commit
3564fc3fb8
|
|
@ -7,6 +7,9 @@
|
|||
use Illuminate\Support\Facades\Hash;
|
||||
use App\Models\Biodata;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonPeriod;
|
||||
use App\Models\Ulasan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
|
|
@ -39,15 +42,29 @@ public function authenticate(Request $request)
|
|||
|
||||
public function dashboard()
|
||||
{
|
||||
|
||||
// total diagnosis
|
||||
$totalDiagnosis = Biodata::count();
|
||||
|
||||
// hari ini
|
||||
$todayDiagnosis = Biodata::whereDate('created_at', Carbon::today())->count();
|
||||
|
||||
// kemarin
|
||||
$yesterday = Biodata::whereDate('created_at', Carbon::yesterday())->count();
|
||||
$diff = $todayDiagnosis - $yesterday;
|
||||
|
||||
// total user
|
||||
$totalUsers = Biodata::count();
|
||||
|
||||
// user list
|
||||
$sort = request('sort');
|
||||
|
||||
if ($sort == 'oldest') {
|
||||
$data = Biodata::orderBy('created_at', 'asc')->get();
|
||||
} else {
|
||||
$data = Biodata::orderBy('created_at', 'desc')->get();
|
||||
}
|
||||
|
||||
// penyakit paling umum
|
||||
$mostCommon = Biodata::select('hasil_diagnosis')
|
||||
->whereNotNull('hasil_diagnosis')
|
||||
|
|
@ -55,13 +72,20 @@ public function dashboard()
|
|||
->orderByRaw('COUNT(*) DESC')
|
||||
->value('hasil_diagnosis');
|
||||
|
||||
// penyakit terbanyak hari ini
|
||||
$todayDisease = Biodata::whereDate('created_at', Carbon::today())
|
||||
->select('hasil_diagnosis')
|
||||
->whereNotNull('hasil_diagnosis')
|
||||
->groupBy('hasil_diagnosis')
|
||||
->orderByRaw('COUNT(*) DESC')
|
||||
->value('hasil_diagnosis');
|
||||
|
||||
// diagnosis terbaru
|
||||
$recent = Biodata::select('hasil_diagnosis', 'created_at')
|
||||
->latest()
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
// format tabel
|
||||
$recentFormatted = $recent->map(function ($item) {
|
||||
return [
|
||||
'date' => $item->created_at,
|
||||
|
|
@ -70,7 +94,7 @@ public function dashboard()
|
|||
];
|
||||
});
|
||||
|
||||
// 🔥 CHART (HARUS DI LUAR MAP)
|
||||
// chart penyakit
|
||||
$diseaseStats = Biodata::select('hasil_diagnosis')
|
||||
->whereNotNull('hasil_diagnosis')
|
||||
->get()
|
||||
|
|
@ -82,6 +106,19 @@ public function dashboard()
|
|||
$chartLabels = $diseaseStats->keys()->values();
|
||||
$chartData = $diseaseStats->values();
|
||||
|
||||
// 🔥 7 hari terakhir
|
||||
$period = CarbonPeriod::create(Carbon::now()->subDays(6), Carbon::now());
|
||||
|
||||
$dailyLabels = [];
|
||||
$dailyData = [];
|
||||
|
||||
foreach ($period as $date) {
|
||||
$count = Biodata::whereDate('created_at', $date)->count();
|
||||
|
||||
$dailyLabels[] = $date->format('d M');
|
||||
$dailyData[] = $count;
|
||||
}
|
||||
|
||||
// kirim ke blade
|
||||
$stats = [
|
||||
'total_diagnosis' => $totalDiagnosis,
|
||||
|
|
@ -90,10 +127,22 @@ public function dashboard()
|
|||
'most_common_disease' => $mostCommon,
|
||||
'recent_diagnosis' => $recentFormatted,
|
||||
'chart_labels' => $chartLabels,
|
||||
'chart_data' => $chartData
|
||||
'chart_data' => $chartData,
|
||||
'diagnosis_diff' => $diff,
|
||||
'today_top_disease' => $todayDisease,
|
||||
'daily_labels' => $dailyLabels,
|
||||
'daily_data' => $dailyData
|
||||
];
|
||||
// 🔥 STAT
|
||||
$ratingChart = Ulasan::select('rating', DB::raw('count(*) as total'))
|
||||
->groupBy('rating')
|
||||
->orderBy('rating')
|
||||
->get();
|
||||
|
||||
return view('admin.dashboard', compact('stats'));
|
||||
$stats['rating_labels'] = $ratingChart->pluck('rating');
|
||||
$stats['rating_data'] = $ratingChart->pluck('total');
|
||||
|
||||
return view('admin.dashboard', compact('stats', 'data'));
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
|
|
@ -120,4 +169,31 @@ private function getStatistics()
|
|||
]
|
||||
];
|
||||
}
|
||||
public function statistik()
|
||||
{
|
||||
$diseaseStats = Biodata::select('hasil_diagnosis')
|
||||
->whereNotNull('hasil_diagnosis')
|
||||
->get()
|
||||
->groupBy('hasil_diagnosis')
|
||||
->map(function ($item) {
|
||||
return count($item);
|
||||
});
|
||||
|
||||
$chartLabels = $diseaseStats->keys()->values();
|
||||
$chartData = $diseaseStats->values();
|
||||
|
||||
return view('admin.statistik', compact('chartLabels', 'chartData'));
|
||||
}
|
||||
public function sortDiagnosis(Request $request)
|
||||
{
|
||||
$sort = $request->sort;
|
||||
|
||||
if ($sort == 'oldest') {
|
||||
$data = Biodata::orderBy('created_at', 'asc')->get();
|
||||
} else {
|
||||
$data = Biodata::orderBy('created_at', 'desc')->get();
|
||||
}
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Ulasan;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class UlasanController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$ulasan = Ulasan::latest()->get();
|
||||
// 🔥 TOTAL ULASAN
|
||||
$total = $ulasan->count();
|
||||
// 🔥 RATING RATA-RATA
|
||||
$avg = $ulasan->avg('rating') ?? 0;
|
||||
// 🔥 5 BINTANG %
|
||||
$fiveStar = $ulasan->where('rating', 5)->count();
|
||||
$fivePercent = $total > 0 ? round(($fiveStar / $total) * 100) : 0;
|
||||
|
||||
return view('ulasan', compact('ulasan', 'total', 'avg', 'fivePercent'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
Ulasan::create($request->all());
|
||||
|
||||
return redirect()->back()->with('success', 'Ulasan berhasil dikirim!');
|
||||
}
|
||||
public function destroy($id)
|
||||
{
|
||||
if (!Auth::check() || Auth::user()->email !== 'admin@pawmedic.app') {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$ulasan = Ulasan::findOrFail($id);
|
||||
$ulasan->delete();
|
||||
|
||||
return redirect()->back()->with('success', 'Ulasan berhasil dihapus');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Ulasan extends Model
|
||||
{
|
||||
protected $table = 'ulasans';
|
||||
protected $fillable = [
|
||||
'nama',
|
||||
'nama_kucing',
|
||||
'hasil_diagnosis',
|
||||
'rating',
|
||||
'komentar'
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?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::create('ulasans', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ulasans');
|
||||
}
|
||||
};
|
||||
|
|
@ -145,6 +145,7 @@
|
|||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card{
|
||||
|
|
@ -306,6 +307,9 @@
|
|||
padding:12px 8px;
|
||||
}
|
||||
}
|
||||
#chartBox {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
@ -341,13 +345,6 @@
|
|||
|
||||
<!-- Statistics -->
|
||||
<div class="stats-grid">
|
||||
</div>
|
||||
<div id="chartBox" style="display:none; margin-top:20px;">
|
||||
<div class="data-section">
|
||||
<div class="section-title">📊 Statistik Penyakit</div>
|
||||
<canvas id="chartPenyakit"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
|
|
@ -358,6 +355,10 @@
|
|||
<div class="stat-icon">📊</div>
|
||||
</div>
|
||||
<div class="stat-change">+{{ $stats['today_diagnosis'] }} hari ini</div>
|
||||
<a href="#" onclick="toggleDiagnosis(); return false;"
|
||||
style="margin-top:10px; display:inline-block; font-size:13px; color:#4bb66f; font-weight:600;">
|
||||
Lihat Data →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
|
|
@ -369,6 +370,19 @@
|
|||
<div class="stat-icon">📈</div>
|
||||
</div>
|
||||
<div class="stat-change">Aktif hari ini</div>
|
||||
<div class="stat-change">
|
||||
@if($stats['diagnosis_diff'] > 0)
|
||||
↑ +{{ $stats['diagnosis_diff'] }} dari kemarin
|
||||
@elseif($stats['diagnosis_diff'] < 0)
|
||||
↓ {{ $stats['diagnosis_diff'] }} dari kemarin
|
||||
@else
|
||||
Sama dengan kemarin
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div style="font-size:12px; color:#64748b; margin-top:4px;">
|
||||
Terbanyak: {{ $stats['today_top_disease'] ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
|
|
@ -380,9 +394,13 @@
|
|||
<div class="stat-icon">👥</div>
|
||||
</div>
|
||||
<div class="stat-change">Pengguna aktif</div>
|
||||
<a href="#" onclick="toggleUsers(); return false;"
|
||||
style="margin-top:10px; display:inline-block; font-size:13px; color:#4bb66f; font-weight:600;">
|
||||
Lihat Data →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="stat-card" onclick="toggleChart()" style="cursor:pointer;">
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<div>
|
||||
<div class="stat-value" style="font-size:24px;">{{ $stats['most_common_disease'] }}</div>
|
||||
|
|
@ -391,6 +409,110 @@
|
|||
<div class="stat-icon">🩺</div>
|
||||
</div>
|
||||
<div class="stat-change">Paling sering didiagnosis</div>
|
||||
|
||||
<a href="#" onclick="toggleChart(); return false;"
|
||||
style="margin-top:10px; display:inline-block; font-size:13px; color:#4bb66f; font-weight:600;">
|
||||
Lihat Data →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chartBox" style="display:none; margin-top:20px;">
|
||||
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap: 20px;">
|
||||
<div class="data-section">
|
||||
<div class="section-title">📊 Statistik Penyakit</div>
|
||||
<div style="max-width:600px; margin:auto;">
|
||||
<canvas id="chartPenyakit"></canvas></div>
|
||||
</div>
|
||||
|
||||
<div class="data-section">
|
||||
<div class="section-title">⭐ Distribusi Rating Ulasan</div>
|
||||
<div style="max-width:400px; margin:auto;">
|
||||
<canvas id="chartRating" height="250"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-section">
|
||||
<div class="section-title">📈 Tren Diagnosis (7 Hari Terakhir)</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 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;">
|
||||
<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">
|
||||
<option value="latest">Terbaru</option>
|
||||
<option value="oldest">Terlama</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="max-height:400px; overflow-y:auto;">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nama Pemilik</th>
|
||||
<th>Nama Kucing</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>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="userBox" style="display:none; margin-top:20px;">
|
||||
<div class="data-section">
|
||||
<div class="section-title">👥 Data Pengguna</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -443,23 +565,120 @@
|
|||
</div>
|
||||
|
||||
@include('components.scroll-top')
|
||||
<script>
|
||||
function toggleChart() {
|
||||
const chartBox = document.getElementById('chartBox');
|
||||
|
||||
if (chartBox.style.display === "none") {
|
||||
chartBox.style.display = "block";
|
||||
} else {
|
||||
chartBox.style.display = "none";
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
const rowsPerPage = 20;
|
||||
let allRows = [];
|
||||
|
||||
|
||||
// SEARCH + FILTER + SORT
|
||||
function applyFilters() {
|
||||
let search = document.getElementById("searchDiagnosis").value.toLowerCase();
|
||||
let filter = document.getElementById("filterDiagnosis").value;
|
||||
|
||||
let rows = Array.from(document.querySelectorAll("#diagnosisTable tr"));
|
||||
|
||||
let filtered = rows.filter(row => {
|
||||
let text = row.innerText.toLowerCase();
|
||||
let penyakit = row.children[2].innerText.toLowerCase();
|
||||
|
||||
return text.includes(search) &&
|
||||
(filter === "" || penyakit === filter);
|
||||
});
|
||||
|
||||
// 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}`;
|
||||
}
|
||||
|
||||
// PAGINATION
|
||||
function nextPage() {
|
||||
currentPage++;
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
applyFilters();
|
||||
}
|
||||
}
|
||||
</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();
|
||||
|
||||
document.getElementById("searchDiagnosis").addEventListener("keyup", () => {
|
||||
currentPage = 1;
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.getElementById("filterDiagnosis").addEventListener("change", () => {
|
||||
currentPage = 1;
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.getElementById("sortDiagnosis").addEventListener("change", () => {
|
||||
currentPage = 1;
|
||||
applyFilters();
|
||||
});
|
||||
};
|
||||
const ctx2 = document.getElementById('chartHarian');
|
||||
|
||||
new Chart(ctx2, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: {!! json_encode($stats['daily_labels']) !!},
|
||||
datasets: [{
|
||||
label: 'Jumlah Diagnosis',
|
||||
data: {!! json_encode($stats['daily_data']) !!},
|
||||
tension: 0.4,
|
||||
fill: false,
|
||||
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');
|
||||
|
||||
new Chart(ctx, {
|
||||
chart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: {!! json_encode($stats['chart_labels']) !!},
|
||||
|
|
@ -467,7 +686,116 @@ function toggleChart() {
|
|||
label: 'Jumlah Kasus',
|
||||
data: {!! json_encode($stats['chart_data']) !!}
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: { 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, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: {!! json_encode($stats['rating_labels']) !!}.map(r => 'Bintang ' + r),
|
||||
datasets: [{
|
||||
data: {!! json_encode($stats['rating_data']) !!}
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
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 + '%';
|
||||
},
|
||||
font: {
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
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);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
<h2>📊 Statistik Penyakit</h2>
|
||||
|
||||
<canvas id="chartPenyakit"></canvas>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<script>
|
||||
const ctx = document.getElementById('chartPenyakit');
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: {!! json_encode($chartLabels) !!},
|
||||
datasets: [{
|
||||
label: 'Jumlah Kasus',
|
||||
data: {!! json_encode($chartData) !!}
|
||||
}]
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
@ -876,7 +876,7 @@ function updateSelectedCount() {
|
|||
form.addEventListener('submit', function(e) {
|
||||
const checked = document.querySelectorAll('.gejala-checkbox:checked').length;
|
||||
|
||||
if (checked < 5) {
|
||||
if (checked < 4) {
|
||||
e.preventDefault();
|
||||
alert("Minimal pilih 4 gejala!");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -613,54 +613,24 @@
|
|||
</a>
|
||||
</p>
|
||||
<div class="testimonials">
|
||||
@foreach($ulasan as $item)
|
||||
<div class="card testimonial">
|
||||
<div class="testimonial-head">
|
||||
<div class="avatar" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img">
|
||||
<path d="M4 10c1-3 3-4 8-4s7 1 8 4c0 5-3 7-8 7s-8-2-8-7z" stroke="#fff" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="9" cy="11" r="0.9" fill="#fff"/>
|
||||
<circle cx="15" cy="11" r="0.9" fill="#fff"/>
|
||||
</svg>
|
||||
<div class="avatar">
|
||||
{{ strtoupper(substr($item->nama, 0, 1)) }}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="rating" aria-label="5 dari 5 bintang">★★★★★</div>
|
||||
<div class="author">Siti — pemilik dari <em>Kiki</em></div>
|
||||
<div class="rating">
|
||||
{{ str_repeat('★', $item->rating) }}
|
||||
</div>
|
||||
<div class="author">
|
||||
{{ $item->nama }} — pemilik dari <em>{{ $item->nama_kucing }}</em>
|
||||
</div>
|
||||
</div>
|
||||
<p class="quote">PawMedic memberi panduan cepat yang membantu saya mengambil tindakan tepat pada kucing saya.</p>
|
||||
</div>
|
||||
<div class="card testimonial">
|
||||
<div class="testimonial-head">
|
||||
<div class="avatar" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img">
|
||||
<path d="M4 10c1-3 3-4 8-4s7 1 8 4c0 5-3 7-8 7s-8-2-8-7z" stroke="#fff" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="9" cy="11" r="0.9" fill="#fff"/>
|
||||
<circle cx="15" cy="11" r="0.9" fill="#fff"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="rating" aria-label="5 dari 5 bintang">★★★★★</div>
|
||||
<div class="author">Budi — pemilik dari <em>Cleo</em></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="quote">Aplikasinya mudah dipahami dan rekomendasinya sangat membantu sebelum pergi ke dokter hewan.</p>
|
||||
</div>
|
||||
<div class="card testimonial">
|
||||
<div class="testimonial-head">
|
||||
<div class="avatar" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img">
|
||||
<path d="M4 10c1-3 3-4 8-4s7 1 8 4c0 5-3 7-8 7s-8-2-8-7z" stroke="#fff" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="9" cy="11" r="0.9" fill="#fff"/>
|
||||
<circle cx="15" cy="11" r="0.9" fill="#fff"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="rating" aria-label="5 dari 5 bintang">★★★★★</div>
|
||||
<div class="author">Lina — pemilik dari <em>Oreo</em></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="quote">Sangat berguna! Saya merasa lebih tenang mengetahui langkah awal yang harus dilakukan.</p>
|
||||
<p class="quote">{{ $item->komentar }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -490,15 +490,15 @@
|
|||
<!-- Stats -->
|
||||
<div class="stats-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="totalReviews">12</div>
|
||||
<div class="stat-value">{{ $total }}</div>
|
||||
<div class="stat-label">Total Ulasan</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="avgRating">4.8</div>
|
||||
<div class="stat-value">{{ number_format($avg, 1) }}</div>
|
||||
<div class="stat-label">Rating Rata-rata</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="fiveStars">95%</div>
|
||||
<div class="stat-value">{{ $fivePercent }}%</div>
|
||||
<div class="stat-label">5 Bintang</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -509,29 +509,31 @@
|
|||
<span>✍️</span>
|
||||
<span>Tulis Ulasan Anda</span>
|
||||
</div>
|
||||
<form id="reviewForm">
|
||||
<form method="POST" action="{{ route('ulasan.store') }}">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label>Nama Anda</label>
|
||||
<input type="text" id="reviewName" placeholder="Masukkan nama Anda" required>
|
||||
<input type="text" name="nama" placeholder="Masukkan nama Anda" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nama Kucing</label>
|
||||
<input type="text" id="reviewCat" placeholder="Nama kucing Anda (opsional)">
|
||||
<input type="text" name="nama_kucing" placeholder="Nama kucing Anda">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Rating</label>
|
||||
<div class="rating-input">
|
||||
<span class="star" data-rating="1">★</span>
|
||||
<span class="star" data-rating="2">★</span>
|
||||
<span class="star" data-rating="3">★</span>
|
||||
<span class="star" data-rating="4">★</span>
|
||||
<span class="star" data-rating="5">★</span>
|
||||
<input type="hidden" id="ratingValue" value="0" required>
|
||||
<div id="rating-stars" style="font-size: 28px; cursor: pointer;">
|
||||
<span data-value="1">☆</span>
|
||||
<span data-value="2">☆</span>
|
||||
<span data-value="3">☆</span>
|
||||
<span data-value="4">☆</span>
|
||||
<span data-value="5">☆</span>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="rating" id="rating-value">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Ulasan</label>
|
||||
<textarea id="reviewText" placeholder="Bagikan pengalaman Anda menggunakan PawMedic..." required></textarea>
|
||||
<textarea name="komentar" placeholder="Bagikan pengalaman Anda menggunakan PawMedic..." required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span>Kirim Ulasan</span>
|
||||
|
|
@ -551,7 +553,49 @@
|
|||
<button class="filter-btn" data-filter="3">3 Bintang</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="reviews-grid" id="reviewsGrid">
|
||||
<div class="reviews-grid">
|
||||
|
||||
@foreach($ulasan as $review)
|
||||
<div class="review-card" data-rating="{{ $review->rating }}">
|
||||
<div class="review-header">
|
||||
<div class="avatar">{{ substr($review->nama,0,1) }}</div>
|
||||
<div class="review-info">
|
||||
<div class="review-name">{{ $review->nama }}</div>
|
||||
<div class="review-cat">
|
||||
{{ $review->hasil_diagnosis ?? 'Pengguna' }}
|
||||
</div>
|
||||
<div class="review-rating">
|
||||
{{ str_repeat('★', $review->rating) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ KOMENTAR UNTUK SEMUA -->
|
||||
<p class="review-text">{{ $review->komentar }}</p>
|
||||
|
||||
<!-- ✅ HANYA ADMIN -->
|
||||
@auth
|
||||
@if(Auth::user()->email === 'admin@pawmedic.app')
|
||||
<form action="{{ route('ulasan.delete', $review->id) }}"
|
||||
method="POST"
|
||||
onsubmit="return confirm('Yakin hapus ulasan ini?')"
|
||||
style="margin-top:10px;">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit"
|
||||
style="background:#ef4444; color:white; border:none; padding:6px 12px; border-radius:6px;">
|
||||
🗑 Hapus
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@endauth
|
||||
|
||||
<div class="review-date">
|
||||
{{ $review->created_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endforeach
|
||||
<!-- Reviews will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -559,182 +603,63 @@
|
|||
|
||||
@include('components.toast')
|
||||
@include('components.scroll-top')
|
||||
|
||||
<script>
|
||||
// Sample reviews data
|
||||
const reviews = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Siti',
|
||||
cat: 'Kiki',
|
||||
rating: 5,
|
||||
text: 'PawMedic memberi panduan cepat yang membantu saya mengambil tindakan tepat pada kucing saya. Sangat membantu!',
|
||||
date: '2 hari yang lalu'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Budi',
|
||||
cat: 'Cleo',
|
||||
rating: 5,
|
||||
text: 'Aplikasinya mudah dipahami dan rekomendasinya sangat membantu sebelum pergi ke dokter hewan.',
|
||||
date: '5 hari yang lalu'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Lina',
|
||||
cat: 'Oreo',
|
||||
rating: 5,
|
||||
text: 'Sangat berguna! Saya merasa lebih tenang mengetahui langkah awal yang harus dilakukan.',
|
||||
date: '1 minggu yang lalu'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Ahmad',
|
||||
cat: 'Milo',
|
||||
rating: 5,
|
||||
text: 'Sistem diagnosisnya akurat dan mudah digunakan. Sangat membantu untuk pemilik kucing pemula seperti saya.',
|
||||
date: '2 minggu yang lalu'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Dewi',
|
||||
cat: 'Luna',
|
||||
rating: 4,
|
||||
text: 'Bagus sekali aplikasinya. Interface-nya user-friendly dan informasinya lengkap.',
|
||||
date: '3 minggu yang lalu'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Rudi',
|
||||
cat: 'Max',
|
||||
rating: 5,
|
||||
text: 'PawMedic membantu saya memahami kondisi kucing dengan lebih baik. Terima kasih!',
|
||||
date: '1 bulan yang lalu'
|
||||
}
|
||||
];
|
||||
const buttons = document.querySelectorAll('.filter-btn');
|
||||
const cards = document.querySelectorAll('.review-card');
|
||||
|
||||
let currentFilter = 'all';
|
||||
|
||||
// Rating stars
|
||||
const stars = document.querySelectorAll('.star');
|
||||
const ratingInput = document.getElementById('ratingValue');
|
||||
|
||||
stars.forEach((star, index) => {
|
||||
star.addEventListener('click', () => {
|
||||
const rating = index + 1;
|
||||
ratingInput.value = rating;
|
||||
updateStars(rating);
|
||||
});
|
||||
|
||||
star.addEventListener('mouseenter', () => {
|
||||
updateStars(index + 1);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('.rating-input').addEventListener('mouseleave', () => {
|
||||
const currentRating = parseInt(ratingInput.value) || 0;
|
||||
updateStars(currentRating);
|
||||
});
|
||||
|
||||
function updateStars(rating) {
|
||||
stars.forEach((star, index) => {
|
||||
if (index < rating) {
|
||||
star.classList.add('active');
|
||||
} else {
|
||||
star.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Display reviews
|
||||
function displayReviews(filter = 'all') {
|
||||
const grid = document.getElementById('reviewsGrid');
|
||||
const filteredReviews = filter === 'all'
|
||||
? reviews
|
||||
: reviews.filter(r => r.rating === parseInt(filter));
|
||||
|
||||
grid.innerHTML = filteredReviews.map(review => `
|
||||
<div class="review-card">
|
||||
<div class="review-header">
|
||||
<div class="avatar">${review.name.charAt(0)}</div>
|
||||
<div class="review-info">
|
||||
<div class="review-name">${review.name}</div>
|
||||
<div class="review-cat">pemilik dari <em>${review.cat}</em></div>
|
||||
<div class="review-rating">${'★'.repeat(review.rating)}${'☆'.repeat(5 - review.rating)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="review-text">${review.text}</p>
|
||||
<div class="review-date">${review.date}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Filter buttons
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
buttons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||||
|
||||
// hapus active semua
|
||||
buttons.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentFilter = btn.dataset.filter;
|
||||
displayReviews(currentFilter);
|
||||
});
|
||||
});
|
||||
|
||||
// Form submission
|
||||
document.getElementById('reviewForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const filter = btn.getAttribute('data-filter');
|
||||
|
||||
const name = document.getElementById('reviewName').value;
|
||||
const cat = document.getElementById('reviewCat').value;
|
||||
const rating = parseInt(ratingInput.value);
|
||||
const text = document.getElementById('reviewText').value;
|
||||
cards.forEach(card => {
|
||||
const rating = card.getAttribute('data-rating');
|
||||
|
||||
if (!rating || rating === 0) {
|
||||
alert('Mohon berikan rating!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add new review
|
||||
const newReview = {
|
||||
id: reviews.length + 1,
|
||||
name: name,
|
||||
cat: cat || 'Kucing',
|
||||
rating: rating,
|
||||
text: text,
|
||||
date: 'Baru saja'
|
||||
};
|
||||
|
||||
reviews.unshift(newReview);
|
||||
displayReviews(currentFilter);
|
||||
updateStats();
|
||||
|
||||
// Reset form
|
||||
this.reset();
|
||||
ratingInput.value = 0;
|
||||
updateStars(0);
|
||||
|
||||
// Show toast
|
||||
if (window.showToast) {
|
||||
showToast('Terima kasih atas ulasan Anda!', 'success', 'Ulasan Terkirim');
|
||||
if (filter === 'all' || rating === filter) {
|
||||
card.style.display = 'block';
|
||||
} else {
|
||||
alert('Terima kasih atas ulasan Anda! 🐾');
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Update stats
|
||||
function updateStats() {
|
||||
const total = reviews.length;
|
||||
const avg = (reviews.reduce((sum, r) => sum + r.rating, 0) / total).toFixed(1);
|
||||
const fiveStars = Math.round((reviews.filter(r => r.rating === 5).length / total) * 100);
|
||||
|
||||
document.getElementById('totalReviews').textContent = total;
|
||||
document.getElementById('avgRating').textContent = avg;
|
||||
document.getElementById('fiveStars').textContent = fiveStars + '%';
|
||||
}
|
||||
|
||||
// Initialize
|
||||
displayReviews();
|
||||
updateStats();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
const stars = document.querySelectorAll('#rating-stars span');
|
||||
const ratingInput = document.getElementById('rating-value');
|
||||
|
||||
stars.forEach((star, index) => {
|
||||
|
||||
// ✅ CLICK (INI YANG KAMU KURANG)
|
||||
star.addEventListener('click', () => {
|
||||
const value = star.getAttribute('data-value');
|
||||
ratingInput.value = value;
|
||||
|
||||
stars.forEach((s, i) => {
|
||||
s.textContent = i < value ? '★' : '☆';
|
||||
});
|
||||
});
|
||||
|
||||
// hover (punya kamu)
|
||||
star.addEventListener('mouseover', () => {
|
||||
stars.forEach((s, i) => {
|
||||
s.textContent = i <= index ? '★' : '☆';
|
||||
});
|
||||
});
|
||||
|
||||
// keluar hover
|
||||
star.addEventListener('mouseout', () => {
|
||||
let value = ratingInput.value;
|
||||
stars.forEach((s, i) => {
|
||||
s.textContent = i < value ? '★' : '☆';
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,24 @@
|
|||
use App\Http\Controllers\DiagnosisController;
|
||||
use App\Http\Controllers\AdminController;
|
||||
use App\Http\Controllers\GejalaController;
|
||||
use App\Http\Controllers\UlasanController;
|
||||
use App\Models\Ulasan;
|
||||
|
||||
Route::get('/admin/sort-diagnosis', [AdminController::class, 'sortDiagnosis']);
|
||||
|
||||
Route::delete('/ulasan/{id}', [UlasanController::class, 'destroy'])->name('ulasan.delete');
|
||||
|
||||
Route::get('/ulasan', [UlasanController::class, 'index'])->name('ulasan');
|
||||
Route::post('/ulasan', [UlasanController::class, 'store'])->name('ulasan.store');
|
||||
|
||||
Route::get('/gejala', [GejalaController::class, 'index'])->name('gejala');
|
||||
|
||||
Route::delete('/admin/ulasan/{id}', [UlasanController::class, 'destroy'])
|
||||
->name('ulasan.delete');
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('landing');
|
||||
$ulasan = Ulasan::latest()->take(3)->get();
|
||||
return view('landing', compact('ulasan'));
|
||||
});
|
||||
|
||||
Route::get('/biodata', function () {
|
||||
|
|
@ -23,10 +36,6 @@
|
|||
|
||||
Route::get('/hasil-diagnosis', [DiagnosisController::class, 'hasil'])->name('hasil-diagnosis');
|
||||
|
||||
Route::get('/ulasan', function () {
|
||||
return view('ulasan');
|
||||
})->name('ulasan');
|
||||
|
||||
Route::get('/faq', function () {
|
||||
return view('faq');
|
||||
})->name('faq');
|
||||
|
|
@ -36,5 +45,5 @@
|
|||
Route::post('/admin/login', [AdminController::class, 'authenticate'])->name('admin.authenticate');
|
||||
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');
|
||||
Loading…
Reference in New Issue