Files
Verde-Web/resources/views/admin/dumpsites.blade.php

309 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('admin.layouts.app', ['pageTitle' => 'Dumpsites'])
@section('page')
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
<div class="mx-auto max-w-7xl">
<header class="mb-6 flex items-end justify-between">
<div>
<h2 class="text-2xl font-semibold tracking-tight text-neutral-900">Dumpsites</h2>
<p class="mt-1 text-sm text-neutral-500">Final disposal facilities with geofence boundaries.</p>
</div>
<button id="new-btn" class="btn-primary">+ New Dumpsite</button>
</header>
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Code</th>
<th>Capacity</th>
<th>Permit #</th>
<th>Boundary</th>
<th>Status</th>
</tr>
</thead>
<tbody id="rows">
<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>
</tbody>
</table>
</div>
</div>
<div id="form-modal" class="hidden">
<div class="slide-over-mask" data-close></div>
<div class="slide-over-panel translate-x-0 p-6 overflow-y-auto">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-base font-semibold text-neutral-900">New dumpsite</h3>
<button data-close class="text-neutral-400 hover:text-neutral-600">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<form id="create-form" class="space-y-4">
<div><label class="form-label">Name</label><input name="name" required class="form-input"></div>
<div><label class="form-label">Code</label><input name="code" required class="form-input" placeholder="DS-..."></div>
<div><label class="form-label">Address</label><input name="address_line" required class="form-input"></div>
<div class="grid grid-cols-2 gap-3">
<div><label class="form-label">Latitude</label><input name="lat" id="ds-lat" type="number" step="0.000001" required class="form-input bg-neutral-100 cursor-not-allowed" readonly></div>
<div><label class="form-label">Longitude</label><input name="lng" id="ds-lng" type="number" step="0.000001" required class="form-input bg-neutral-100 cursor-not-allowed" readonly></div>
</div>
<div class="space-y-1">
<label class="form-label text-neutral-700">Search Address / Place</label>
<div class="flex gap-2">
<input type="text" id="ds-search" class="form-input flex-1 text-sm" placeholder="Search a place (e.g. Payatas, Quezon City...)">
<button type="button" id="ds-search-btn" class="btn-primary px-3 py-2 text-xs">Search</button>
</div>
<div class="relative">
<ul id="ds-search-results" class="absolute z-50 w-full rounded border border-neutral-200 bg-white shadow-md max-h-40 overflow-y-auto hidden"></ul>
</div>
</div>
<div>
<label class="form-label text-neutral-700">Pin Location on Map</label>
<div id="map-picker" class="h-48 w-full rounded border border-neutral-200" style="min-height: 200px; z-index: 0;"></div>
<p class="mt-1 text-xs text-neutral-500">Click anywhere on the map or drag the marker to auto-fill coordinates.</p>
</div>
<div>
<div class="flex items-center justify-between">
<label class="form-label mb-0">Boundary polygon (geofence)</label>
<button type="button" id="add-point" class="text-xs font-medium text-verde-700 hover:text-verde-800">+ Add point</button>
</div>
<p class="mb-2 mt-1 text-xs text-neutral-500">At least 3 points. Ring auto-closes.</p>
<div id="boundary-points" class="space-y-2"></div>
</div>
<div><label class="form-label">Capacity (tons)</label><input name="capacity_tons" type="number" min="0" class="form-input"></div>
<div><label class="form-label">Permit number</label><input name="permit_number" class="form-input" placeholder="DENR-..."></div>
<div><label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="active">Active</option>
<option value="maintenance">Maintenance</option>
<option value="closed">Closed</option>
</select>
</div>
<div class="flex justify-end gap-2 pt-2">
<button type="button" data-close class="btn-ghost">Cancel</button>
<button type="submit" class="btn-primary">Create</button>
</div>
</form>
</div>
</div>
<script type="module">
const rows = document.getElementById('rows');
const modal = document.getElementById('form-modal');
const pointsEl = document.getElementById('boundary-points');
function badge(s) {
const m = { active: 'badge-active', maintenance: 'badge-pending', closed: 'badge-rejected' };
return `<span class="badge ${m[s] || 'badge-inactive'}">${s}</span>`;
}
async function load() {
rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>`;
const res = await window.Verde.apiFetch('/api/v1/admin/dumpsites?per_page=50');
if (!res.ok) { rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-red-500">Failed to load.</td></tr>`; return; }
const items = res.body.data ?? [];
if (items.length === 0) { rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">No dumpsites configured.</td></tr>`; return; }
rows.innerHTML = items.map(d => `
<tr>
<td class="font-medium text-neutral-900">${window.Verde.escapeHtml(d.name)}</td>
<td class="font-mono text-xs text-neutral-600">${window.Verde.escapeHtml(d.code)}</td>
<td>${d.capacity_tons ? `${d.capacity_tons} t` : '—'}</td>
<td class="font-mono text-xs">${window.Verde.escapeHtml(d.permit_number ?? '—')}</td>
<td>${d.boundary_polygon && d.boundary_polygon.length > 0
? `<span class="badge badge-active">${d.boundary_polygon.length - 1} pts</span>`
: '<span class="badge badge-inactive">None</span>'}</td>
<td>${badge(d.status)}</td>
</tr>
`).join('');
}
function addPointRow(lat = '', lng = '') {
const div = document.createElement('div');
div.className = 'flex gap-2';
div.innerHTML = `
<input type="number" step="0.000001" placeholder="lat" value="${lat}" class="form-input flex-1 boundary-lat" required>
<input type="number" step="0.000001" placeholder="lng" value="${lng}" class="form-input flex-1 boundary-lng" required>
<button type="button" class="btn-ghost px-2 remove-point">×</button>
`;
div.querySelector('.remove-point').addEventListener('click', () => div.remove());
pointsEl.appendChild(div);
}
function getBoundary() {
const pts = [];
pointsEl.querySelectorAll('.flex').forEach(row => {
const lat = row.querySelector('.boundary-lat').value;
const lng = row.querySelector('.boundary-lng').value;
if (lat && lng) pts.push({ lat: parseFloat(lat), lng: parseFloat(lng) });
});
return pts;
}
function awaitLeaflet() {
return new Promise(resolve => {
const tick = () => window.L ? resolve(window.L) : setTimeout(tick, 50);
tick();
});
}
let map = null;
let marker = null;
const latInput = document.getElementById('ds-lat');
const lngInput = document.getElementById('ds-lng');
async function initMap(initialLat = 14.6539, initialLng = 121.0685) {
if (map) {
if (marker) {
marker.setLatLng([initialLat, initialLng]);
}
map.setView([initialLat, initialLng], 13);
setTimeout(() => map.invalidateSize(), 50);
return;
}
const L = await awaitLeaflet();
map = L.map('map-picker').setView([initialLat, initialLng], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap',
maxZoom: 19,
}).addTo(map);
marker = L.marker([initialLat, initialLng], { draggable: true }).addTo(map);
marker.on('dragend', function () {
const pos = marker.getLatLng();
latInput.value = pos.lat.toFixed(6);
lngInput.value = pos.lng.toFixed(6);
});
map.on('click', function (e) {
marker.setLatLng(e.latlng);
latInput.value = e.latlng.lat.toFixed(6);
lngInput.value = e.latlng.lng.toFixed(6);
});
// Setup geocoding search
const searchInput = document.getElementById('ds-search');
const searchBtn = document.getElementById('ds-search-btn');
const resultsList = document.getElementById('ds-search-results');
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
const searchPlace = async () => {
const query = searchInput.value.trim();
if (!query) {
resultsList.innerHTML = '';
resultsList.classList.add('hidden');
return;
}
searchBtn.disabled = true;
searchBtn.textContent = '...';
resultsList.innerHTML = '';
resultsList.classList.add('hidden');
try {
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=5`);
if (!res.ok) throw new Error('Search failed');
const data = await res.json();
if (data.length === 0) {
resultsList.innerHTML = `<li class="px-3 py-2 text-xs text-neutral-400">No places found.</li>`;
resultsList.classList.remove('hidden');
return;
}
resultsList.innerHTML = data.map(item => `
<li data-lat="${item.lat}" data-lng="${item.lon}" class="px-3 py-2 text-xs border-b border-neutral-100 cursor-pointer hover:bg-neutral-50 text-neutral-800 last:border-b-0">
${window.Verde.escapeHtml(item.display_name)}
</li>
`).join('');
resultsList.classList.remove('hidden');
resultsList.querySelectorAll('li[data-lat]').forEach(li => {
li.addEventListener('click', () => {
const lat = parseFloat(li.dataset.lat);
const lng = parseFloat(li.dataset.lng);
latInput.value = lat.toFixed(6);
lngInput.value = lng.toFixed(6);
marker.setLatLng([lat, lng]);
map.setView([lat, lng], 15);
resultsList.classList.add('hidden');
searchInput.value = '';
});
});
} catch (err) {
resultsList.innerHTML = `<li class="px-3 py-2 text-xs text-red-500">Error searching.</li>`;
resultsList.classList.remove('hidden');
} finally {
searchBtn.disabled = false;
searchBtn.textContent = 'Search';
}
};
searchBtn.onclick = (e) => { e.preventDefault(); searchPlace(); };
searchInput.onkeydown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
searchPlace();
}
};
searchInput.addEventListener('input', debounce(searchPlace, 300));
const dismissSearch = (e) => {
if (!searchInput.contains(e.target) && !searchBtn.contains(e.target) && !resultsList.contains(e.target)) {
resultsList.classList.add('hidden');
}
};
document.removeEventListener('click', dismissSearch);
document.addEventListener('click', dismissSearch);
setTimeout(() => map.invalidateSize(), 100);
}
document.getElementById('new-btn').addEventListener('click', () => {
pointsEl.innerHTML = '';
addPointRow(); addPointRow(); addPointRow();
latInput.value = '14.653900';
lngInput.value = '121.068500';
modal.classList.remove('hidden');
initMap(14.6539, 121.0685);
});
modal.querySelectorAll('[data-close]').forEach(el => el.addEventListener('click', () => modal.classList.add('hidden')));
document.getElementById('add-point').addEventListener('click', () => addPointRow());
document.getElementById('create-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const payload = Object.fromEntries(fd.entries());
payload.lat = parseFloat(payload.lat);
payload.lng = parseFloat(payload.lng);
if (payload.capacity_tons) payload.capacity_tons = parseInt(payload.capacity_tons, 10);
else delete payload.capacity_tons;
const boundary = getBoundary();
if (boundary.length >= 3) payload.boundary_polygon = boundary;
const res = await window.Verde.apiFetch('/api/v1/admin/dumpsites', {
method: 'POST',
body: JSON.stringify(payload),
});
if (res.ok) { window.Verde.toast('Dumpsite created', 'success'); modal.classList.add('hidden'); e.target.reset(); load(); }
else window.Verde.toast(res.body?.message ?? 'Create failed', 'error');
});
load();
</script>
@endsection