validate([ 'city_municipality_id' => ['nullable', 'integer'], 'q' => ['nullable', 'string', 'max:100'], 'per_page' => ['nullable', 'integer', 'min:1', 'max:100'], ]); $perPage = (int) $request->input('per_page', 25); $barangays = Barangay::query() ->with(['cityMunicipality.province']) ->when($request->filled('city_municipality_id'), fn ($q) => $q->where('city_municipality_id', $request->integer('city_municipality_id'))) ->when($request->filled('q'), function ($q) use ($request) { $term = '%'.$request->string('q').'%'; $q->where(function ($qq) use ($term) { $qq->where('name', 'like', $term) ->orWhere('code', 'like', $term) ->orWhere('psgc_code', 'like', $term); }); }) ->orderBy('name') ->paginate($perPage); return $this->ok(BarangayResource::collection($barangays)); } public function store(StoreBarangayRequest $request): JsonResponse { $data = $request->validated(); $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 ); } } // Code auto-generation if empty $city = CityMunicipality::findOrFail($cityId); if (empty($data['psgc_code'])) { $prefix = substr($city->psgc_code, 0, 9); $maxLocal = Barangay::where('psgc_code', 'like', $prefix.'%') ->whereRaw('LENGTH(psgc_code) = 12') ->orderBy('psgc_code', 'desc') ->first(); $suffix = $maxLocal ? ((int) substr($maxLocal->psgc_code, -3) + 1) : 1; $data['psgc_code'] = $prefix.str_pad($suffix, 3, '0', STR_PAD_LEFT); } if (empty($data['code'])) { $prefix = substr($city->code, 0, 12); $maxLocal = Barangay::where('code', 'like', $prefix.'%') ->orderBy('code', 'desc') ->first(); $suffix = $maxLocal ? ((int) substr(strrchr($maxLocal->code, '-'), 1) + 1) : 1; $data['code'] = $prefix.'-'.str_pad($suffix, 3, '0', STR_PAD_LEFT); } $barangay = Barangay::create([ 'name' => $data['name'], 'city_municipality_id' => $cityId, 'psgc_code' => $data['psgc_code'], 'code' => $data['code'], 'urban_rural' => $data['urban_rural'], 'population' => $data['population'], 'boundary' => $boundary, 'centroid' => $centroid, ]); return $this->created(new BarangayResource($barangay), 'Barangay created successfully'); } public function show(Barangay $barangay): JsonResponse { $barangay->load(['cityMunicipality.province']); return $this->ok(new BarangayResource($barangay)); } public function update(UpdateBarangayRequest $request, Barangay $barangay): JsonResponse { $data = $request->validated(); $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 ); } } // Code auto-generation if empty $city = CityMunicipality::findOrFail($cityId); if (empty($data['psgc_code'])) { $prefix = substr($city->psgc_code, 0, 9); $maxLocal = Barangay::where('psgc_code', 'like', $prefix.'%') ->whereRaw('LENGTH(psgc_code) = 12') ->orderBy('psgc_code', 'desc') ->first(); $suffix = $maxLocal ? ((int) substr($maxLocal->psgc_code, -3) + 1) : 1; $data['psgc_code'] = $prefix.str_pad($suffix, 3, '0', STR_PAD_LEFT); } if (empty($data['code'])) { $prefix = substr($city->code, 0, 12); $maxLocal = Barangay::where('code', 'like', $prefix.'%') ->orderBy('code', 'desc') ->first(); $suffix = $maxLocal ? ((int) substr(strrchr($maxLocal->code, '-'), 1) + 1) : 1; $data['code'] = $prefix.'-'.str_pad($suffix, 3, '0', STR_PAD_LEFT); } $barangay->update([ 'name' => $data['name'], 'city_municipality_id' => $cityId, 'psgc_code' => $data['psgc_code'], 'code' => $data['code'], 'urban_rural' => $data['urban_rural'], 'population' => $data['population'], 'boundary' => $boundary, 'centroid' => $centroid, ]); return $this->ok(new BarangayResource($barangay), 'Barangay updated successfully'); } public function destroy(Barangay $barangay): JsonResponse { $barangay->delete(); return $this->ok(null, 'Barangay deleted successfully'); } private function buildPolygon(array $points): Polygon { $ring = array_map( fn ($p) => new Point((float) $p['lat'], (float) $p['lng'], 4326), $points, ); $first = $ring[0]; $last = $ring[count($ring) - 1]; if ($first->latitude !== $last->latitude || $first->longitude !== $last->longitude) { $ring[] = new Point($first->latitude, $first->longitude, 4326); } return new Polygon([new LineString($ring)], 4326); } private function calculateCentroid(Polygon $polygon): Point { $res = DB::selectOne( 'SELECT ST_AsText(ST_SRID(ST_Centroid(ST_GeomFromText(?, 0)), 4326)) as wkt', [$polygon->toWkt()] ); if ($res && preg_match('/POINT\(([^ ]+) ([^ ]+)\)/', $res->wkt, $matches)) { $lat = (float) $matches[1]; $lng = (float) $matches[2]; return new Point($lat, $lng, 4326); } $rings = $polygon->getGeometries(); $ring = $rings->first(); if ($ring) { $coords = $ring->getGeometries(); $lats = $coords->map(fn ($p) => $p->latitude)->all(); $lngs = $coords->map(fn ($p) => $p->longitude)->all(); return new Point(array_sum($lats) / count($lats), array_sum($lngs) / count($lngs), 4326); } return new Point(0, 0, 4326); } }