pembaruan sig tps

This commit is contained in:
rahmagustin 2026-02-09 14:43:26 +07:00
parent d54ab1258e
commit ccf66298ab
6 changed files with 457 additions and 434 deletions

View File

@ -10,66 +10,47 @@
class TpsController extends Controller
{
public function index()
{
public function index()
{
$title = 'Data TPS';
// Ambil TPS + kategori + jumlah aduan
$tps = LokasiTps::with('kategori')
->withCount('aduan')
->get();
return view('admin.tps.index', compact('title', 'tps'));
}
}
public function create()
{
public function create()
{
$title = 'Tambah TPS';
$kategori = KategoriTps::all();
return view('admin.tps.create', compact('title', 'kategori'));
}
private function convertToDecimal($coordinate)
{
// decimal langsung
}
private function convertToDecimal($coordinate)
{
if (is_numeric($coordinate)) {
return (float) $coordinate;
}
$coordinate = html_entity_decode($coordinate);
$coordinate = strtoupper(trim($coordinate));
// ganti simbol jadi seragam
$coordinate = str_replace(
['°', "'", '"'],
[' ', ' ', ' '],
$coordinate
);
// ambil arah
preg_match('/([NSEW])/', $coordinate, $dirMatch);
if (!$dirMatch) return null;
$direction = $dirMatch[1];
// ambil angka
preg_match_all('/\d+(\.\d+)?/', $coordinate, $numbers);
if (count($numbers[0]) < 3) return null;
[$deg, $min, $sec] = array_map('floatval', $numbers[0]);
$decimal = $deg + ($min / 60) + ($sec / 3600);
if (in_array($direction, ['S', 'W'])) {
$decimal *= -1;
}
return $decimal;
}
public function store(Request $request)
{
}
public function store(Request $request)
{
$request->validate([
'kategori_tps_id' => 'required|exists:kategori_tps,id_kategori_tps',
'nama_tps' => 'required|string|max:255',
@ -81,20 +62,14 @@ public function store(Request $request)
'longitude' => 'required',
'foto_tps' => 'nullable|image|mimes:jpg,jpeg,png|max:2048',
]);
// 🔥 KONVERSI KOORDINAT
$latitude = $this->convertToDecimal($request->latitude);
$longitude = $this->convertToDecimal($request->longitude);
if ($latitude === null || $longitude === null) {
return back()->withErrors(['Koordinat tidak valid'])->withInput();
}
// Upload foto
$foto = $request->hasFile('foto_tps')
? $request->file('foto_tps')->store('foto-tps', 'public')
: null;
LokasiTps::create([
'kategori_tps_id' => $request->kategori_tps_id,
'nama_tps' => $request->nama_tps,
@ -106,26 +81,20 @@ public function store(Request $request)
'longitude' => $longitude,
'foto_tps' => $foto,
]);
return redirect()->route('admin.tps.index')
->with('success', 'Data TPS berhasil ditambahkan');
}
}
public function edit($id)
{
public function edit($id)
{
$title = 'Edit TPS';
$tps = LokasiTps::findOrFail($id);
$kategori = KategoriTps::all();
return view('admin.tps.edit', compact('title', 'tps', 'kategori'));
}
public function update(Request $request, $id)
{
}
public function update(Request $request, $id)
{
$tps = LokasiTps::findOrFail($id);
// VALIDASI
$request->validate([
'kategori_tps_id' => 'required|exists:kategori_tps,id_kategori_tps',
'nama_tps' => 'required|string|max:255',
@ -137,32 +106,21 @@ public function update(Request $request, $id)
'longitude' => 'required',
'foto_tps' => 'nullable|image|mimes:jpg,jpeg,png|max:4096',
]);
// 🔥 KONVERSI KOORDINAT (DMS / DECIMAL)
$latitude = $this->convertToDecimal($request->latitude);
$longitude = $this->convertToDecimal($request->longitude);
// Jika koordinat tidak valid
if ($latitude === null || $longitude === null) {
return back()
->withErrors(['Koordinat tidak valid. Gunakan format Decimal atau DMS.'])
->withInput();
}
// 📸 UPLOAD FOTO JIKA ADA
if ($request->hasFile('foto_tps')) {
// Hapus foto lama
if ($tps->foto_tps) {
Storage::disk('public')->delete($tps->foto_tps);
}
$foto = $request->file('foto_tps')->store('foto-tps', 'public');
} else {
$foto = $tps->foto_tps;
}
// 💾 UPDATE DATA
$tps->update([
'kategori_tps_id' => $request->kategori_tps_id,
'nama_tps' => $request->nama_tps,
@ -174,22 +132,18 @@ public function update(Request $request, $id)
'longitude' => $longitude,
'foto_tps' => $foto,
]);
return redirect()->route('admin.tps.index')
->with('success', 'Data TPS berhasil diperbarui');
}
}
public function destroy($id)
{
public function destroy($id)
{
$tps = LokasiTps::findOrFail($id);
if ($tps->foto_tps) {
Storage::disk('public')->delete($tps->foto_tps);
}
$tps->delete();
return redirect()->route('admin.tps.index')
->with('success', 'Data TPS berhasil dihapus');
}
}
}

View File

@ -8,22 +8,15 @@
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use App\Models\User;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
@ -32,31 +25,25 @@ public function rules(): array
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('username', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
public function authenticate(): void
{
$user = User::where('username', $this->username)->first();
if (!$user) {
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
'username' => 'Username tidak terdaftar.',
]);
}
RateLimiter::clear($this->throttleKey());
if (!Auth::attempt([
'username' => $this->username,
'password' => $this->password,
])) {
throw ValidationException::withMessages([
'password' => 'Password yang dimasukkan salah.',
]);
}
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
@ -68,18 +55,16 @@ public function ensureIsNotRateLimited(): void
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'username' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
return Str::transliterate(Str::lower($this->string('username')) . '|' . $this->ip());
}
}

View File

@ -3027,3 +3027,44 @@ .btn-aduan-tps:hover {
background: #bb2d3b; /* merah lebih gelap */
color: #fff;
}
.map-wrapper {
position: relative;
}
.map-action {
position: absolute;
top: 16px;
right: 16px;
z-index: 1000;
background: #fff;
padding: 12px 14px;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0,0,0,.15);
max-width: 260px;
}
.map-action-text {
font-size: 14px;
color: #333;
margin-bottom: 8px;
line-height: 1.4;
}
.btn-lokasi {
width: 100%;
border: none;
background: #00A86B;
color: #fff;
padding: 8px 12px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
.btn-lokasi:hover,
.btn-lokasi:focus:hover {
color: var(--contrast-color);
background: color-mix(in srgb, var(--accent-color), transparent 15%);
}

View File

@ -7,10 +7,10 @@
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="mb-3 d-flex justify-content-between align-items-center">
<div>
<h4 class="card-title mb-0">Data TPS</h4>
<p class="card-description mb-0">
<h4 class="mb-0 card-title">Data TPS</h4>
<p class="mb-0 card-description">
Daftar Tempat Pembuangan Sampah (TPS)
</p>
</div>
@ -20,7 +20,7 @@
</div>
<div class="table-responsive">
<table class="table">
<table class="table">
<thead>
<tr>
<th>Nama TPS</th>
@ -31,18 +31,12 @@
</tr>
</thead>
<tbody>
@forelse ($tps as $item)
<tr>
<!-- NAMA -->
<td>{{ $item->nama_tps }}</td>
<!-- KATEGORI -->
<td>
{{ $item->kategori->nama_kategori ?? '-' }}
</td>
<!-- FOTO -->
<td>
@if ($item->foto_tps)
<img src="{{ asset('storage/' . $item->foto_tps) }}" alt="Foto TPS"
@ -55,9 +49,6 @@
<span class="text-muted">-</span>
@endif
</td>
<!-- STATUS -->
<td>
@if ($item->status_tps == 'Aktif')
<label class="badge badge-success">Aktif</label>
@ -67,14 +58,11 @@
<label class="badge badge-warning">Pembangunan</label>
@endif
</td>
<!-- AKSI -->
<td class="text-center">
<a href="{{ route('admin.tps.edit', $item->id_tps) }}"
class="btn btn-warning btn-sm me-1">
<i class="bi bi-pencil-square"></i>
</a>
<form action="{{ route('admin.tps.destroy', $item->id_tps) }}" method="POST"
class="form-hapus" style="display:inline;">
@csrf
@ -83,7 +71,6 @@ class="form-hapus" style="display:inline;">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
@empty
@ -93,9 +80,8 @@ class="form-hapus" style="display:inline;">
</td>
</tr>
@endforelse
</tbody>
</table>
</table>
</div>
</div>

View File

@ -0,0 +1,67 @@
<?php
public function create()
{
$title = 'Tambah Data Sampah';
$users = User::all();
return view('admin.sampah.create', compact('title', 'users'));
}
public function store(Request $request)
{
$request->validate([
'tahun' => 'required|numeric',
'total_sampah' => 'required|numeric',
'total_kelola' => 'required|numeric',
'total_daur_ulang' => 'required|numeric',
]);
$sisa_sampah = $request->total_sampah
- ($request->total_kelola + $request->total_daur_ulang);
Sampah::create([
'user_id' => Auth::id(),
'tahun' => $request->tahun,
'total_sampah' => $request->total_sampah,
'total_kelola' => $request->total_kelola,
'total_daur_ulang' => $request->total_daur_ulang,
'sisa_sampah' => $sisa_sampah,
]);
return redirect()->route('admin.sampah.index')
->with('success', 'Data sampah berhasil ditambahkan');
}
public function edit($id)
{
$title = 'Edit Data Sampah';
$sampah = Sampah::findOrFail($id);
$users = User::all();
return view('admin.sampah.edit', compact('title', 'sampah', 'users'));
}
public function update(Request $request, $id)
{
$sampah = Sampah::findOrFail($id);
$request->validate([
'user_id' => 'required|exists:users,id',
'tahun' => 'required|numeric',
'total_sampah' => 'required|numeric',
'total_kelola' => 'required|numeric',
'total_daur_ulang' => 'required|numeric',
]);
$sisa_sampah = $request->total_sampah
- ($request->total_kelola + $request->total_daur_ulang);
$sampah->update([
'user_id' => $request->user_id,
'tahun' => $request->tahun,
'total_sampah' => $request->total_sampah,
'total_kelola' => $request->total_kelola,
'total_daur_ulang' => $request->total_daur_ulang,
'sisa_sampah' => $sisa_sampah,
]);
return redirect()->route('admin.sampah.index')
->with('success', 'Data sampah berhasil diperbarui');
}
public function destroy($id)
{
$sampah = Sampah::findOrFail($id);
$sampah->delete();
return redirect()->route('admin.sampah.index')
->with('success', 'Data sampah berhasil dihapus');
}

View File

@ -5,7 +5,9 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
header, .navbar, #header {
header,
.navbar,
#header {
position: sticky;
top: 0;
z-index: 2000 !important;
@ -22,20 +24,7 @@
width: 100%;
height: 450px;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0,0,0,.12);
}
.map-filter {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 12px;
align-items: center;
}
.map-info {
font-size: 14px;
color: #555;
box-shadow: 0 6px 20px rgba(0, 0, 0, .12);
}
.legend {
@ -43,7 +32,7 @@
padding: 10px 12px;
border-radius: 10px;
font-size: 14px;
box-shadow: 0 2px 12px rgba(0,0,0,.15);
box-shadow: 0 2px 12px rgba(0, 0, 0, .15);
line-height: 18px;
}
@ -53,6 +42,11 @@
float: left;
margin-right: 8px;
}
.user-location {
filter: drop-shadow(0 0 8px rgba(13,110,253,0.8))
drop-shadow(0 0 16px rgba(13,110,253,0.6));
}
</style>
<div class="page-title">
@ -69,37 +63,30 @@
<section class="section">
<div class="container">
<!-- INFO & BUTTON -->
<div class="map-filter">
<div class="map-info">
Klik tombol di samping untuk menemukan <b>TPS terdekat</b> dari lokasi Anda saat ini.
<div class="map-wrapper">
<div class="map-action">
<div class="map-action-text">
Temukan <b>TPS terdekat</b> dari lokasi Anda saat ini
</div>
<button id="btnLokasi" class="btn btn-primary btn-sm">
<button type="button" id="btnLokasi" class="btn-lokasi">
Cari TPS Terdekat
</button>
</div>
<div id="mapTPS"></div>
</div>
</div>
</section>
<script>
/* =============================
INIT MAP
============================= */
var map = L.map('mapTPS').setView([-7.6078, 111.903], 12);
var map = L.map('mapTPS').setView([-7.6078, 111.903], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(map);
}).addTo(map);
/* =============================
ICON TPS
============================= */
function icon(color) {
function icon(color) {
return new L.Icon({
iconUrl: `https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-${color}.png`,
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
@ -107,23 +94,24 @@ function icon(color) {
iconAnchor: [12, 41],
popupAnchor: [1, -34]
});
}
}
var iconTPS = icon('green');
var iconTPS3R = icon('blue');
var iconTPA = icon('red');
var iconNear = icon('yellow');
var iconTPS = icon('green');
var iconTPS3R = icon('blue');
var iconTPA = icon('red');
/* =============================
DATA TPS
============================= */
var tpsData = @json($tps);
var markers = [];
var iconNear = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-gold.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34]
});
/* =============================
MARKER TPS
============================= */
tpsData.forEach(tps => {
var tpsData = @json($tps);
var markers = [];
tpsData.forEach(tps => {
if (!tps.latitude || !tps.longitude) return;
let iconUse = iconTPS;
@ -134,7 +122,7 @@ function icon(color) {
.bindPopup(`
<strong>${tps.nama_tps}</strong><br>
<small>${tps.alamat_tps ?? '-'}</small><br>
<span class="mt-1 badge bg-success">${tps.status_tps ?? '-'}</span>
<span class="badge bg-success">${tps.status_tps ?? '-'}</span>
<hr style="margin:6px 0">
<a href="/tps/${tps.id_tps}" style="font-size:13px">Detail TPS</a>
`)
@ -142,13 +130,11 @@ function icon(color) {
marker.tpsData = tps;
markers.push(marker);
});
});
/* =============================
LEGEND
============================= */
var legend = L.control({ position: 'bottomleft' });
legend.onAdd = function () {
/* ===== LEGEND KATEGORI (BALIK 😤) ===== */
var legend = L.control({ position: 'bottomleft' });
legend.onAdd = function () {
var div = L.DomUtil.create('div', 'legend');
div.innerHTML = `
<strong>Kategori TPS</strong><br>
@ -157,28 +143,22 @@ function icon(color) {
<i style="background:#dc3545"></i> TPA
`;
return div;
};
legend.addTo(map);
};
legend.addTo(map);
/* =============================
HITUNG JARAK
============================= */
function getDistance(lat1, lon1, lat2, lon2) {
function getDistance(lat1, lon1, lat2, lon2) {
const R = 6371;
const dLat = (lat2-lat1) * Math.PI/180;
const dLon = (lon2-lon1) * Math.PI/180;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a =
Math.sin(dLat/2)**2 +
Math.cos(lat1*Math.PI/180) *
Math.cos(lat2*Math.PI/180) *
Math.sin(dLon/2)**2;
return R * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)));
}
Math.sin(dLat / 2) ** 2 +
Math.cos(lat1 * Math.PI / 180) *
Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) ** 2;
return R * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)));
}
/* =============================
TPS TERDEKAT
============================= */
document.getElementById('btnLokasi').addEventListener('click', function () {
document.getElementById('btnLokasi').addEventListener('click', function () {
if (!navigator.geolocation) {
alert('Browser tidak mendukung GPS');
@ -189,27 +169,37 @@ function getDistance(lat1, lon1, lat2, lon2) {
let userLat = pos.coords.latitude;
let userLng = pos.coords.longitude;
// LINGKARAN BIRU (USER)
L.circleMarker([userLat, userLng], {
radius: 8,
radius: 6,
color: '#0d6efd',
fillColor: '#0d6efd',
fillOpacity: 0.8
fillOpacity: 1,
className: 'user-location'
}).addTo(map)
.bindPopup('Lokasi Anda')
.openPopup();
setInterval(() => {
let pulse = L.circle([userLat, userLng], {
radius: 40,
color: '#0d6efd',
fillColor: '#0d6efd',
fillOpacity: 0.35,
weight: 0
}).addTo(map);
setTimeout(() => map.removeLayer(pulse), 1000);
}, 1200);
let nearest = null;
let minDist = Infinity;
markers.forEach(m => {
let tps = m.tpsData;
let dist = getDistance(userLat, userLng, tps.latitude, tps.longitude);
if (dist < minDist) {
minDist = dist;
nearest = m;
nearest.distance = dist;
}
});
@ -219,6 +209,6 @@ function getDistance(lat1, lon1, lat2, lon2) {
map.setView(nearest.getLatLng(), 15);
}
});
});
});
</script>
@endsection