feat: update barangay management and dashboard roles

- Created dashboard-router to separate LGU and Super Admin landing pages
- Added Barangays and LGUs statistical cards to Super Admin dashboard
- Moved LGU Management to Areas group in sidebar and added API Docs link
- Removed geofence spatial validation (ST_Contains) for Barangay creation in both LGU and Super Admin controllers to allow flexible boundaries
This commit is contained in:
Developer
2026-07-07 10:32:38 +08:00
parent ee72cd7211
commit aaa5058249
7 changed files with 43 additions and 67 deletions

View File

@@ -55,20 +55,7 @@ class AdminBarangayController extends ApiController
$boundary = $this->buildPolygon($data['boundary']);
$centroid = $this->calculateCentroid($boundary);
if ($tenant->boundary_polygon) {
$isContained = DB::selectOne(
'SELECT ST_Contains(boundary_polygon, ST_GeomFromText(?, 4326, \'axis-order=long-lat\')) as contained FROM tenants WHERE id = ?',
[$boundary->toWkt(), $tenant->id]
);
if (! $isContained || ! $isContained->contained) {
return $this->fail(
'The drawn Barangay boundary must lie completely within your LGU boundary.',
['boundary' => ['The Barangay boundary is outside your LGU border.']],
422
);
}
}
// Spatial validation check removed as per user request
// Code auto-generation if empty
$city = CityMunicipality::findOrFail($cityId);
@@ -130,20 +117,7 @@ class AdminBarangayController extends ApiController
$boundary = $this->buildPolygon($data['boundary']);
$centroid = $this->calculateCentroid($boundary);
if ($tenant->boundary_polygon) {
$isContained = DB::selectOne(
'SELECT ST_Contains(boundary_polygon, ST_GeomFromText(?, 4326, \'axis-order=long-lat\')) as contained FROM tenants WHERE id = ?',
[$boundary->toWkt(), $tenant->id]
);
if (! $isContained || ! $isContained->contained) {
return $this->fail(
'The drawn Barangay boundary must lie completely within your LGU boundary.',
['boundary' => ['The Barangay boundary is outside your LGU border.']],
422
);
}
}
// Spatial validation check removed as per user request
// Code auto-generation if empty
$city = CityMunicipality::findOrFail($cityId);

View File

@@ -52,24 +52,7 @@ class SuperAdminBarangayController extends ApiController
$boundary = $this->buildPolygon($data['boundary']);
$centroid = $this->calculateCentroid($boundary);
// Spatial validation check
$cityId = $data['city_municipality_id'];
$tenant = Tenant::where('city_municipality_id', $cityId)->first();
if ($tenant && $tenant->boundary_polygon) {
$isContained = DB::selectOne(
'SELECT ST_Contains(boundary_polygon, ST_GeomFromText(?, 4326, \'axis-order=long-lat\')) as contained FROM tenants WHERE id = ?',
[$boundary->toWkt(), $tenant->id]
);
if (! $isContained || ! $isContained->contained) {
return $this->fail(
'The drawn Barangay boundary must lie completely within the LGU/Municipality boundary.',
['boundary' => ['The Barangay boundary is outside the LGU border.']],
422
);
}
}
// Spatial validation check removed as per user request
// Code auto-generation if empty
$city = CityMunicipality::findOrFail($cityId);
@@ -120,24 +103,7 @@ class SuperAdminBarangayController extends ApiController
$boundary = $this->buildPolygon($data['boundary']);
$centroid = $this->calculateCentroid($boundary);
// Spatial validation check
$cityId = $data['city_municipality_id'];
$tenant = Tenant::where('city_municipality_id', $cityId)->first();
if ($tenant && $tenant->boundary_polygon) {
$isContained = DB::selectOne(
'SELECT ST_Contains(boundary_polygon, ST_GeomFromText(?, 4326, \'axis-order=long-lat\')) as contained FROM tenants WHERE id = ?',
[$boundary->toWkt(), $tenant->id]
);
if (! $isContained || ! $isContained->contained) {
return $this->fail(
'The drawn Barangay boundary must lie completely within the LGU/Municipality boundary.',
['boundary' => ['The Barangay boundary is outside the LGU border.']],
422
);
}
}
// Spatial validation check removed as per user request
// Code auto-generation if empty
$city = CityMunicipality::findOrFail($cityId);

Binary file not shown.

View File

@@ -0,0 +1,19 @@
@extends('admin.layouts.app', ['pageTitle' => 'Loading Dashboard...'])
@section('page')
<div class="flex flex-col items-center justify-center h-64 space-y-4">
<svg class="h-8 w-8 animate-spin text-verde-600" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z"/>
</svg>
<div class="text-sm text-neutral-500 font-medium tracking-tight">Routing to your dashboard...</div>
</div>
<script type="module">
const user = window.Verde?.getUser();
if (user?.role === 'super_admin') {
window.location.replace('/admin/superadmin-dashboard');
} else {
window.location.replace('/admin/lgu-dashboard');
}
</script>
@endsection

View File

@@ -15,6 +15,8 @@
['key' => 'pending_households', 'label' => 'Pending households', 'href' => '/admin/households'],
['key' => 'verified_households', 'label' => 'Verified households', 'href' => '/admin/households'],
['key' => 'service_areas', 'label' => 'Service areas', 'href' => '/admin/service-areas'],
['key' => 'barangays', 'label' => 'Barangays', 'href' => '/admin/barangays'],
['key' => 'lgus', 'label' => 'LGUs', 'href' => '/admin/lgus'],
['key' => 'drop_off_points', 'label' => 'Drop-off points', 'href' => '/admin/drop-off-points'],
['key' => 'dumpsites', 'label' => 'Dumpsites', 'href' => '/admin/dumpsites'],
['key' => 'qr_batches', 'label' => 'QR batches', 'href' => '/admin/qr-batches'],
@@ -81,6 +83,12 @@
const sa = await window.Verde.apiFetch('/api/v1/service-areas');
if (sa.ok) setStat('service_areas', (sa.body.data ?? []).length);
const ba = await window.Verde.apiFetch('/api/v1/super-admin/barangays?per_page=1');
if (ba.ok) setStat('barangays', ba.body.meta?.total ?? 0);
const lgus = await window.Verde.apiFetch('/api/v1/super-admin/tenants?per_page=1');
if (lgus.ok) setStat('lgus', lgus.body.meta?.total ?? 0);
const dop = await window.Verde.apiFetch('/api/v1/admin/drop-off-points?per_page=1');
if (dop.ok) setStat('drop_off_points', dop.body.meta?.total ?? 0);

View File

@@ -27,6 +27,7 @@
'Areas' => [
['href' => '/admin/service-areas', 'label' => 'Service Areas', 'icon' => 'globe'],
['href' => '/admin/barangays', 'label' => 'Barangays', 'icon' => 'pin'],
['href' => '/admin/lgus', 'label' => 'LGU Management', 'icon' => 'globe', 'super_admin_only' => true],
],
'Finance' => [
['href' => '/admin/finance', 'label' => 'Payments', 'icon' => 'cash'],
@@ -38,8 +39,8 @@
['href' => '/admin/settings', 'label' => 'System Config', 'icon' => 'cog'],
],
'System Admin' => [
['href' => '/admin/lgus', 'label' => 'LGU Management', 'icon' => 'globe'],
['href' => '/admin/system-users', 'label' => 'User Accounts', 'icon' => 'users'],
['href' => '/docs/api', 'label' => 'API Docs', 'icon' => 'route', 'external' => true],
],
];
@@ -89,11 +90,16 @@
$disabled = ! empty($item['disabled']);
$href = $disabled ? '#' : $item['href'];
$active = ! $disabled && $isActive(ltrim($item['href'], '/'));
$superAdminOnly = ! empty($item['super_admin_only']);
$classes = 'nav-link ' . ($active ? 'is-active ' : '') . ($disabled ? 'is-disabled ' : '');
if ($superAdminOnly) {
$classes .= ' hidden super-admin-item';
}
@endphp
<a href="{{ $href }}"
@if($disabled) onclick="event.preventDefault(); window.Verde?.toast('{{ $item['label'] }} ships in a later module.', 'info');" @endif
@if(! empty($item['external'])) target="_blank" rel="noopener" @endif
class="nav-link {{ $active ? 'is-active' : '' }} {{ $disabled ? 'is-disabled' : '' }}">
class="{{ trim($classes) }}">
{!! $renderIcon($item['icon']) !!}
<span class="flex-1">{{ $item['label'] }}</span>
@if ($disabled)
@@ -111,6 +117,7 @@
// Show System Admin group only for super_admin
if (user.role === 'super_admin') {
document.getElementById('nav-group-superadmin')?.classList.remove('hidden');
document.querySelectorAll('.super-admin-item').forEach(el => el.classList.remove('hidden'));
}
// Update role badge

View File

@@ -14,7 +14,9 @@ Route::get('/qr/{serial}', function ($serial) {
})->name('qr.verify');
Route::prefix('admin')->group(function () {
Route::view('/dashboard', 'admin.lgu-dashboard')->name('admin.dashboard');
Route::view('/dashboard', 'admin.dashboard-router')->name('admin.dashboard');
Route::view('/lgu-dashboard', 'admin.lgu-dashboard')->name('admin.lgu-dashboard');
Route::view('/superadmin-dashboard', 'admin.dashboard')->name('admin.superadmin-dashboard');
Route::view('/households', 'admin.households')->name('admin.households');
Route::view('/service-areas', 'admin.service-areas')->name('admin.service-areas');
Route::view('/qr-batches', 'admin.qr-batches')->name('admin.qr-batches');