Update Map Tanaman & User
This commit is contained in:
@@ -10,6 +10,10 @@
|
||||
<label class="form-label" for="add-user-fullname">Nama Lengkap</label>
|
||||
<input type="text" class="form-control" id="add-user-fullname" placeholder="John Doe" name="name" required />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="add-user-username">Nama akun</label>
|
||||
<input type="text" class="form-control" id="add-user-username" placeholder="john" name="username" required />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="add-user-email">Email</label>
|
||||
<input type="email" id="add-user-email" class="form-control" placeholder="john.doe@example.com" name="email" required />
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
</ul>
|
||||
<ul class="menu-sub">
|
||||
<li class="menu-item">
|
||||
<a href="{{ route('tanaman.index') }}" class="menu-link">
|
||||
<a href="{{ route('map-tanaman.index') }}" class="menu-link">
|
||||
<div data-i18n="Pemantauan Tanaman">Pemantauan Tanaman</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -251,10 +251,27 @@
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="{{ route('komoditas.index') }}" class="menu-link">
|
||||
<a href="javascript:void(0);" class="menu-link menu-toggle">
|
||||
<i class="menu-icon icon-base ti tabler-plant"></i>
|
||||
<div data-i18n="Komoditas">Komoditas</div>
|
||||
<div data-i18n="Tanaman">Tanaman</div>
|
||||
</a>
|
||||
<ul class="menu-sub">
|
||||
<li class="menu-item">
|
||||
<a href="{{ route('komoditas.index') }}" class="menu-link">
|
||||
<div data-i18n="Komoditas">Komoditas</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="{{ route('jenis-komoditas.index') }}" class="menu-link">
|
||||
<div data-i18n="Jenis Komoditas">Jenis Komoditas</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="{{ route('varietas.index') }}" class="menu-link">
|
||||
<div data-i18n="Varietas">Varietas</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- Konfigurasi -->
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,832 @@
|
||||
@extends('layout.main')
|
||||
|
||||
@push('style')
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
|
||||
<style>
|
||||
#map {
|
||||
height: 100%;
|
||||
min-height: 620px;
|
||||
width: 100%;
|
||||
background-color: var(--bs-body-bg);
|
||||
z-index: 1;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.map-wrapper {
|
||||
min-height: 620px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.points-table-wrapper {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
color: #2f2b3d;
|
||||
}
|
||||
|
||||
.plant-marker-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--bs-success-rgb), 0.95);
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.plant-marker-icon.sakit {
|
||||
background: rgba(var(--bs-danger-rgb), 0.95);
|
||||
}
|
||||
|
||||
.plant-marker-icon.mati {
|
||||
background: rgba(var(--bs-dark-rgb), 0.95);
|
||||
}
|
||||
|
||||
.plant-dot-marker {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--bs-success-rgb), 0.95);
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.plant-dot-marker.sakit {
|
||||
background: rgba(var(--bs-danger-rgb), 0.95);
|
||||
}
|
||||
|
||||
.plant-dot-marker.mati {
|
||||
background: rgba(var(--bs-dark-rgb), 0.95);
|
||||
}
|
||||
|
||||
.detail-box {
|
||||
padding: .75rem;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: .375rem;
|
||||
background-color: rgba(var(--bs-primary-rgb), .04);
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: .75rem;
|
||||
color: var(--bs-secondary-color);
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-weight: 500;
|
||||
color: var(--bs-heading-color);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.custom-plant-popup .leaflet-popup-content-wrapper {
|
||||
border-radius: 14px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: var(--bs-card-bg);
|
||||
color: var(--bs-body-color);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.custom-plant-popup .leaflet-popup-content {
|
||||
margin: 0;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.custom-plant-popup .leaflet-popup-tip {
|
||||
background: var(--bs-card-bg);
|
||||
}
|
||||
|
||||
.plant-popup-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plant-popup-header {
|
||||
padding: 10px 12px;
|
||||
background: rgba(var(--bs-primary-rgb), 0.14);
|
||||
border-bottom: 1px solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
.plant-popup-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--bs-heading-color);
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.plant-popup-subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: var(--bs-secondary-color);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.plant-popup-body {
|
||||
padding: 10px 12px 12px;
|
||||
}
|
||||
|
||||
.plant-popup-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.plant-popup-label {
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
.plant-popup-value {
|
||||
color: var(--bs-heading-color);
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.plant-popup-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.plant-popup-actions .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.plant-popup-help {
|
||||
font-size: 0.7rem;
|
||||
color: var(--bs-secondary-color);
|
||||
margin-top: 8px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$groupName = ($tanaman->komoditas->nama ?? 'Tanaman') . ' - ' . ($tanaman->profile->nama ?? 'Pemilik');
|
||||
@endphp
|
||||
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="card">
|
||||
<div class="card-header border-bottom mb-4 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="card-title mb-0">Edit Kelompok Tanaman</h5>
|
||||
<small class="text-muted">{{ $groupName }}</small>
|
||||
</div>
|
||||
|
||||
<a href="{{ route('map-tanaman.index') }}" class="btn btn-sm btn-label-secondary">
|
||||
<i class="ti tabler-arrow-left me-1"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form id="editTanamanForm" action="{{ route('map-tanaman.update', $tanaman->id) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-5 mb-4">
|
||||
|
||||
<h6 class="mb-3 text-primary">
|
||||
<i class="ti tabler-seeding me-1"></i> Informasi Kelompok
|
||||
</h6>
|
||||
|
||||
<div class="alert alert-info mb-3">
|
||||
<i class="ti tabler-info-circle me-1"></i>
|
||||
Data lahan, pemilik, komoditas, jenis, dan varietas tidak diubah dari halaman ini. Perubahan hanya untuk titik tanaman.
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="detail-box">
|
||||
<div class="detail-label">Pemilik/Pengelola</div>
|
||||
<div class="detail-value">{{ $tanaman->profile->nama ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="detail-box">
|
||||
<div class="detail-label">Lahan</div>
|
||||
<div class="detail-value">{{ $tanaman->lahan->nama ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="detail-box">
|
||||
<div class="detail-label">Komoditas</div>
|
||||
<div class="detail-value">{{ $tanaman->komoditas->nama ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="detail-box">
|
||||
<div class="detail-label">Jenis Komoditas</div>
|
||||
<div class="detail-value">{{ $tanaman->jenisKomoditas->nama ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="detail-box">
|
||||
<div class="detail-label">Varietas</div>
|
||||
<div class="detail-value">{{ $tanaman->varietas->nama ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-control-validation">
|
||||
<label class="form-label">Deskripsi</label>
|
||||
<textarea
|
||||
name="deskripsi"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="Catatan tambahan">{{ old('deskripsi', $tanaman->deskripsi ?? '') }}</textarea>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h6 class="mb-3 text-primary">
|
||||
<i class="ti tabler-current-location me-1"></i> Titik Koordinat Tanaman
|
||||
</h6>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<label class="form-label text-success fw-bold mb-0">
|
||||
Klik map untuk menambahkan titik baru
|
||||
</label>
|
||||
<small class="text-muted d-block">
|
||||
Kondisi dan hapus titik bisa dilakukan dari tabel atau popup map.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" role="group" aria-label="Marker Mode">
|
||||
<input type="radio" class="btn-check" name="marker_mode" id="marker-mode-plant" value="plant" checked>
|
||||
<label class="btn btn-sm btn-outline-primary" for="marker-mode-plant">
|
||||
<i class="ti tabler-plant-2 me-1"></i> Ikon
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="marker_mode" id="marker-mode-dot" value="dot">
|
||||
<label class="btn btn-sm btn-outline-primary" for="marker-mode-dot">
|
||||
<i class="ti tabler-point me-1"></i> Titik
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-control-validation">
|
||||
<input type="hidden" id="points_validation" name="points_validation">
|
||||
</div>
|
||||
|
||||
<div class="points-table-wrapper border rounded">
|
||||
<table class="table table-sm table-hover mb-0" id="points-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50px;">No</th>
|
||||
<th>Latitude</th>
|
||||
<th>Longitude</th>
|
||||
<th style="width: 150px;">Kondisi</th>
|
||||
<th style="width: 60px;" class="text-center">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr id="empty-points-row">
|
||||
<td colspan="5" class="text-center text-muted py-4">
|
||||
Belum ada titik tanaman. Klik area pada peta.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="points-hidden-wrapper"></div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<a href="{{ route('map-tanaman.index') }}" class="btn btn-label-secondary me-2">
|
||||
Batal
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary data-submit">
|
||||
Simpan Perubahan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-7">
|
||||
<div class="border rounded h-100 map-wrapper">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
$(document).ready(function () {
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': "{{ csrf_token() }}"
|
||||
}
|
||||
});
|
||||
|
||||
const editForm = document.getElementById('editTanamanForm');
|
||||
|
||||
const lahanPath = {{ Illuminate\Support\Js::from($tanaman->lahan->path ?? null) }};
|
||||
|
||||
let plantPoints = {{ Illuminate\Support\Js::from(
|
||||
$points->map(function ($point) {
|
||||
$kondisi = $point->kondisi_tanaman;
|
||||
$kondisiValue = is_object($kondisi) && property_exists($kondisi, 'value')
|
||||
? $kondisi->value
|
||||
: $kondisi;
|
||||
|
||||
return [
|
||||
'id' => $point->id,
|
||||
'nama' => $point->nama,
|
||||
'latitude' => $point->latitude,
|
||||
'longitude' => $point->longitude,
|
||||
'kondisi_tanaman' => $kondisiValue,
|
||||
'_delete' => false,
|
||||
'_new' => false,
|
||||
];
|
||||
})->values()
|
||||
) }};
|
||||
|
||||
const map = L.map('map').setView([-2.5489, 118.0149], 5);
|
||||
|
||||
L.tileLayer('https://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
|
||||
maxZoom: 22,
|
||||
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
|
||||
attribution: '© Google Maps Satellite'
|
||||
}).addTo(map);
|
||||
|
||||
let currentPolygon = null;
|
||||
let plantMarkers = [];
|
||||
|
||||
function showError(xhr, fallback = 'Terjadi kesalahan sistem.') {
|
||||
let message = fallback;
|
||||
|
||||
if (xhr.responseJSON?.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
|
||||
if (xhr.responseJSON?.errors) {
|
||||
message = Object.values(xhr.responseJSON.errors).flat().join('<br>');
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Gagal!',
|
||||
html: message,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-primary'
|
||||
},
|
||||
buttonsStyling: false
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function parsePath(path) {
|
||||
if (!path) return null;
|
||||
|
||||
let parsed = path;
|
||||
|
||||
if (typeof path === 'string') {
|
||||
try {
|
||||
parsed = JSON.parse(path);
|
||||
} catch (error) {
|
||||
console.error('Path bukan JSON valid:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.type && parsed.coordinates) {
|
||||
return {
|
||||
type: 'geojson',
|
||||
data: parsed
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
return {
|
||||
type: 'array',
|
||||
data: parsed
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderLahanPolygon(path) {
|
||||
const parsed = parsePath(path);
|
||||
|
||||
if (!parsed) return null;
|
||||
|
||||
let layer = null;
|
||||
|
||||
if (parsed.type === 'geojson') {
|
||||
layer = L.geoJSON(parsed.data, {
|
||||
style: {
|
||||
color: '#28c76f',
|
||||
fillColor: '#28c76f',
|
||||
fillOpacity: 0.35,
|
||||
weight: 2
|
||||
}
|
||||
}).addTo(map);
|
||||
|
||||
map.fitBounds(layer.getBounds(), { padding: [30, 30] });
|
||||
}
|
||||
|
||||
if (parsed.type === 'array') {
|
||||
const latLngs = parsed.data.map(coord => [coord[1], coord[0]]);
|
||||
|
||||
layer = L.polygon(latLngs, {
|
||||
color: '#28c76f',
|
||||
fillColor: '#28c76f',
|
||||
fillOpacity: 0.35,
|
||||
weight: 2
|
||||
}).addTo(map);
|
||||
|
||||
map.fitBounds(layer.getBounds(), { padding: [30, 30] });
|
||||
}
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
function getMarkerMode() {
|
||||
return $('input[name="marker_mode"]:checked').val() || 'plant';
|
||||
}
|
||||
|
||||
function getPlantIcon(point) {
|
||||
const kondisi = point.kondisi_tanaman || 'sehat';
|
||||
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `
|
||||
<div class="plant-marker-icon ${kondisi}">
|
||||
<i class="ti tabler-plant-2"></i>
|
||||
</div>
|
||||
`,
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 16],
|
||||
popupAnchor: [0, -16]
|
||||
});
|
||||
}
|
||||
|
||||
function getDotIcon(point) {
|
||||
const kondisi = point.kondisi_tanaman || 'sehat';
|
||||
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="plant-dot-marker ${kondisi}"></div>`,
|
||||
iconSize: [11, 11],
|
||||
iconAnchor: [5.5, 5.5],
|
||||
popupAnchor: [0, -8]
|
||||
});
|
||||
}
|
||||
|
||||
function formatKondisiLabel(kondisi) {
|
||||
const labels = {
|
||||
sehat: 'Sehat',
|
||||
sakit: 'Sakit',
|
||||
mati: 'Mati'
|
||||
};
|
||||
|
||||
return labels[kondisi] || '-';
|
||||
}
|
||||
|
||||
function getActivePoints() {
|
||||
return plantPoints.filter(point => !point._delete);
|
||||
}
|
||||
|
||||
function buildPlantPopup(point, index, visibleIndex) {
|
||||
const title = point.nama && point.nama !== 'null'
|
||||
? point.nama
|
||||
: `Titik Tanaman ${visibleIndex + 1}`;
|
||||
|
||||
return `
|
||||
<div class="plant-popup-card">
|
||||
<div class="plant-popup-header">
|
||||
<p class="plant-popup-title">${escapeHtml(title)}</p>
|
||||
<div class="plant-popup-subtitle">Titik Tanaman ${visibleIndex + 1}</div>
|
||||
</div>
|
||||
|
||||
<div class="plant-popup-body">
|
||||
<div class="plant-popup-row">
|
||||
<span class="plant-popup-label">Latitude</span>
|
||||
<span class="plant-popup-value">${point.latitude}</span>
|
||||
</div>
|
||||
|
||||
<div class="plant-popup-row">
|
||||
<span class="plant-popup-label">Longitude</span>
|
||||
<span class="plant-popup-value">${point.longitude}</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1" style="font-size: .75rem;">Kondisi Tanaman</label>
|
||||
<select class="form-select form-select-sm popup-point-condition" data-index="${index}">
|
||||
<option value="sehat" ${point.kondisi_tanaman === 'sehat' ? 'selected' : ''}>Sehat</option>
|
||||
<option value="sakit" ${point.kondisi_tanaman === 'sakit' ? 'selected' : ''}>Sakit</option>
|
||||
<option value="mati" ${point.kondisi_tanaman === 'mati' ? 'selected' : ''}>Mati</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="plant-popup-actions">
|
||||
<button type="button" class="btn btn-sm btn-label-danger popup-remove-point" data-index="${index}">
|
||||
<i class="ti tabler-trash me-1"></i> Hapus Titik
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="plant-popup-help">
|
||||
Perubahan akan tersimpan setelah klik <strong>Simpan Perubahan</strong>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderPointsTable() {
|
||||
const $tbody = $('#points-table tbody');
|
||||
const $hiddenWrapper = $('#points-hidden-wrapper');
|
||||
|
||||
$tbody.empty();
|
||||
$hiddenWrapper.empty();
|
||||
|
||||
const activePoints = getActivePoints();
|
||||
|
||||
if (!activePoints.length) {
|
||||
$tbody.append(`
|
||||
<tr id="empty-points-row">
|
||||
<td colspan="5" class="text-center text-muted py-4">
|
||||
Belum ada titik tanaman. Klik area pada peta.
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
|
||||
$('#points_validation').val('');
|
||||
} else {
|
||||
$('#points_validation').val('filled');
|
||||
}
|
||||
|
||||
let visibleNumber = 1;
|
||||
|
||||
plantPoints.forEach((point, index) => {
|
||||
const prefix = `points[${index}]`;
|
||||
|
||||
$hiddenWrapper.append(`
|
||||
<input type="hidden" name="${prefix}[id]" value="${point.id || ''}">
|
||||
<input type="hidden" name="${prefix}[latitude]" value="${point.latitude}">
|
||||
<input type="hidden" name="${prefix}[longitude]" value="${point.longitude}">
|
||||
<input type="hidden" name="${prefix}[kondisi_tanaman]" value="${point.kondisi_tanaman}">
|
||||
<input type="hidden" name="${prefix}[_delete]" value="${point._delete ? 1 : 0}">
|
||||
`);
|
||||
|
||||
if (point._delete) return;
|
||||
|
||||
$tbody.append(`
|
||||
<tr>
|
||||
<td>${visibleNumber}</td>
|
||||
<td>${point.latitude}</td>
|
||||
<td>${point.longitude}</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm point-condition" data-index="${index}">
|
||||
<option value="sehat" ${point.kondisi_tanaman === 'sehat' ? 'selected' : ''}>Sehat</option>
|
||||
<option value="sakit" ${point.kondisi_tanaman === 'sakit' ? 'selected' : ''}>Sakit</option>
|
||||
<option value="mati" ${point.kondisi_tanaman === 'mati' ? 'selected' : ''}>Mati</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-icon btn-label-danger remove-point" data-index="${index}">
|
||||
<i class="ti tabler-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
|
||||
visibleNumber++;
|
||||
});
|
||||
}
|
||||
|
||||
function refreshPlantMarkers() {
|
||||
plantMarkers.forEach(marker => map.removeLayer(marker));
|
||||
plantMarkers = [];
|
||||
|
||||
const mode = getMarkerMode();
|
||||
let visibleIndex = 0;
|
||||
|
||||
plantPoints.forEach((point, index) => {
|
||||
if (point._delete) return;
|
||||
|
||||
const icon = mode === 'dot'
|
||||
? getDotIcon(point)
|
||||
: getPlantIcon(point);
|
||||
|
||||
const marker = L.marker([point.latitude, point.longitude], {
|
||||
icon: icon
|
||||
}).addTo(map)
|
||||
.bindPopup(buildPlantPopup(point, index, visibleIndex), {
|
||||
className: 'custom-plant-popup',
|
||||
closeButton: true,
|
||||
maxWidth: 280
|
||||
});
|
||||
|
||||
plantMarkers.push(marker);
|
||||
visibleIndex++;
|
||||
});
|
||||
}
|
||||
|
||||
function refreshField(name) {
|
||||
if (typeof fvEdit !== 'undefined' && name) {
|
||||
fvEdit.revalidateField(name);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAll() {
|
||||
renderPointsTable();
|
||||
refreshPlantMarkers();
|
||||
refreshField('points_validation');
|
||||
}
|
||||
|
||||
currentPolygon = renderLahanPolygon(lahanPath);
|
||||
|
||||
renderPointsTable();
|
||||
refreshPlantMarkers();
|
||||
|
||||
const activePoints = getActivePoints();
|
||||
|
||||
if (activePoints.length > 0) {
|
||||
const bounds = L.latLngBounds(
|
||||
activePoints
|
||||
.filter(point => point.latitude && point.longitude)
|
||||
.map(point => [point.latitude, point.longitude])
|
||||
);
|
||||
|
||||
if (bounds.isValid()) {
|
||||
map.fitBounds(bounds, { padding: [40, 40] });
|
||||
}
|
||||
}
|
||||
|
||||
map.on('click', function (e) {
|
||||
const lat = e.latlng.lat.toFixed(8);
|
||||
const lng = e.latlng.lng.toFixed(8);
|
||||
|
||||
plantPoints.push({
|
||||
id: '',
|
||||
nama: 'Titik Baru',
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
kondisi_tanaman: 'sehat',
|
||||
_delete: false,
|
||||
_new: true,
|
||||
});
|
||||
|
||||
refreshAll();
|
||||
});
|
||||
|
||||
$(document).on('change', '.point-condition', function () {
|
||||
const index = $(this).data('index');
|
||||
const value = $(this).val();
|
||||
|
||||
if (plantPoints[index]) {
|
||||
plantPoints[index].kondisi_tanaman = value;
|
||||
}
|
||||
|
||||
refreshAll();
|
||||
});
|
||||
|
||||
$(document).on('change', '.popup-point-condition', function () {
|
||||
const index = $(this).data('index');
|
||||
const value = $(this).val();
|
||||
|
||||
if (plantPoints[index]) {
|
||||
plantPoints[index].kondisi_tanaman = value;
|
||||
}
|
||||
|
||||
map.closePopup();
|
||||
refreshAll();
|
||||
});
|
||||
|
||||
$(document).on('click', '.remove-point', function () {
|
||||
const index = $(this).data('index');
|
||||
|
||||
if (!plantPoints[index]) return;
|
||||
|
||||
if (plantPoints[index].id) {
|
||||
plantPoints[index]._delete = true;
|
||||
} else {
|
||||
plantPoints.splice(index, 1);
|
||||
}
|
||||
|
||||
refreshAll();
|
||||
});
|
||||
|
||||
$(document).on('click', '.popup-remove-point', function () {
|
||||
const index = $(this).data('index');
|
||||
|
||||
if (!plantPoints[index]) return;
|
||||
|
||||
Swal.fire({
|
||||
title: 'Hapus titik ini?',
|
||||
text: 'Titik tanaman akan dihapus setelah kamu klik Simpan Perubahan.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya, hapus',
|
||||
cancelButtonText: 'Batal',
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-primary me-3',
|
||||
cancelButton: 'btn btn-label-secondary'
|
||||
},
|
||||
buttonsStyling: false
|
||||
}).then(function (result) {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
if (plantPoints[index].id) {
|
||||
plantPoints[index]._delete = true;
|
||||
} else {
|
||||
plantPoints.splice(index, 1);
|
||||
}
|
||||
|
||||
map.closePopup();
|
||||
refreshAll();
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('change', 'input[name="marker_mode"]', function () {
|
||||
refreshPlantMarkers();
|
||||
});
|
||||
|
||||
let fvEdit = FormValidation.formValidation(editForm, {
|
||||
fields: {
|
||||
points_validation: {
|
||||
validators: {
|
||||
callback: {
|
||||
message: 'Minimal harus ada satu titik tanaman aktif.',
|
||||
callback: function () {
|
||||
return getActivePoints().length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
trigger: new FormValidation.plugins.Trigger(),
|
||||
bootstrap5: new FormValidation.plugins.Bootstrap5({
|
||||
eleValidClass: '',
|
||||
rowSelector: '.form-control-validation'
|
||||
}),
|
||||
submitButton: new FormValidation.plugins.SubmitButton(),
|
||||
}
|
||||
}).on('core.form.valid', function () {
|
||||
const formData = $(editForm).serialize();
|
||||
const $button = $(editForm).find('button[type="submit"]');
|
||||
|
||||
$button.prop('disabled', true)
|
||||
.html('<span class="spinner-border spinner-border-sm me-1"></span>Menyimpan...');
|
||||
|
||||
$.ajax({
|
||||
url: editForm.action,
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
success: function (res) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil!',
|
||||
text: res.message || 'Data titik tanaman berhasil diperbarui.',
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
}).then(() => {
|
||||
window.location.href = res.redirect || "{{ route('map-tanaman.index') }}";
|
||||
});
|
||||
},
|
||||
error: function (xhr) {
|
||||
showError(xhr, 'Gagal memperbarui data tanaman.');
|
||||
},
|
||||
complete: function () {
|
||||
$button.prop('disabled', false).html('Simpan Perubahan');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => map.invalidateSize(), 300);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,230 @@
|
||||
@extends('layout.main')
|
||||
|
||||
@section('content')
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="card">
|
||||
<div class="card-body border-bottom">
|
||||
<form method="GET" action="{{ route('tanaman.index') }}" class="d-flex justify-content-between align-items-center row gap-3 gap-md-0">
|
||||
<div class="col-md-auto">
|
||||
<div class="d-flex align-items-center">
|
||||
<label class="me-2 text-nowrap">Show</label>
|
||||
<select name="per_page" class="form-select form-select-sm w-auto" onchange="this.form.submit()">
|
||||
@foreach([10, 25, 50, 100] as $limit)
|
||||
<option value="{{ $limit }}" {{ request('per_page', 10) == $limit ? 'selected' : '' }}>
|
||||
{{ $limit }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="ms-2 text-nowrap">entries</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-auto d-flex gap-2">
|
||||
<div class="input-group input-group-sm w-auto">
|
||||
<input type="text" name="search" class="form-control" placeholder="Cari tanaman..." value="{{ request('search') }}">
|
||||
|
||||
@if(request('search'))
|
||||
<a href="{{ route('tanaman.index', ['per_page' => request('per_page', 10)]) }}" class="btn btn-outline-secondary">
|
||||
<i class="ti tabler-x"></i>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<button type="submit" class="btn btn-outline-primary">
|
||||
<i class="ti tabler-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<a href="{{ route('map-tanaman.create') }}" class="btn btn-sm btn-primary">
|
||||
<i class="ti tabler-plus me-1"></i> Tambah
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive text-nowrap">
|
||||
<table class="table table-hover">
|
||||
<thead class="border-top">
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>Kelompok Tanaman</th>
|
||||
<th>Pemilik</th>
|
||||
<th>Lahan</th>
|
||||
<th>Wilayah</th>
|
||||
<th>Varietas</th>
|
||||
<th>Jumlah Titik</th>
|
||||
<th>Kondisi</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@forelse($tanaman as $index => $item)
|
||||
@php
|
||||
$states = ['success', 'danger', 'warning', 'info', 'primary', 'secondary'];
|
||||
$state = $states[$item->id % count($states)];
|
||||
|
||||
$groupName = ($item->komoditas->nama ?? 'Tanaman') . ' - ' . ($item->profile->nama ?? 'Pemilik');
|
||||
@endphp
|
||||
|
||||
<tr>
|
||||
<td>{{ $tanaman->firstItem() + $index }}</td>
|
||||
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-wrapper me-3">
|
||||
<div class="avatar avatar-sm">
|
||||
<span class="avatar-initial rounded-circle bg-label-{{ $state }}">
|
||||
{{ strtoupper(substr($item->komoditas->nama ?? 'TN', 0, 2)) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<span class="fw-medium text-heading">{{ $groupName }}</span>
|
||||
<small class="text-muted">
|
||||
{{ $item->jenisKomoditas->nama ?? '-' }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>{{ $item->profile->nama ?? '-' }}</td>
|
||||
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<span>{{ $item->lahan->nama ?? '-' }}</span>
|
||||
<small class="text-muted">{{ $item->lahan->uid ?? '-' }}</small>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<small>
|
||||
{{ $item->lahan->desaKelurahan->nama ?? '-' }}<br>
|
||||
<span class="text-muted">
|
||||
{{ $item->lahan->desaKelurahan->kecamatan->nama ?? '-' }}
|
||||
</span>
|
||||
</small>
|
||||
</td>
|
||||
|
||||
<td>{{ $item->varietas->nama ?? '-' }}</td>
|
||||
|
||||
<td>
|
||||
<span class="badge bg-label-primary">
|
||||
{{ $item->total_tanaman }} titik
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<span class="badge bg-label-success">
|
||||
Sehat: {{ $item->total_sehat ?? 0 }}
|
||||
</span>
|
||||
<span class="badge bg-label-danger">
|
||||
Sakit: {{ $item->total_sakit ?? 0 }}
|
||||
</span>
|
||||
<span class="badge bg-label-dark">
|
||||
Mati: {{ $item->total_mati ?? 0 }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<a href="{{ route('map-tanaman.show', $item->id) }}"
|
||||
class="btn btn-icon btn-text-secondary rounded-pill"
|
||||
title="Lihat">
|
||||
<i class="ti tabler-eye ti-md"></i>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('map-tanaman.edit', $item->id) }}"
|
||||
class="btn btn-icon btn-text-secondary rounded-pill"
|
||||
title="Edit">
|
||||
<i class="ti tabler-edit ti-md"></i>
|
||||
</a>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon btn-text-secondary rounded-pill delete-group"
|
||||
data-id="{{ $item->id }}"
|
||||
data-nama="{{ $groupName }}"
|
||||
title="Hapus">
|
||||
<i class="ti tabler-trash ti-md"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-4">
|
||||
Tidak ada data ditemukan.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($tanaman->hasPages())
|
||||
<div class="card-footer d-flex justify-content-center justify-content-md-end border-top">
|
||||
{{ $tanaman->appends(request()->query())->links('pagination::bootstrap-5') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
$(function () {
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': "{{ csrf_token() }}"
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.delete-group', function () {
|
||||
const id = $(this).data('id');
|
||||
const nama = $(this).data('nama');
|
||||
|
||||
Swal.fire({
|
||||
title: 'Apakah Anda yakin?',
|
||||
text: `Semua titik tanaman pada kelompok "${nama}" akan dihapus.`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya, Hapus!',
|
||||
cancelButtonText: 'Batal',
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-primary me-3',
|
||||
cancelButton: 'btn btn-label-secondary'
|
||||
},
|
||||
buttonsStyling: false
|
||||
}).then(function (result) {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
$.ajax({
|
||||
url: `/map-tanaman/${id}`,
|
||||
type: 'DELETE',
|
||||
success: function (res) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Terhapus!',
|
||||
text: res.message || 'Data berhasil dihapus.',
|
||||
showConfirmButton: false,
|
||||
timer: 1500
|
||||
}).then(() => window.location.reload());
|
||||
},
|
||||
error: function (xhr) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Gagal!',
|
||||
text: xhr.responseJSON?.message || 'Gagal menghapus data.'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,698 @@
|
||||
@extends('layout.main')
|
||||
|
||||
@push('style')
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
|
||||
<style>
|
||||
#map {
|
||||
height: 560px;
|
||||
width: 100%;
|
||||
background-color: var(--bs-body-bg);
|
||||
z-index: 1;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--bs-secondary-color);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-weight: 500;
|
||||
color: var(--bs-heading-color);
|
||||
margin-bottom: 1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
padding-bottom: .75rem;
|
||||
margin-bottom: .75rem;
|
||||
border-bottom: 1px solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
.detail-section:last-child {
|
||||
border-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.map-note {
|
||||
background-color: rgba(var(--bs-info-rgb), 0.16);
|
||||
color: var(--bs-info);
|
||||
border: 1px solid rgba(var(--bs-info-rgb), 0.25);
|
||||
}
|
||||
|
||||
.points-table-wrapper {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
color: #2f2b3d;
|
||||
}
|
||||
|
||||
.plant-marker-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--bs-success-rgb), 0.95);
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.plant-marker-icon.sakit {
|
||||
background: rgba(var(--bs-danger-rgb), 0.95);
|
||||
}
|
||||
|
||||
.plant-marker-icon.mati {
|
||||
background: rgba(var(--bs-dark-rgb), 0.95);
|
||||
}
|
||||
|
||||
.plant-dot-marker {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--bs-success-rgb), 0.95);
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.plant-dot-marker.sakit {
|
||||
background: rgba(var(--bs-danger-rgb), 0.95);
|
||||
}
|
||||
|
||||
.plant-dot-marker.mati {
|
||||
background: rgba(var(--bs-dark-rgb), 0.95);
|
||||
}
|
||||
|
||||
.custom-plant-popup .leaflet-popup-content-wrapper {
|
||||
border-radius: 14px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: var(--bs-card-bg);
|
||||
color: var(--bs-body-color);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.custom-plant-popup .leaflet-popup-content {
|
||||
margin: 0;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.custom-plant-popup .leaflet-popup-tip {
|
||||
background: var(--bs-card-bg);
|
||||
}
|
||||
|
||||
.plant-popup-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plant-popup-header {
|
||||
padding: 10px 12px;
|
||||
background: rgba(var(--bs-primary-rgb), 0.14);
|
||||
border-bottom: 1px solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
.plant-popup-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--bs-heading-color);
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.plant-popup-subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: var(--bs-secondary-color);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.plant-popup-body {
|
||||
padding: 10px 12px 12px;
|
||||
}
|
||||
|
||||
.plant-popup-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.plant-popup-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.plant-popup-label {
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
.plant-popup-value {
|
||||
color: var(--bs-heading-color);
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.plant-popup-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.plant-popup-badge.sehat {
|
||||
background: rgba(var(--bs-success-rgb), 0.16);
|
||||
color: var(--bs-success);
|
||||
}
|
||||
|
||||
.plant-popup-badge.sakit {
|
||||
background: rgba(var(--bs-danger-rgb), 0.16);
|
||||
color: var(--bs-danger);
|
||||
}
|
||||
|
||||
.plant-popup-badge.mati {
|
||||
background: rgba(var(--bs-dark-rgb), 0.16);
|
||||
color: var(--bs-dark);
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$totalTanaman = $points->count();
|
||||
|
||||
$totalSehat = $points->filter(function ($point) {
|
||||
$kondisi = $point->kondisi_tanaman;
|
||||
$value = is_object($kondisi) && property_exists($kondisi, 'value') ? $kondisi->value : $kondisi;
|
||||
return $value === 'sehat';
|
||||
})->count();
|
||||
|
||||
$totalSakit = $points->filter(function ($point) {
|
||||
$kondisi = $point->kondisi_tanaman;
|
||||
$value = is_object($kondisi) && property_exists($kondisi, 'value') ? $kondisi->value : $kondisi;
|
||||
return $value === 'sakit';
|
||||
})->count();
|
||||
|
||||
$totalMati = $points->filter(function ($point) {
|
||||
$kondisi = $point->kondisi_tanaman;
|
||||
$value = is_object($kondisi) && property_exists($kondisi, 'value') ? $kondisi->value : $kondisi;
|
||||
return $value === 'mati';
|
||||
})->count();
|
||||
|
||||
$groupName = ($tanaman->komoditas->nama ?? 'Tanaman') . ' - ' . ($tanaman->profile->nama ?? 'Pemilik');
|
||||
@endphp
|
||||
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="mb-1">Detail Kelompok Tanaman</h4>
|
||||
<small class="text-muted">{{ $groupName }}</small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ route('map-tanaman.index') }}" class="btn btn-label-secondary">
|
||||
<i class="ti tabler-arrow-left me-1"></i> Kembali
|
||||
</a>
|
||||
|
||||
<a href="{{ route('map-tanaman.edit', $tanaman->id) }}" class="btn btn-primary">
|
||||
<i class="ti tabler-edit me-1"></i> Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{{-- DETAIL DATA --}}
|
||||
<div class="col-lg-5 mb-4">
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header border-bottom">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="ti tabler-seeding me-1"></i> Ringkasan Kelompok
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body pt-4">
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Kelompok Tanaman</div>
|
||||
<div class="detail-value">{{ $groupName }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Jumlah Titik Tanaman</div>
|
||||
<div class="detail-value">
|
||||
<span class="badge bg-label-primary">{{ $totalTanaman }} titik</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Ringkasan Kondisi</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<span class="badge bg-label-success">Sehat: {{ $totalSehat }}</span>
|
||||
<span class="badge bg-label-danger">Sakit: {{ $totalSakit }}</span>
|
||||
<span class="badge bg-label-dark">Mati: {{ $totalMati }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Deskripsi</div>
|
||||
<div class="detail-value">{{ $tanaman->deskripsi ?? '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Metode Pengumpulan</div>
|
||||
<div class="detail-value">{{ $tanaman->metode_pengumpulan ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header border-bottom">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="ti tabler-user-pin me-1"></i> Pemilik & Lahan
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body pt-4">
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Pemilik/Pengelola</div>
|
||||
<div class="detail-value">{{ $tanaman->profile->nama ?? '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Lahan</div>
|
||||
<div class="detail-value">{{ $tanaman->lahan->nama ?? '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">UID Lahan</div>
|
||||
<div class="detail-value">{{ $tanaman->lahan->uid ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header border-bottom">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="ti tabler-map-pin me-1"></i> Wilayah
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body pt-4">
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Provinsi</div>
|
||||
<div class="detail-value">
|
||||
{{ $tanaman->lahan->desaKelurahan->kecamatan->kabupatenkota->provinsi->nama ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Kabupaten/Kota</div>
|
||||
<div class="detail-value">
|
||||
{{ $tanaman->lahan->desaKelurahan->kecamatan->kabupatenkota->nama ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Kecamatan</div>
|
||||
<div class="detail-value">
|
||||
{{ $tanaman->lahan->desaKelurahan->kecamatan->nama ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Desa/Kelurahan</div>
|
||||
<div class="detail-value">
|
||||
{{ $tanaman->lahan->desaKelurahan->nama ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header border-bottom">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="ti tabler-leaf me-1"></i> Komoditas
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body pt-4">
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Komoditas</div>
|
||||
<div class="detail-value">{{ $tanaman->komoditas->nama ?? '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Jenis Komoditas</div>
|
||||
<div class="detail-value">{{ $tanaman->jenisKomoditas->nama ?? '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Varietas</div>
|
||||
<div class="detail-value">{{ $tanaman->varietas->nama ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- MAP --}}
|
||||
<div class="col-lg-7 mb-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header border-bottom d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="ti tabler-map-pin me-1"></i> Lokasi Semua Titik Tanaman
|
||||
</h5>
|
||||
|
||||
<div class="btn-group" role="group" aria-label="Marker Mode">
|
||||
<input type="radio" class="btn-check" name="marker_mode" id="marker-mode-plant" value="plant" checked>
|
||||
<label class="btn btn-sm btn-outline-primary" for="marker-mode-plant">
|
||||
<i class="ti tabler-plant-2 me-1"></i> Ikon
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="marker_mode" id="marker-mode-dot" value="dot">
|
||||
<label class="btn btn-sm btn-outline-primary" for="marker-mode-dot">
|
||||
<i class="ti tabler-point me-1"></i> Titik
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body pt-4">
|
||||
<div id="map"></div>
|
||||
|
||||
<div class="alert map-note mt-3 mb-0">
|
||||
<i class="ti tabler-info-circle me-1"></i>
|
||||
Area hijau menunjukkan polygon lahan. Marker menunjukkan seluruh titik tanaman dalam kelompok ini.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header border-bottom">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="ti tabler-list-details me-1"></i> Daftar Titik Tanaman
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body pt-4">
|
||||
<div class="points-table-wrapper border rounded">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50px;">No</th>
|
||||
<th>Nama</th>
|
||||
<th>Latitude</th>
|
||||
<th>Longitude</th>
|
||||
<th>Kondisi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($points as $index => $point)
|
||||
@php
|
||||
$kondisi = $point->kondisi_tanaman;
|
||||
$kondisiValue = is_object($kondisi) && property_exists($kondisi, 'value')
|
||||
? $kondisi->value
|
||||
: $kondisi;
|
||||
|
||||
$conditionClass = match($kondisiValue) {
|
||||
'sehat' => 'success',
|
||||
'sakit' => 'danger',
|
||||
'mati' => 'dark',
|
||||
default => 'secondary',
|
||||
};
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
<td>
|
||||
<div class="fw-medium">{{ $point->nama ?? '-' }}</div>
|
||||
<small class="text-muted">{{ $point->uid ?? '-' }}</small>
|
||||
</td>
|
||||
<td>{{ $point->latitude ?? '-' }}</td>
|
||||
<td>{{ $point->longitude ?? '-' }}</td>
|
||||
<td>
|
||||
<span class="badge bg-label-{{ $conditionClass }}">
|
||||
{{ $kondisiValue ? ucfirst(str_replace('_', ' ', $kondisiValue)) : '-' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted py-4">
|
||||
Belum ada titik tanaman.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
$(document).ready(function () {
|
||||
const lahanPath = {{ Illuminate\Support\Js::from($tanaman->lahan->path ?? null) }};
|
||||
|
||||
const points = {{ Illuminate\Support\Js::from(
|
||||
$points->map(function ($point) {
|
||||
$kondisi = $point->kondisi_tanaman;
|
||||
$kondisiValue = is_object($kondisi) && property_exists($kondisi, 'value')
|
||||
? $kondisi->value
|
||||
: $kondisi;
|
||||
|
||||
return [
|
||||
'id' => $point->id,
|
||||
'uid' => $point->uid,
|
||||
'nama' => $point->nama,
|
||||
'latitude' => $point->latitude,
|
||||
'longitude' => $point->longitude,
|
||||
'kondisi_tanaman' => $kondisiValue,
|
||||
];
|
||||
})->values()
|
||||
) }};
|
||||
|
||||
const map = L.map('map').setView([-2.5489, 118.0149], 5);
|
||||
|
||||
L.tileLayer('https://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
|
||||
maxZoom: 22,
|
||||
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
|
||||
attribution: '© Google Maps Satellite'
|
||||
}).addTo(map);
|
||||
|
||||
let pointMarkers = [];
|
||||
|
||||
function parsePath(path) {
|
||||
if (!path) return null;
|
||||
|
||||
let parsed = path;
|
||||
|
||||
if (typeof path === 'string') {
|
||||
try {
|
||||
parsed = JSON.parse(path);
|
||||
} catch (error) {
|
||||
console.error('Path bukan JSON valid:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.type && parsed.coordinates) {
|
||||
return {
|
||||
type: 'geojson',
|
||||
data: parsed
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
return {
|
||||
type: 'array',
|
||||
data: parsed
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderLahanPolygon(path) {
|
||||
const parsed = parsePath(path);
|
||||
|
||||
if (!parsed) {
|
||||
console.warn('Format koordinat lahan tidak valid atau kosong.');
|
||||
return null;
|
||||
}
|
||||
|
||||
let layer = null;
|
||||
|
||||
if (parsed.type === 'geojson') {
|
||||
layer = L.geoJSON(parsed.data, {
|
||||
style: {
|
||||
color: '#28c76f',
|
||||
fillColor: '#28c76f',
|
||||
fillOpacity: 0.35,
|
||||
weight: 2
|
||||
}
|
||||
}).addTo(map);
|
||||
|
||||
map.fitBounds(layer.getBounds(), { padding: [30, 30] });
|
||||
}
|
||||
|
||||
if (parsed.type === 'array') {
|
||||
const latLngs = parsed.data.map(coord => [coord[1], coord[0]]);
|
||||
|
||||
layer = L.polygon(latLngs, {
|
||||
color: '#28c76f',
|
||||
fillColor: '#28c76f',
|
||||
fillOpacity: 0.35,
|
||||
weight: 2
|
||||
}).addTo(map);
|
||||
|
||||
map.fitBounds(layer.getBounds(), { padding: [30, 30] });
|
||||
}
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
function getMarkerMode() {
|
||||
return $('input[name="marker_mode"]:checked').val() || 'plant';
|
||||
}
|
||||
|
||||
function getPlantIcon(point) {
|
||||
const kondisi = point.kondisi_tanaman || 'sehat';
|
||||
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `
|
||||
<div class="plant-marker-icon ${kondisi}">
|
||||
<i class="ti tabler-plant-2"></i>
|
||||
</div>
|
||||
`,
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 16],
|
||||
popupAnchor: [0, -16]
|
||||
});
|
||||
}
|
||||
|
||||
function getDotIcon(point) {
|
||||
const kondisi = point.kondisi_tanaman || 'sehat';
|
||||
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="plant-dot-marker ${kondisi}"></div>`,
|
||||
iconSize: [11, 11],
|
||||
iconAnchor: [5.5, 5.5],
|
||||
popupAnchor: [0, -8]
|
||||
});
|
||||
}
|
||||
|
||||
function formatKondisiLabel(kondisi) {
|
||||
const labels = {
|
||||
sehat: 'Sehat',
|
||||
sakit: 'Sakit',
|
||||
mati: 'Mati'
|
||||
};
|
||||
|
||||
return labels[kondisi] || '-';
|
||||
}
|
||||
|
||||
function renderPointMarkers() {
|
||||
pointMarkers.forEach(marker => map.removeLayer(marker));
|
||||
pointMarkers = [];
|
||||
|
||||
const mode = getMarkerMode();
|
||||
|
||||
points.forEach((point, index) => {
|
||||
if (!point.latitude || !point.longitude) return;
|
||||
|
||||
const icon = mode === 'dot'
|
||||
? getDotIcon(point)
|
||||
: getPlantIcon(point);
|
||||
|
||||
const popupContent = `
|
||||
<div class="plant-popup-card">
|
||||
<div class="plant-popup-header">
|
||||
<p class="plant-popup-title">
|
||||
${point.nama || 'Titik Tanaman ' + (index + 1)}
|
||||
</p>
|
||||
<div class="plant-popup-subtitle">
|
||||
Titik Tanaman ${index + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plant-popup-body">
|
||||
<div class="plant-popup-row">
|
||||
<span class="plant-popup-label">Latitude</span>
|
||||
<span class="plant-popup-value">${point.latitude}</span>
|
||||
</div>
|
||||
|
||||
<div class="plant-popup-row">
|
||||
<span class="plant-popup-label">Longitude</span>
|
||||
<span class="plant-popup-value">${point.longitude}</span>
|
||||
</div>
|
||||
|
||||
<div class="plant-popup-row">
|
||||
<span class="plant-popup-label">Kondisi</span>
|
||||
<span class="plant-popup-value">
|
||||
<span class="plant-popup-badge ${point.kondisi_tanaman || 'sehat'}">
|
||||
${formatKondisiLabel(point.kondisi_tanaman)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const marker = L.marker([point.latitude, point.longitude], {
|
||||
icon: icon
|
||||
}).addTo(map)
|
||||
.bindPopup(popupContent, {
|
||||
className: 'custom-plant-popup',
|
||||
closeButton: true,
|
||||
maxWidth: 260
|
||||
});
|
||||
|
||||
pointMarkers.push(marker);
|
||||
});
|
||||
}
|
||||
|
||||
const lahanLayer = renderLahanPolygon(lahanPath);
|
||||
|
||||
renderPointMarkers();
|
||||
|
||||
if (points.length > 0) {
|
||||
const bounds = L.latLngBounds(
|
||||
points
|
||||
.filter(point => point.latitude && point.longitude)
|
||||
.map(point => [point.latitude, point.longitude])
|
||||
);
|
||||
|
||||
if (bounds.isValid()) {
|
||||
map.fitBounds(bounds, { padding: [40, 40] });
|
||||
}
|
||||
} else if (lahanLayer) {
|
||||
map.fitBounds(lahanLayer.getBounds(), { padding: [30, 30] });
|
||||
}
|
||||
|
||||
$(document).on('change', 'input[name="marker_mode"]', function () {
|
||||
renderPointMarkers();
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
map.invalidateSize();
|
||||
}, 300);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
Reference in New Issue
Block a user