566 lines
24 KiB
PHP
566 lines
24 KiB
PHP
@extends('admin.layouts.app', ['pageTitle' => 'Drop-off Points'])
|
|
|
|
@section('page')
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="">
|
|
<link rel="stylesheet" href="https://unpkg.com/@geoman-io/leaflet-geoman-free@2.14.2/dist/leaflet-geoman.css" />
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
|
<script src="https://unpkg.com/@geoman-io/leaflet-geoman-free@2.14.2/dist/leaflet-geoman.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></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">Drop-off Points</h2>
|
|
<p class="mt-1 text-sm text-neutral-500">Where residents bring trash for collection.</p>
|
|
</div>
|
|
<button id="new-btn" class="btn-primary">+ New DOP</button>
|
|
</header>
|
|
|
|
<div class="card mb-4 flex flex-wrap items-center gap-3 p-4">
|
|
<select id="filter-status" class="form-select w-44">
|
|
<option value="">All statuses</option>
|
|
<option value="active">Active</option>
|
|
<option value="maintenance">Maintenance</option>
|
|
<option value="closed">Closed</option>
|
|
</select>
|
|
<input id="filter-q" type="search" placeholder="Search name, code, address…" class="form-input flex-1 min-w-[200px]">
|
|
<button id="filter-apply" class="btn-primary">Apply</button>
|
|
</div>
|
|
|
|
<div class="table-wrap">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>LGU</th>
|
|
<th>Code</th>
|
|
<th>Address</th>
|
|
<th>Capacity</th>
|
|
<th>Status</th>
|
|
<th class="text-right">Actions</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 id="modal-title" class="text-base font-semibold text-neutral-900">New drop-off point</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">
|
|
<input type="hidden" name="geofence_wkt" id="geofence_wkt">
|
|
<div>
|
|
<label class="form-label">Name</label>
|
|
<input name="name" required class="form-input">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Barangay (LGU)</label>
|
|
<select name="barangay_id" id="form-barangay" class="form-select">
|
|
<option value="">— Select Barangay —</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Code</label>
|
|
<input name="code" id="form-code" required class="form-input" placeholder="DOP-...">
|
|
</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="dop-lat" type="number" step="0.000001" required class="form-input bg-neutral-100 cursor-not-allowed" placeholder="14.6539" readonly>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Longitude</label>
|
|
<input name="lng" id="dop-lng" type="number" step="0.000001" required class="form-input bg-neutral-100 cursor-not-allowed" placeholder="121.0685" 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="dop-search" class="form-input flex-1 text-sm" placeholder="Search a place (e.g. Payatas, Quezon City...)">
|
|
<button type="button" id="dop-search-btn" class="btn-primary px-3 py-2 text-xs">Search</button>
|
|
</div>
|
|
<div class="relative">
|
|
<ul id="dop-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>
|
|
<label class="form-label">Capacity (kg)</label>
|
|
<input name="capacity_kg" type="number" min="0" class="form-input" placeholder="1000">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Contact Person</label>
|
|
<input name="contact_person" class="form-input" placeholder="Juan Dela Cruz">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Contact Phone</label>
|
|
<input name="contact_phone" class="form-input" placeholder="+639...">
|
|
</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" id="submit-btn" class="btn-primary">Create</button>
|
|
</div>
|
|
<div class="pt-2 border-t border-neutral-100 mt-4" id="delete-container">
|
|
<button type="button" id="delete-btn" class="btn-danger w-full hidden">Delete Drop-off Point</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
const rows = document.getElementById('rows');
|
|
const modal = document.getElementById('form-modal');
|
|
const createForm = document.getElementById('create-form');
|
|
const deleteBtn = document.getElementById('delete-btn');
|
|
const submitBtn = document.getElementById('submit-btn');
|
|
const modalTitle = document.getElementById('modal-title');
|
|
const formCode = document.getElementById('form-code');
|
|
|
|
let editingDopId = null;
|
|
|
|
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 editDop(id) {
|
|
editingDopId = id;
|
|
modalTitle.textContent = 'Edit drop-off point';
|
|
submitBtn.textContent = 'Save Changes';
|
|
deleteBtn.classList.remove('hidden');
|
|
|
|
formCode.readOnly = true;
|
|
formCode.classList.add('bg-neutral-100');
|
|
|
|
const res = await window.Verde.apiFetch(`/api/v1/admin/drop-off-points/${id}`);
|
|
if (!res.ok) {
|
|
window.Verde.toast('Failed to load drop-off point details', 'error');
|
|
return;
|
|
}
|
|
|
|
const d = res.body.data.drop_off_point || res.body.data;
|
|
createForm.querySelector('[name="name"]').value = d.name;
|
|
createForm.querySelector('[name="code"]').value = d.code;
|
|
createForm.querySelector('[name="barangay_id"]').value = d.barangay_id || '';
|
|
barangaySelect.dispatchEvent(new Event('change'));
|
|
createForm.querySelector('[name="address_line"]').value = d.address_line;
|
|
createForm.querySelector('[name="capacity_kg"]').value = d.capacity_kg ?? '';
|
|
createForm.querySelector('[name="contact_person"]').value = d.contact_person ?? '';
|
|
createForm.querySelector('[name="contact_phone"]').value = d.contact_phone ?? '';
|
|
createForm.querySelector('[name="status"]').value = d.status;
|
|
|
|
const lat = d.coordinates ? parseFloat(d.coordinates.lat) : 14.6539;
|
|
const lng = d.coordinates ? parseFloat(d.coordinates.lng) : 121.0685;
|
|
latInput.value = lat.toFixed(6);
|
|
lngInput.value = lng.toFixed(6);
|
|
|
|
if (d.geofence) {
|
|
geofenceInput.value = JSON.stringify(d.geofence);
|
|
} else {
|
|
geofenceInput.value = '';
|
|
}
|
|
|
|
modal.classList.remove('hidden');
|
|
await initMap(lat, lng, d.geofence);
|
|
}
|
|
|
|
async function load() {
|
|
rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>`;
|
|
const params = new URLSearchParams();
|
|
const status = document.getElementById('filter-status').value;
|
|
const q = document.getElementById('filter-q').value.trim();
|
|
if (status) params.set('status', status);
|
|
if (q) params.set('q', q);
|
|
params.set('per_page', '50');
|
|
|
|
const res = await window.Verde.apiFetch(`/api/v1/admin/drop-off-points?${params}`);
|
|
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 drop-off points yet.</td></tr>`;
|
|
return;
|
|
}
|
|
rows.innerHTML = items.map(d => `
|
|
<tr>
|
|
<td class="font-medium text-neutral-900">${window.Verde.escapeHtml(d.name)}</td>
|
|
<td class="text-xs text-neutral-500">${window.Verde.escapeHtml(d.tenant?.name || d.barangay?.city_municipality?.name || 'N/A')}</td>
|
|
<td class="font-mono text-xs text-neutral-600">${window.Verde.escapeHtml(d.code)}</td>
|
|
<td class="max-w-xs truncate text-neutral-600">${window.Verde.escapeHtml(d.address_line)}</td>
|
|
<td>${d.capacity_kg ?? '—'} kg</td>
|
|
<td>${badge(d.status)}</td>
|
|
<td class="text-right">
|
|
<button data-id="${d.id}" data-action="edit" class="btn-ghost px-3 py-1 text-xs">Edit</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
rows.querySelectorAll('button[data-action="edit"]').forEach(btn => {
|
|
btn.addEventListener('click', () => editDop(btn.dataset.id));
|
|
});
|
|
}
|
|
|
|
function awaitLeaflet() {
|
|
return new Promise(resolve => {
|
|
const tick = () => window.L ? resolve(window.L) : setTimeout(tick, 50);
|
|
tick();
|
|
});
|
|
}
|
|
|
|
let map = null;
|
|
let marker = null;
|
|
let geofenceLayer = null;
|
|
let currentLguBoundaryLayer = null;
|
|
let currentLguPolygon = null;
|
|
let allBarangays = [];
|
|
const latInput = document.getElementById('dop-lat');
|
|
const lngInput = document.getElementById('dop-lng');
|
|
const geofenceInput = document.getElementById('geofence_wkt');
|
|
const barangaySelect = document.getElementById('form-barangay');
|
|
|
|
async function loadBarangays() {
|
|
const res = await window.Verde.apiFetch('/api/v1/geo/barangays');
|
|
if (res.ok) {
|
|
allBarangays = res.body.data || [];
|
|
barangaySelect.innerHTML = '<option value="">— Select Barangay —</option>' +
|
|
allBarangays.map(b => {
|
|
const lgu = b.city_municipality ? b.city_municipality.name : '';
|
|
const label = lgu ? `${b.name} (${lgu})` : b.name;
|
|
return `<option value="${b.id}">${window.Verde.escapeHtml(label)}</option>`;
|
|
}).join('');
|
|
}
|
|
}
|
|
loadBarangays();
|
|
|
|
barangaySelect.addEventListener('change', () => {
|
|
const id = barangaySelect.value;
|
|
const b = allBarangays.find(x => x.id == id);
|
|
|
|
if (currentLguBoundaryLayer && map) {
|
|
map.removeLayer(currentLguBoundaryLayer);
|
|
currentLguBoundaryLayer = null;
|
|
}
|
|
currentLguPolygon = null;
|
|
|
|
if (b && b.boundary && b.boundary.length > 2 && map) {
|
|
const coords = b.boundary.map(p => [p.lat, p.lng]);
|
|
currentLguBoundaryLayer = L.polygon(coords, { color: '#3b82f6', fillOpacity: 0.1, weight: 2 }).addTo(map);
|
|
map.fitBounds(currentLguBoundaryLayer.getBounds());
|
|
|
|
// Close the loop for turf
|
|
const turfCoords = b.boundary.map(p => [p.lng, p.lat]);
|
|
turfCoords.push([b.boundary[0].lng, b.boundary[0].lat]);
|
|
currentLguPolygon = turf.polygon([turfCoords]);
|
|
}
|
|
});
|
|
|
|
function validatePoint(latlng) {
|
|
if (!currentLguPolygon) return true;
|
|
const pt = turf.point([latlng.lng, latlng.lat]);
|
|
return turf.booleanPointInPolygon(pt, currentLguPolygon);
|
|
}
|
|
|
|
function updateGeofenceInput(layer) {
|
|
if (!layer) {
|
|
geofenceInput.value = '';
|
|
return;
|
|
}
|
|
geofenceInput.value = JSON.stringify(layer.toGeoJSON().geometry);
|
|
}
|
|
|
|
function bindGeofenceEvents(layer) {
|
|
layer.on('pm:edit', (e) => updateGeofenceInput(e.layer));
|
|
layer.on('pm:remove', () => updateGeofenceInput(null));
|
|
}
|
|
|
|
async function initMap(initialLat = 14.6539, initialLng = 121.0685, geofenceObj = null) {
|
|
const L = window.L || await awaitLeaflet();
|
|
|
|
if (map) {
|
|
if (marker) {
|
|
marker.setLatLng([initialLat, initialLng]);
|
|
}
|
|
map.setView([initialLat, initialLng], 13);
|
|
|
|
if (geofenceLayer) {
|
|
map.removeLayer(geofenceLayer);
|
|
geofenceLayer = null;
|
|
}
|
|
if (geofenceObj) {
|
|
const group = L.geoJSON(geofenceObj);
|
|
if (group.getLayers().length > 0) {
|
|
geofenceLayer = group.getLayers()[0];
|
|
geofenceLayer.addTo(map);
|
|
bindGeofenceEvents(geofenceLayer);
|
|
}
|
|
}
|
|
|
|
setTimeout(() => map.invalidateSize(), 50);
|
|
return;
|
|
}
|
|
map = L.map('map-picker').setView([initialLat, initialLng], 13);
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap',
|
|
maxZoom: 19,
|
|
}).addTo(map);
|
|
|
|
marker = L.marker([initialLat, initialLng], { draggable: true }).addTo(map);
|
|
|
|
marker.on('dragstart', function () {
|
|
marker._lastPos = marker.getLatLng();
|
|
});
|
|
|
|
marker.on('dragend', function () {
|
|
const pos = marker.getLatLng();
|
|
if (!validatePoint(pos)) {
|
|
marker.setLatLng(marker._lastPos);
|
|
window.Verde.toast('Drop-off point must be within the selected LGU boundary.', 'error');
|
|
return;
|
|
}
|
|
latInput.value = pos.lat.toFixed(6);
|
|
lngInput.value = pos.lng.toFixed(6);
|
|
});
|
|
|
|
map.on('click', function (e) {
|
|
if (!validatePoint(e.latlng)) {
|
|
window.Verde.toast('Drop-off point must be within the selected LGU boundary.', 'error');
|
|
return;
|
|
}
|
|
marker.setLatLng(e.latlng);
|
|
latInput.value = e.latlng.lat.toFixed(6);
|
|
lngInput.value = e.latlng.lng.toFixed(6);
|
|
});
|
|
|
|
map.pm.addControls({
|
|
position: 'topleft',
|
|
drawCircle: false,
|
|
drawMarker: false,
|
|
drawCircleMarker: false,
|
|
drawPolyline: false,
|
|
drawRectangle: true,
|
|
drawText: false,
|
|
cutPolygon: false,
|
|
editMode: true,
|
|
dragMode: true,
|
|
removalMode: true
|
|
});
|
|
|
|
map.on('pm:create', (e) => {
|
|
if (geofenceLayer) {
|
|
map.removeLayer(geofenceLayer);
|
|
}
|
|
geofenceLayer = e.layer;
|
|
bindGeofenceEvents(geofenceLayer);
|
|
updateGeofenceInput(geofenceLayer);
|
|
});
|
|
|
|
map.on('pm:remove', (e) => {
|
|
if (e.layer === geofenceLayer) {
|
|
geofenceLayer = null;
|
|
updateGeofenceInput(null);
|
|
}
|
|
});
|
|
|
|
if (geofenceObj) {
|
|
const group = L.geoJSON(geofenceObj);
|
|
if (group.getLayers().length > 0) {
|
|
geofenceLayer = group.getLayers()[0];
|
|
geofenceLayer.addTo(map);
|
|
bindGeofenceEvents(geofenceLayer);
|
|
}
|
|
}
|
|
|
|
// Setup geocoding search
|
|
const searchInput = document.getElementById('dop-search');
|
|
const searchBtn = document.getElementById('dop-search-btn');
|
|
const resultsList = document.getElementById('dop-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);
|
|
|
|
if (!validatePoint({lat, lng})) {
|
|
window.Verde.toast('Searched location is outside the selected LGU boundary.', 'error');
|
|
return;
|
|
}
|
|
|
|
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', () => {
|
|
editingDopId = null;
|
|
createForm.reset();
|
|
modalTitle.textContent = 'New drop-off point';
|
|
submitBtn.textContent = 'Create';
|
|
deleteBtn.classList.add('hidden');
|
|
|
|
formCode.readOnly = false;
|
|
formCode.classList.remove('bg-neutral-100');
|
|
|
|
latInput.value = '14.653900';
|
|
lngInput.value = '121.068500';
|
|
geofenceInput.value = '';
|
|
createForm.querySelector('[name="barangay_id"]').value = '';
|
|
barangaySelect.dispatchEvent(new Event('change'));
|
|
modal.classList.remove('hidden');
|
|
initMap(14.6539, 121.0685, null);
|
|
});
|
|
|
|
modal.querySelectorAll('[data-close]').forEach(el => el.addEventListener('click', () => modal.classList.add('hidden')));
|
|
document.getElementById('filter-apply').addEventListener('click', load);
|
|
document.getElementById('filter-status').addEventListener('change', load);
|
|
|
|
createForm.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.barangay_id) payload.barangay_id = parseInt(payload.barangay_id, 10);
|
|
else delete payload.barangay_id;
|
|
if (payload.capacity_kg) payload.capacity_kg = parseInt(payload.capacity_kg, 10);
|
|
else delete payload.capacity_kg;
|
|
if (!payload.contact_person) payload.contact_person = null;
|
|
if (!payload.contact_phone) payload.contact_phone = null;
|
|
|
|
const url = editingDopId ? `/api/v1/admin/drop-off-points/${editingDopId}` : '/api/v1/admin/drop-off-points';
|
|
const method = editingDopId ? 'PATCH' : 'POST';
|
|
|
|
const res = await window.Verde.apiFetch(url, {
|
|
method: method,
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (res.ok) {
|
|
window.Verde.toast(editingDopId ? 'Drop-off point updated' : 'Drop-off point created', 'success');
|
|
modal.classList.add('hidden');
|
|
createForm.reset();
|
|
load();
|
|
} else {
|
|
window.Verde.toast(res.body?.message ?? 'Save failed', 'error');
|
|
}
|
|
});
|
|
|
|
deleteBtn.addEventListener('click', async () => {
|
|
if (!editingDopId) return;
|
|
if (!confirm('Are you sure you want to delete this drop-off point? This action cannot be undone.')) return;
|
|
|
|
const res = await window.Verde.apiFetch(`/api/v1/admin/drop-off-points/${editingDopId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (res.ok) {
|
|
window.Verde.toast('Drop-off point deleted successfully', 'success');
|
|
modal.classList.add('hidden');
|
|
createForm.reset();
|
|
load();
|
|
} else {
|
|
window.Verde.toast(res.body?.message ?? 'Delete failed', 'error');
|
|
}
|
|
});
|
|
|
|
load();
|
|
</script>
|
|
@endsection
|