feat: tambahkan grafik produk terlaris & kestabilan harga di dashboard admin, hapus fitur verifikasi petani

This commit is contained in:
sayasilvi 2026-03-02 03:20:52 +07:00
parent f01c00fa87
commit 6097b439fc
3 changed files with 209 additions and 112 deletions

View File

@ -7,75 +7,63 @@
use App\Models\Petani;
use App\Models\Produk;
use App\Models\Transaksi;
use App\Models\DetailTransaksi;
use Illuminate\Support\Facades\DB;
class AdminController extends Controller
{
public function dashboard()
{
// STATISTIK
$totalPetani = Petani::where('status_akun', 'aktif')->count();
$petaniPending = Petani::where('status_akun', 'menunggu')->count();
$totalProduk = Produk::count();
$totalTransaksi = Transaksi::count();
$transaksiTerbaru = Transaksi::with(['pembeli', 'petani'])->latest()->take(5)->get();
$transaksiTerbaru = Transaksi::with(['pembeli', 'petani'])
->latest()
// PRODUK TERLARIS
$produkTerlaris = Produk::withSum(['detailTransaksis as total_terjual' => function ($query) {
$query->whereHas('transaksi', function ($q) {
$q->where('status', '!=', 'batal');
});
}], 'jumlah')
->orderByDesc('total_terjual')
->take(5)
->get();
$labelTerlaris = $produkTerlaris->pluck('nama_produk');
$dataTerlaris = $produkTerlaris->pluck('total_terjual');
// Menghitung rata-rata harga jual harian selama 7 hari terakhir
$hargaStabil = DetailTransaksi::join('transaksis', 'detail_transaksis.transaksi_id', '=', 'transaksis.id')
->selectRaw('DATE(transaksis.tanggal_transaksi) as tanggal, AVG(detail_transaksis.harga_satuan) as rata_harga')
->where('transaksis.status', '!=', 'batal')
->groupBy('tanggal')
->orderBy('tanggal', 'asc')
->take(7)
->get();
$labelHarga = $hargaStabil->pluck('tanggal')->map(function($date) {
return \Carbon\Carbon::parse($date)->format('d M');
});
$dataHarga = $hargaStabil->pluck('rata_harga');
return view('admin.dashboard', compact(
'totalPetani',
'petaniPending',
'totalProduk',
'totalTransaksi',
'transaksiTerbaru'
'totalPetani', 'totalProduk', 'totalTransaksi', 'transaksiTerbaru',
'labelTerlaris', 'dataTerlaris', 'labelHarga', 'dataHarga'
));
}
public function monitoring()
{
$produks = Produk::with('petani')->latest()->paginate(10);
$transaksis = Transaksi::with(['pembeli', 'petani'])
->latest()
->paginate(10);
$transaksis = Transaksi::with(['pembeli', 'petani'])->latest()->paginate(10);
return view('admin.monitoring', compact('produks', 'transaksis'));
}
public function transaksiDetail($id)
{
$transaksi = Transaksi::with(['pembeli', 'petani', 'detailTransaksis.produk'])
->findOrFail($id);
$transaksi = Transaksi::with(['pembeli', 'petani', 'detailTransaksis.produk'])->findOrFail($id);
return view('admin.transaksi_detail', compact('transaksi'));
}
public function verifikasiIndex()
{
$petanis = Petani::orderBy('created_at', 'desc')->get();
return view('admin.verifikasi.index', compact('petanis'));
}
public function verifikasiShow($id)
{
$petani = Petani::findOrFail($id);
return view('admin.verifikasi.show', compact('petani'));
}
public function verifikasiApprove($id)
{
$petani = Petani::findOrFail($id);
$petani->status_akun = 'aktif';
$petani->save();
return redirect('admin/verifikasi')->with('success', 'Pendaftaran Petani BERHASIL diterima.');
}
public function verifikasiReject($id)
{
$petani = Petani::findOrFail($id);
$petani->status_akun = 'ditolak';
$petani->save();
return redirect('admin/verifikasi')->with('success', 'Pendaftaran Petani DITOLAK.');
}
}

View File

@ -23,7 +23,7 @@ public function checkoutPage(Request $request)
// Mendefinisikan ID Pembeli agar tidak error 'Undefined Variable'
$pembeli_id = Auth::guard('pembeli')->id();
// 1. LOGIKA BELI LANGSUNG (Buy Now)
// LOGIKA BELI LANGSUNG (Buy Now)
if ($request->has('produk_id')) {
$produk = Produk::with('petani')->findOrFail($request->produk_id);
$items = collect([
@ -41,7 +41,7 @@ public function checkoutPage(Request $request)
return view('landing.checkout', compact('items', 'total_belanja'));
}
// 2. LOGIKA CHECKOUT DARI KERANJANG (Database)
// LOGIKA CHECKOUT DARI KERANJANG (Database)
$cartIds = $request->query('cart_ids');
if (!$cartIds) {
@ -50,7 +50,6 @@ public function checkoutPage(Request $request)
$selectedIds = explode(',', $cartIds);
// Ambil data dari Tabel Cart yang ID-nya dicentang oleh user
$cartItems = Cart::with('produk.petani')
->where('pembeli_id', $pembeli_id)
->whereIn('id', $selectedIds)
@ -132,7 +131,6 @@ public function prosesCheckout(Request $request)
->where('pembeli_id', $pembeli_id)
->get();
// Kelompokkan produk berdasarkan Petani ID agar Invoice terpisah per toko
$groupedByPetani = [];
foreach ($cartItems as $item) {
$groupedByPetani[$item->produk->petani_id][] = $item;
@ -144,7 +142,7 @@ public function prosesCheckout(Request $request)
'petani_id' => $petani_id,
'tanggal_transaksi' => now(),
'alamat_pengiriman' => $request->alamat_pengiriman,
'total_harga' => 0, // Diupdate setelah menghitung subtotal
'total_harga' => 0,
'status' => 'menunggu konfirmasi',
'kode_invoice' => 'INV/' . date('Ymd') . '/' . rand(1000, 9999),
]);
@ -169,7 +167,6 @@ public function prosesCheckout(Request $request)
$transaksi->update(['total_harga' => $subtotal_transaksi]);
}
// OTOMATIS BERSIHKAN ITEM KERANJANG YANG SUDAH DIBELI
Cart::whereIn('id', $cartIds)->delete();
}
});

View File

@ -6,45 +6,27 @@
@section('content')
<section class="row">
<div class="col-12">
{{-- STATISTIK ATAS --}}
<div class="row">
{{-- Statistik Petani Aktif --}}
<div class="col-6 col-lg-3 col-md-6">
<div class="card">
<div class="card-body px-3 py-4-5">
<div class="col-6 col-lg-4 col-md-6">
<div class="card shadow-sm">
<div class="card-body px-4 py-4-5">
<div class="row">
<div class="col-md-4 d-flex justify-content-center align-items-center">
<div class="stats-icon purple"><i class="bi bi-people-fill"></i></div>
</div>
<div class="col-md-8">
<h6 class="text-muted font-semibold">Petani Aktif</h6>
<h6 class="text-muted font-semibold">Total Petani</h6>
<h6 class="font-extrabold mb-0">{{ $totalPetani }}</h6>
</div>
</div>
</div>
</div>
</div>
{{-- Statistik Menunggu Verifikasi --}}
<div class="col-6 col-lg-3 col-md-6">
<div class="card">
<div class="card-body px-3 py-4-5">
<div class="row">
<div class="col-md-4 d-flex justify-content-center align-items-center">
<div class="stats-icon red"><i class="bi bi-person-plus-fill"></i></div>
</div>
<div class="col-md-8">
<h6 class="text-muted font-semibold">Verifikasi Masuk</h6>
<h6 class="font-extrabold mb-0">{{ $petaniPending }}</h6>
</div>
</div>
</div>
</div>
</div>
{{-- Statistik Total Produk --}}
<div class="col-6 col-lg-3 col-md-6">
<div class="card">
<div class="card-body px-3 py-4-5">
<div class="col-6 col-lg-4 col-md-6">
<div class="card shadow-sm">
<div class="card-body px-4 py-4-5">
<div class="row">
<div class="col-md-4 d-flex justify-content-center align-items-center">
<div class="stats-icon green"><i class="bi bi-basket-fill"></i></div>
@ -57,11 +39,9 @@
</div>
</div>
</div>
{{-- Statistik Total Transaksi --}}
<div class="col-6 col-lg-3 col-md-6">
<div class="card">
<div class="card-body px-3 py-4-5">
<div class="col-12 col-lg-4 col-md-12">
<div class="card shadow-sm">
<div class="card-body px-4 py-4-5">
<div class="row">
<div class="col-md-4 d-flex justify-content-center align-items-center">
<div class="stats-icon blue"><i class="bi bi-receipt"></i></div>
@ -76,14 +56,41 @@
</div>
</div>
{{-- Tabel Ringkasan Transaksi --}}
{{-- AREA GRAFIK BERSEBELAHAN --}}
<div class="row mb-4">
{{-- Kiri: Grafik Produk Terlaris --}}
<div class="col-md-6">
<div class="card shadow-sm h-100">
<div class="card-header bg-white border-bottom">
<h5 class="card-title mb-0"><i class="bi bi-bar-chart-fill text-primary me-2"></i>Produk Terlaris</h5>
</div>
<div class="card-body mt-3">
<canvas id="chartTerlaris" style="max-height: 300px;"></canvas>
</div>
</div>
</div>
{{-- Kanan: Grafik Kestabilan Harga --}}
<div class="col-md-6">
<div class="card shadow-sm h-100">
<div class="card-header bg-white border-bottom">
<h5 class="card-title mb-0"><i class="bi bi-graph-up text-success me-2"></i>Kestabilan Harga Jual Petani</h5>
</div>
<div class="card-body mt-3">
<canvas id="chartHarga" style="max-height: 300px;"></canvas>
</div>
</div>
</div>
</div>
{{-- TABEL TRANSAKSI TERBARU (SAMA SEPERTI SEBELUMNYA) --}}
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4>Transaksi Terbaru di Platform</h4>
<div class="card shadow-sm">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0">Transaksi Terbaru di Platform</h5>
</div>
<div class="card-body">
<div class="card-body mt-3">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
@ -102,20 +109,12 @@
<td class="fw-bold text-primary">#{{ $trx->kode_invoice }}</td>
<td>
<div class="d-flex flex-column">
<span class="fw-bold text-dark">
{{ $trx->petani->nama_lengkap ?? 'Petani Tidak Ditemukan' }}
</span>
<small class="text-muted" style="font-size: 0.85em;">
<i class="bi bi-shop me-1"></i>
{{ $trx->petani->nama_usaha ?? '-' }}
</small>
<span class="fw-bold text-dark">{{ $trx->petani->nama_lengkap ?? 'Petani Tidak Ditemukan' }}</span>
<small class="text-muted"><i class="bi bi-shop me-1"></i>{{ $trx->petani->nama_usaha ?? '-' }}</small>
</div>
</td>
<td>{{ $trx->pembeli->nama_lengkap ?? 'Guest' }}</td>
<td class="fw-bold">Rp {{ number_format($trx->total_harga, 0, ',', '.') }}</td>
<td>
@php
$badgeClass = match ($trx->status) {
@ -127,30 +126,18 @@
default => 'bg-secondary',
};
@endphp
<span class="badge {{ $badgeClass }} rounded-pill px-3">
{{ ucwords($trx->status) }}
</span>
<span class="badge {{ $badgeClass }} rounded-pill px-3">{{ ucwords($trx->status) }}</span>
</td>
<td class="text-muted small">{{ $trx->created_at->format('d M Y') }}</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center py-4 text-muted">
<div class="d-flex flex-column align-items-center">
<i class="bi bi-inbox fs-1 mb-2"></i>
Belum ada transaksi baru.
</div>
</td>
</tr>
<tr><td colspan="6" class="text-center py-4 text-muted">Belum ada transaksi baru.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="text-center mt-3">
<a href="{{ route('admin.monitoring') }}" class="btn btn-sm btn-primary">
Lihat Semua Data
</a>
<a href="{{ route('admin.monitoring') }}" class="btn btn-sm btn-primary">Lihat Semua Data</a>
</div>
</div>
</div>
@ -158,4 +145,129 @@
</div>
</div>
</section>
@endsection
@section('js')
{{-- Load library Chart.js --}}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
const labelTerlarisAsli = {!! json_encode($labelTerlaris) !!};
const dataTerlaris = {!! json_encode($dataTerlaris) !!};
const labelMultiline = labelTerlarisAsli.map(function(label) {
const words = label.split(' ');
const lines = [];
let currentLine = '';
words.forEach(word => {
if ((currentLine + word).length > 15 && currentLine !== '') {
lines.push(currentLine.trim());
currentLine = word + ' ';
} else {
currentLine += word + ' ';
}
});
lines.push(currentLine.trim());
return lines;
});
// --- GRAFIK PRODUK TERLARIS ---
const ctxTerlaris = document.getElementById('chartTerlaris').getContext('2d');
new Chart(ctxTerlaris, {
type: 'bar',
data: {
labels: labelMultiline,
datasets: [{
label: 'Total Terjual',
data: dataTerlaris,
backgroundColor: 'rgba(129, 196, 8, 0.8)',
borderColor: '#81c408',
borderWidth: 1,
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
title: function(context) {
return labelTerlarisAsli[context[0].dataIndex];
},
label: function(context) {
return context.raw + ' Kg';
}
}
}
},
scales: {
x: {
ticks: {
maxRotation: 0,
autoSkip: false,
font: { size: 12 }
},
grid: { display: false }
},
y: {
beginAtZero: true,
ticks: { stepSize: 1 }
}
}
}
});
// --- GRAFIK KESTABILAN HARGA ---
const ctxHarga = document.getElementById('chartHarga').getContext('2d');
new Chart(ctxHarga, {
type: 'line',
data: {
labels: {!! json_encode($labelHarga) !!},
datasets: [{
label: 'Rata-rata Harga',
data: {!! json_encode($dataHarga) !!},
backgroundColor: 'rgba(54, 162, 235, 0.1)',
borderColor: '#36A2EB',
borderWidth: 2,
pointBackgroundColor: '#fff',
pointBorderColor: '#36A2EB',
pointBorderWidth: 2,
pointRadius: 4,
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return 'Rp ' + context.raw.toLocaleString('id-ID');
}
}
}
},
scales: {
x: {
grid: { display: false }
},
y: {
beginAtZero: false,
ticks: {
callback: function(value) { return 'Rp ' + value.toLocaleString('id-ID'); }
}
}
}
}
});
});
</script>
@endsection