TTK_E32222585_laravel/resources/views/dashboard/location.blade.php

817 lines
31 KiB
PHP

@extends('dashboard.base')
@section('title', 'Manajemen Lokasi')
@push('css')
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
@endpush
@section('content')
<div class="content-wrapper">
<div class="row">
<div class="col-md-12 grid-margin">
<div class="row">
<div class="col-12 col-xl-8 mb-4 mb-xl-0">
<h3 class="font-weight-bold">Manajemen Lokasi</h3>
<h6 class="font-weight-normal mb-0">Kelola data lokasi berbasis radius (titik pusat & radius meter)</h6>
</div>
<div class="col-12 col-xl-4">
<button type="button" class="btn btn-primary btn-sm float-right" data-toggle="modal" data-target="#addLocationModal">
<i class="mdi mdi-plus"></i> Tambah Lokasi
</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
<label>Cari Lokasi:</label>
<input type="text" class="form-control" id="searchLocation" placeholder="Cari berdasarkan nama lokasi...">
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped" id="locationsTable">
<thead>
<tr>
<th>No</th>
<th>Nama Lokasi</th>
<th>Latitude</th>
<th>Longitude</th>
<th>Radius (m)</th>
<th>Tanggal Upload</th>
<th>Aksi</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="addLocationModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Tambah Lokasi Radius</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form id="addLocationForm">
<div class="modal-body">
<div class="form-group">
<label>Nama Lokasi <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name" required>
</div>
<div class="form-group">
<label>Latitude <span class="text-danger">*</span></label>
<input type="number" step="any" class="form-control" name="center_lat" required placeholder="Contoh: -8.123456">
<small class="form-text text-muted">Gunakan titik, minimal 4 digit di belakang koma. Contoh: -8.1234</small>
</div>
<div class="form-group">
<label>Longitude <span class="text-danger">*</span></label>
<input type="number" step="any" class="form-control" name="center_lng" required placeholder="Contoh: 114.123456">
<small class="form-text text-muted">Gunakan titik, minimal 4 digit di belakang koma. Contoh: 114.1234</small>
</div>
<div class="form-group">
<label>Radius (meter) <span class="text-danger">*</span></label>
<input type="number" class="form-control" name="radius" min="1" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">
<i class="mdi mdi-upload"></i> Simpan
</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="editLocationModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Lokasi Radius</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form id="editLocationForm">
<input type="hidden" name="location_name" id="editLocationName">
<div class="modal-body">
<div class="form-group">
<label>Latitude <span class="text-danger">*</span></label>
<input type="number" step="any" class="form-control" name="center_lat" id="editCenterLat" required placeholder="Contoh: -8.123456">
<small class="form-text text-muted">Gunakan titik, minimal 4 digit di belakang koma. Contoh: -8.1234</small>
</div>
<div class="form-group">
<label>Longitude <span class="text-danger">*</span></label>
<input type="number" step="any" class="form-control" name="center_lng" id="editCenterLng" required placeholder="Contoh: 114.123456">
<small class="form-text text-muted">Gunakan titik, minimal 4 digit di belakang koma. Contoh: 114.1234</small>
</div>
<div class="form-group">
<label>Radius (meter) <span class="text-danger">*</span></label>
<input type="number" class="form-control" name="radius" id="editRadius" min="1" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-warning">
<i class="mdi mdi-refresh"></i> Update
</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="detailLocationModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Detail Lokasi</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6>Informasi Lokasi</h6>
<table class="table table-borderless">
<tr>
<td><strong>Nama:</strong></td>
<td id="detailName"></td>
</tr>
<tr>
<td><strong>Latitude:</strong></td>
<td id="detailLat"></td>
</tr>
<tr>
<td><strong>Longitude:</strong></td>
<td id="detailLng"></td>
</tr>
<tr>
<td><strong>Radius:</strong></td>
<td id="detailRadius"></td>
</tr>
</table>
</div>
<div class="col-md-6">
<h6>Aksi</h6>
<button type="button" class="btn btn-info btn-sm" onclick="exportLocationData()">
<i class="mdi mdi-download"></i> Export JSON
</button>
<button type="button" class="btn btn-success btn-sm" onclick="viewOnMap()">
<i class="mdi mdi-map"></i> Lihat di Peta
</button>
</div>
</div>
<hr>
<div class="row">
<div class="col-12">
<div id="radiusMapPreview" style="height: 300px;"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="formatHelpModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Format File yang Didukung</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<div class="accordion" id="formatAccordion">
<div class="card">
<div class="card-header p-2">
<h6 class="mb-0">
<button class="btn btn-link text-left w-100" type="button" data-toggle="collapse" data-target="#geojsonFormat">
<strong>1. Format GeoJSON dari QGIS</strong>
</button>
</h6>
</div>
<div id="geojsonFormat" class="collapse show" data-parent="#formatAccordion">
<div class="card-body bg-light">
<pre class="font-monospace"><code>{
"type": "FeatureCollection",
"name": "Nama Lokasi",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[106.8456, -6.2144],
[106.8556, -6.2144],
[106.8556, -6.2044],
[106.8456, -6.2044],
[106.8456, -6.2144]
]
]
}
}
]
}</code></pre>
</div>
</div>
</div>
<div class="card">
<div class="card-header p-2">
<h6 class="mb-0">
<button class="btn btn-link text-left w-100 collapsed" type="button" data-toggle="collapse" data-target="#jsonFormat">
<strong>2. Format JSON Biasa</strong>
</button>
</h6>
</div>
<div id="jsonFormat" class="collapse" data-parent="#formatAccordion">
<div class="card-body bg-light">
<pre class="font-monospace"><code>{
"name": "Nama Lokasi",
"coordinates": [
{ "lat": -6.2144, "lng": 106.8456 },
{ "lat": -6.2144, "lng": 106.8556 },
{ "lat": -6.2044, "lng": 106.8556 },
{ "lat": -6.2044, "lng": 106.8456 },
{ "lat": -6.2144, "lng": 106.8456 }
]
}</code></pre>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="mapDetailModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="mdi mdi-map-marker"></i> Peta Lokasi:
<span id="mapLocationName" class="text-primary"></span>
</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body p-0">
<div class="row no-gutters">
<div class="col-md-8" style="position: relative;">
<div id="locationMap"
style="
height: 400px;
border: 1px solid rgb(0, 0, 0);
position: absolute;
width: 100%;
top: 50%;
transform: translate(-50%,-50%);
left: 50%;
">
</div>
</div>
<div class="col-md-4">
<div class="p-3">
<h6 class="mb-3">Informasi Lokasi</h6>
<div class="mb-3">
<small class="text-muted d-block">Nama Lokasi</small>
<strong id="mapInfoName">-</strong>
</div>
<div class="mb-3">
<small class="text-muted d-block">Total Polygon</small>
<span id="mapInfoPolygons" class="badge badge-info">0</span>
</div>
<div class="mb-3">
<small class="text-muted d-block">Total Koordinat</small>
<span id="mapInfoCoordinates" class="badge badge-success">0</span>
</div>
<hr>
<h6 class="mb-3">Kontrol Peta</h6>
<div class="btn-group-vertical btn-block mb-3">
<button type="button" class="btn btn-sm btn-outline-primary" onclick="fitMapToBounds()">
<i class="mdi mdi-fit-to-page-outline"></i> Fit ke Polygon
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="togglePolygonLabels()">
<i class="mdi mdi-label"></i> Toggle Label
</button>
<button type="button" class="btn btn-sm btn-outline-info" onclick="changeMapLayer()">
<i class="mdi mdi-layers"></i> Ganti Layer
</button>
</div>
<div class="alert alert-light p-2">
<small class="text-muted">
<i class="mdi mdi-information-outline"></i>
Klik pada polygon untuk melihat detail koordinat
</small>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-info btn-sm" onclick="exportLocationData()">
<i class="mdi mdi-download"></i> Export Data
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div>
@endsection
@push('script')
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<script>
let locationsTable;
let currentLocationData = null;
$(document).ready(function() {
initDataTable();
});
function initDataTable() {
locationsTable = $('#locationsTable').DataTable({
processing: true,
serverSide: true,
responsive: true,
dom: '<"top"l>rt<"bottom"ip><"clear">',
ajax: {
url: '/api/admin/locations',
headers: {
'Authorization': getAuthorizationHeader(),
'Accept': 'application/json'
},
data: function(d) {
d.search.value = $('#searchLocation').val();
}
},
columns: [
{
data: null,
orderable: false,
searchable: false,
render: function(data, type, row, meta) {
return meta.row + meta.settings._iDisplayStart + 1;
}
},
{
data: 'name',
name: 'name',
render: function(data) {
return `<span class="badge badge-primary">${data}</span>`;
}
},
{
data: 'center_lat',
name: 'center_lat',
render: function(data) {
return `<span class="text-info">${data}</span>`;
}
},
{
data: 'center_lng',
name: 'center_lng',
render: function(data) {
return `<span class="text-info">${data}</span>`;
}
},
{
data: 'radius',
name: 'radius',
render: function(data) {
return `<span class="text-info">${data} meter</span>`;
}
},
{
data: 'created_at',
name: 'created_at',
render: function(data) {
return new Date(data).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
},
{
data: 'name',
orderable: false,
searchable: false,
render: function(data, type, row) {
return `
<div class="btn-group" role="group">
<button class="btn btn-sm btn-success" onclick="showLocationMap('${data}')" title="Lihat di Peta">
<i class="ti ti-map"></i>
</button>
<button class="btn btn-sm btn-info" onclick="viewLocation('${data}')" title="Detail">
<i class="ti ti-eye"></i>
</button>
<button class="btn btn-sm btn-warning" onclick="editLocation('${data}')" title="Edit">
<i class="ti ti-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deleteLocation('${data}')" title="Hapus">
<i class="ti ti-trash"></i>
</button>
</div>
`;
}
}
]
});
$('#searchLocation').on('keyup', function() {
locationsTable.ajax.reload();
});
}
$('#addLocationForm').submit(function(e) {
e.preventDefault();
const formData = $(this).serialize();
const submitBtn = $(this).find('button[type="submit"]');
const originalText = submitBtn.html();
submitBtn.html('<i class="mdi mdi-loading mdi-spin"></i> Menyimpan...').prop('disabled', true);
$.ajax({
url: '/api/admin/locations',
type: 'POST',
data: formData,
headers: {
'Authorization': getAuthorizationHeader(),
'Accept': 'application/json'
},
success: function(response) {
$('#addLocationModal').modal('hide');
$('#addLocationForm')[0].reset();
locationsTable.ajax.reload();
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: response.message,
html: `
<p>${response.message}</p>
<p><strong>Nama:</strong> ${response.data.name}</p>
<p><strong>Latitude:</strong> ${response.data.center_lat}</p>
<p><strong>Longitude:</strong> ${response.data.center_lng}</p>
<p><strong>Radius:</strong> ${response.data.radius} meter</p>
`
});
},
error: function(xhr) {
Swal.fire({
icon: 'error',
title: 'Gagal!',
text: xhr.responseJSON.message || 'Terjadi kesalahan saat menyimpan lokasi'
});
},
complete: function() {
submitBtn.html(originalText).prop('disabled', false);
}
});
});
function viewLocation(name) {
$.ajax({
url: `/api/admin/locations/${name}`,
type: 'GET',
headers: {
'Authorization': getAuthorizationHeader(),
'Accept': 'application/json'
},
success: function(response) {
currentLocationData = response.data;
$('#detailName').text(response.data.name);
$('#detailLat').text(response.data.center_lat);
$('#detailLng').text(response.data.center_lng);
$('#detailRadius').text(response.data.radius + ' meter');
$('#detailLocationModal').modal('show');
renderRadiusMapPreview(response.data);
},
error: function(xhr) {
Swal.fire({
icon: 'error',
title: 'Gagal!',
text: 'Gagal mengambil detail lokasi'
});
}
});
}
function renderRadiusMapPreview(data) {
setTimeout(function() {
let mapPreview = L.map('radiusMapPreview').setView([data.center_lat, data.center_lng], 16);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 18
}).addTo(mapPreview);
L.circle([data.center_lat, data.center_lng], {
color: '#007bff',
fillColor: '#007bff',
fillOpacity: 0.2,
radius: data.radius
}).addTo(mapPreview);
mapPreview.invalidateSize();
}, 200);
}
function editLocation(name) {
$.ajax({
url: `/api/admin/locations/${name}`,
type: 'GET',
headers: {
'Authorization': getAuthorizationHeader(),
'Accept': 'application/json'
},
success: function(response) {
currentLocationData = response.data;
$('#editLocationName').val(name);
$('#editCenterLat').val(response.data.center_lat);
$('#editCenterLng').val(response.data.center_lng);
$('#editRadius').val(response.data.radius);
$('#editLocationModal').modal('show');
},
error: function(xhr) {
Swal.fire({
icon: 'error',
title: 'Gagal!',
text: 'Gagal mengambil data lokasi untuk edit'
});
}
});
}
$('#editLocationForm').submit(function(e) {
e.preventDefault();
const locationName = $('#editLocationName').val();
const formData = $(this).serialize();
const submitBtn = $(this).find('button[type="submit"]');
const originalText = submitBtn.html();
submitBtn.html('<i class="mdi mdi-loading mdi-spin"></i> Updating...').prop('disabled', true);
$.ajax({
url: `/api/admin/locations/${locationName}`,
type: 'POST',
data: formData,
headers: {
'Authorization': getAuthorizationHeader(),
'Accept': 'application/json',
'X-HTTP-Method-Override': 'PUT'
},
success: function(response) {
$('#editLocationModal').modal('hide');
$('#editLocationForm')[0].reset();
locationsTable.ajax.reload();
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: response.message,
html: `
<p>${response.message}</p>
<p><strong>Nama:</strong> ${response.data.name}</p>
<p><strong>Latitude:</strong> ${response.data.center_lat}</p>
<p><strong>Longitude:</strong> ${response.data.center_lng}</p>
<p><strong>Radius:</strong> ${response.data.radius} meter</p>
`
});
},
error: function(xhr) {
Swal.fire({
icon: 'error',
title: 'Gagal!',
text: xhr.responseJSON.message || 'Terjadi kesalahan saat update lokasi'
});
},
complete: function() {
submitBtn.html(originalText).prop('disabled', false);
}
});
});
function deleteLocation(name) {
Swal.fire({
title: 'Konfirmasi Hapus',
text: `Apakah Anda yakin ingin menghapus lokasi "${name}"?`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Ya, Hapus!',
cancelButtonText: 'Batal'
}).then((result) => {
if (result.isConfirmed) {
$.ajax({
url: `/api/admin/locations/${name}`,
type: 'DELETE',
headers: {
'Authorization': getAuthorizationHeader(),
'Accept': 'application/json'
},
success: function(response) {
locationsTable.ajax.reload();
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: response.message
});
},
error: function(xhr) {
Swal.fire({
icon: 'error',
title: 'Gagal!',
text: xhr.responseJSON.message || 'Gagal menghapus lokasi'
});
}
});
}
});
}
function exportLocationData() {
if (currentLocationData) {
const dataStr = JSON.stringify(currentLocationData, null, 2);
const dataBlob = new Blob([dataStr], {type: 'application/json'});
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `location_${currentLocationData.name}.json`;
link.click();
URL.revokeObjectURL(url);
}
}
function viewOnMap() {
if (currentLocationData) {
$('#detailLocationModal').modal('hide');
showLocationMap(currentLocationData.name);
}
}
// MAP
var map = null;
let radiusLayer = null;
let currentMapLayer = 0;
const mapLayers = [
{
name: 'OpenStreetMap',
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '© OpenStreetMap contributors'
},
{
name: 'Satellite',
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
attribution: 'Tiles © Esri'
},
{
name: 'Terrain',
url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
attribution: '© OpenTopoMap contributors'
}
];
function initializeMap() {
map = L.map('locationMap').setView([0, 0], 2);
L.tileLayer(mapLayers[currentMapLayer].url, {
attribution: mapLayers[currentMapLayer].attribution,
maxZoom: 18
}).addTo(map);
setTimeout(() => {
map.invalidateSize();
}, 100);
}
initializeMap();
function showLocationMap(name) {
$.ajax({
url: `/api/admin/locations/${name}`,
type: 'GET',
headers: {
'Authorization': getAuthorizationHeader(),
'Accept': 'application/json'
},
success: function(response) {
currentLocationData = response.data;
$('#mapLocationName').text(response.data.name);
$('#mapDetailModal').modal('show');
},
error: function(xhr) {
Swal.fire({
icon: 'error',
title: 'Gagal!',
text: 'Gagal mengambil data lokasi untuk peta'
});
}
});
}
$('#mapDetailModal').on('shown.bs.modal', function () {
if (map) {
setTimeout(() => {
map.invalidateSize();
if (currentLocationData) {
displayLocationOnMap(currentLocationData);
}
}, 100);
} else {
initializeMap();
}
});
function displayLocationOnMap(locationData) {
clearMapLayers();
if (!locationData) return;
$('#mapInfoName').text(locationData.name);
$('#mapInfoCoordinates').text('-');
$('#mapInfoPolygons').text('-');
if (locationData.center_lat && locationData.center_lng && locationData.radius) {
map.setView([locationData.center_lat, locationData.center_lng], 16);
radiusLayer = L.circle([locationData.center_lat, locationData.center_lng], {
color: '#007bff',
fillColor: '#007bff',
fillOpacity: 0.2,
radius: locationData.radius
}).addTo(map);
}
}
function clearMapLayers() {
if (radiusLayer && map) {
map.removeLayer(radiusLayer);
radiusLayer = null;
}
}
function changeMapLayer() {
currentMapLayer = (currentMapLayer + 1) % mapLayers.length;
const layer = mapLayers[currentMapLayer];
map.eachLayer(function(layerObj) {
if (layerObj instanceof L.TileLayer) {
map.removeLayer(layerObj);
}
});
L.tileLayer(layer.url, {
attribution: layer.attribution,
maxZoom: 18
}).addTo(map);
const toast = $(`
<div class="toast" style="position: fixed; top: 20px; right: 20px; z-index: 9999;">
<div class="toast-body bg-info text-white">
Layer: ${layer.name}
</div>
</div>
`);
$('body').append(toast);
toast.fadeIn().delay(2000).fadeOut(function() {
$(this).remove();
});
}
$('input[name="center_lat"], input[name="center_lng"]').on('blur', function() {
let val = $(this).val();
if (!/^[-+]?[0-9]{1,3}\.[0-9]{4,}$/.test(val)) {
alert('Masukkan koordinat desimal minimal 4 digit di belakang koma!');
$(this).val('');
}
});
</script>
@endpush