Update Monitoring dan Verifikasi Petani
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
@extends('layout.main')
|
||||
|
||||
@php
|
||||
$menu = $menu ?? 'petani';
|
||||
|
||||
$tabs = [
|
||||
'monitoring' => ['label' => 'Monitoring', 'ikon' => 'tabler-chart-dots'],
|
||||
'verifikasi' => ['label' => 'Verifikasi', 'ikon' => 'tabler-rosette-discount-check'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
{{-- Vuexy: justified pills (nav-fill) → tiap tab sama lebar. --}}
|
||||
<div class="nav-align-top">
|
||||
<ul class="nav nav-pills nav-fill mb-6" role="tablist">
|
||||
@foreach ($tabs as $kunci => $tab)
|
||||
@php
|
||||
$namaRoute = "monitoring-verifikasi.{$menu}.{$kunci}";
|
||||
$tersedia = Route::has($namaRoute);
|
||||
$aktif = request()->routeIs($namaRoute . '*');
|
||||
@endphp
|
||||
<li class="nav-item">
|
||||
<a @class(['nav-link', 'active' => $aktif, 'disabled' => ! $tersedia])
|
||||
href="{{ $tersedia ? route($namaRoute) : 'javascript:void(0);' }}"
|
||||
@if ($aktif) aria-current="page" @endif
|
||||
@unless ($tersedia) aria-disabled="true" @endunless>
|
||||
<span class="d-none d-sm-inline-flex align-items-center">
|
||||
<i class="icon-base ti {{ $tab['ikon'] }} icon-sm me-1_5"></i>{{ $tab['label'] }}
|
||||
</span>
|
||||
<i class="icon-base ti {{ $tab['ikon'] }} icon-sm d-sm-none"></i>
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{-- Sub-tab: halaman turunan cukup mendefinisikan $idSubTab & $subTabs (label, ikon, view, jumlah opsional). --}}
|
||||
@isset($subTabs)
|
||||
@php $subTabAktif = array_key_exists(request('sub'), $subTabs) ? request('sub') : array_key_first($subTabs); @endphp
|
||||
<div class="nav-align-top nav-tabs-shadow">
|
||||
<ul class="nav nav-tabs nav-fill" role="tablist">
|
||||
@foreach ($subTabs as $kunci => $tab)
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" role="tab"
|
||||
@class(['nav-link', 'active' => $kunci === $subTabAktif])
|
||||
data-bs-toggle="tab" data-bs-target="#{{ $idSubTab }}-{{ $kunci }}"
|
||||
aria-controls="{{ $idSubTab }}-{{ $kunci }}" aria-selected="{{ $kunci === $subTabAktif ? 'true' : 'false' }}">
|
||||
<span class="d-none d-sm-inline-flex align-items-center">
|
||||
<i class="icon-base ti {{ $tab['ikon'] }} icon-sm me-1_5"></i>{{ $tab['label'] }}
|
||||
@isset($tab['jumlah'])
|
||||
<span class="badge rounded-pill badge-center h-px-20 w-px-20 bg-label-primary ms-1_5">{{ $tab['jumlah'] }}</span>
|
||||
@endisset
|
||||
</span>
|
||||
<i class="icon-base ti {{ $tab['ikon'] }} icon-sm d-sm-none"></i>
|
||||
</button>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
@foreach ($subTabs as $kunci => $tab)
|
||||
<div @class(['tab-pane fade', 'show active' => $kunci === $subTabAktif]) id="{{ $idSubTab }}-{{ $kunci }}" role="tabpanel">
|
||||
@include($tab['view'])
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<span class="avatar avatar-md mx-auto mb-2">
|
||||
<span class="avatar-initial rounded bg-label-secondary"><i class="icon-base ti tabler-layout-dashboard icon-26px"></i></span>
|
||||
</span>
|
||||
<p class="mb-0 text-body-secondary">Konten tab belum dibuat.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endisset
|
||||
|
||||
{{-- Kartu rincian: disalin otomatis ke setiap .kolom-detail yang masih kosong. --}}
|
||||
<template id="template-kartu-rincian">
|
||||
<div class="card shadow-none border h-100 kartu-rincian">
|
||||
<div class="card-header d-flex justify-content-between align-items-center border-bottom">
|
||||
<h5 class="card-title mb-0 kartu-rincian-judul">Rincian</h5>
|
||||
<button type="button" class="btn-close btn-tutup-rincian" aria-label="Tutup"></button>
|
||||
</div>
|
||||
<div class="card-body pt-6 kartu-rincian-isi"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('style')
|
||||
<style>
|
||||
/* Tabel penuh; saat rincian dibuka, tabel menyusut & kartu rincian bergeser masuk dari kanan. */
|
||||
.monitoring-split { display: flex; gap: 0; transition: gap .3s ease; }
|
||||
.monitoring-split .kolom-tabel { flex: 1 1 100%; min-width: 0; transition: flex-basis .3s ease; }
|
||||
.monitoring-split .kolom-detail {
|
||||
flex: 0 0 0; max-width: 0; min-width: 0; opacity: 0; overflow: hidden;
|
||||
transform: translateX(1.5rem);
|
||||
transition: flex-basis .3s ease, max-width .3s ease, opacity .3s ease, transform .3s ease;
|
||||
}
|
||||
.monitoring-split.terbuka { gap: 1.5rem; }
|
||||
.monitoring-split.terbuka .kolom-tabel { flex-basis: 58%; }
|
||||
.monitoring-split.terbuka .kolom-detail { flex-basis: 42%; max-width: 42%; opacity: 1; transform: none; }
|
||||
|
||||
/* Kartu rincian ikut terlihat saat halaman digulir; isinya bisa digulir sendiri. */
|
||||
.kartu-rincian { position: sticky; top: 1rem; max-height: calc(100vh - 2rem); }
|
||||
.kartu-rincian .card-body { overflow-y: auto; }
|
||||
|
||||
/* Baris tabel yang bisa diklik untuk membuka rincian. */
|
||||
tr.btn-buka-rincian { cursor: pointer; }
|
||||
|
||||
/* Layar kecil: rincian tampil di bawah tabel. */
|
||||
@media (max-width: 991.98px) {
|
||||
.monitoring-split { flex-direction: column; }
|
||||
.monitoring-split.terbuka .kolom-tabel,
|
||||
.monitoring-split.terbuka .kolom-detail { flex-basis: auto; max-width: 100%; }
|
||||
.kartu-rincian { position: static; max-height: none; }
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
(function () {
|
||||
const memuat = '<div class="text-center py-6"><div class="spinner-border text-primary" role="status"></div></div>';
|
||||
const layarKecil = () => window.matchMedia('(max-width: 991.98px)').matches;
|
||||
const templateKartu = document.getElementById('template-kartu-rincian');
|
||||
|
||||
document.querySelectorAll('.monitoring-split').forEach(function (split) {
|
||||
const kolomTabel = split.querySelector('.kolom-tabel');
|
||||
const kolomDetail = split.querySelector('.kolom-detail');
|
||||
if (!kolomDetail.children.length) kolomDetail.append(templateKartu.content.cloneNode(true));
|
||||
const judul = kolomDetail.querySelector('.kartu-rincian-judul');
|
||||
const isi = kolomDetail.querySelector('.kartu-rincian-isi');
|
||||
let permintaan = 0; // abaikan respons lama kalau tombol lain sudah diklik
|
||||
let tombolAktif = null;
|
||||
|
||||
const tandai = (tombol) => {
|
||||
kolomTabel.querySelectorAll('tr.table-active').forEach((tr) => tr.classList.remove('table-active'));
|
||||
tombol?.closest('tr')?.classList.add('table-active');
|
||||
tombolAktif = tombol;
|
||||
};
|
||||
|
||||
const tutup = () => {
|
||||
split.classList.remove('terbuka');
|
||||
kolomDetail.setAttribute('aria-hidden', 'true');
|
||||
tandai(null);
|
||||
permintaan++;
|
||||
};
|
||||
split.tutupRincian = tutup;
|
||||
|
||||
kolomDetail.querySelector('.btn-tutup-rincian').addEventListener('click', tutup);
|
||||
|
||||
kolomTabel.addEventListener('click', async function (e) {
|
||||
const tombol = e.target.closest('.btn-buka-rincian');
|
||||
if (!tombol) return;
|
||||
// Tautan di dalam baris yang bisa diklik tetap berfungsi sebagai tautan.
|
||||
const tautan = e.target.closest('a');
|
||||
if (tautan && tautan !== tombol && tombol.contains(tautan)) return;
|
||||
|
||||
// Klik tombol yang sama lagi → tutup.
|
||||
if (tombol === tombolAktif && split.classList.contains('terbuka')) return tutup();
|
||||
|
||||
const nomor = ++permintaan;
|
||||
tandai(tombol);
|
||||
judul.textContent = tombol.dataset.judul || 'Rincian';
|
||||
isi.innerHTML = memuat;
|
||||
isi.scrollTop = 0;
|
||||
split.classList.add('terbuka');
|
||||
kolomDetail.setAttribute('aria-hidden', 'false');
|
||||
if (layarKecil()) kolomDetail.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
try {
|
||||
const res = await fetch(tombol.dataset.url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
if (!res.ok) throw new Error(res.status);
|
||||
const html = await res.text();
|
||||
if (nomor === permintaan) isi.innerHTML = html;
|
||||
} catch (err) {
|
||||
if (nomor === permintaan) {
|
||||
isi.innerHTML = '<div class="alert alert-danger mb-0">Gagal memuat rincian. Coba lagi.</div>';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Esc menutup rincian yang sedang terbuka.
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key !== 'Escape') return;
|
||||
document.querySelectorAll('.monitoring-split.terbuka').forEach((split) => split.tutupRincian?.());
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
Reference in New Issue
Block a user