Initial commit

This commit is contained in:
Your Name
2026-06-02 12:40:37 +07:00
commit 20607b0e75
117 changed files with 25598 additions and 0 deletions
+344
View File
@@ -0,0 +1,344 @@
# WebGIS - Sistem Informasi Geografis untuk Pengentasan Kemiskinan
Aplikasi web berbasis map untuk visualisasi, manajemen, dan analisis data kemiskinan di wilayah Pontianak.
## 🚀 Quick Start
### Prerequisites
- PHP 7.4+
- Web Browser (Chrome, Firefox, Edge)
- Git
### Installation
```bash
# 1. Clone or download repository
cd /workspaces/project92e4821824385
# 2. Start PHP development server
php -S localhost:8000
# 3. Open in browser
# Navigate to: http://localhost:8000
# 4. Login with demo credentials
# Email: admin@system.local
# Password: admin123
```
## 📋 Features
### ✅ Authentication & Access Control
- Role-based login system (Admin, Petugas, Pimpinan)
- Session management dengan automatic timeout
- Secure password hashing dengan bcrypt
### ✅ Dashboard Analytics
- Real-time KPI cards (total households, avg poverty score, assistance rate)
- Status breakdown (verification status)
- Priority ranking (intervention priority levels)
- Area-wise statistics breakdown
### ✅ User Management (Admin Only)
- Create new users dengan role assignment
- Edit user roles
- Deactivate users (soft delete)
- User list dengan status indicators
### ✅ Spatial Data Management
- Multi-layer interactive map dengan Leaflet.js
- Map layers: Points (households), Roads, Parcels, Choropleth (Pontianak areas)
- Drag-drop marker repositioning
- Point search dan filtering
### ✅ Household Data
- Create household records dengan poverty assessment
- Income, employment status, housing condition tracking
- Household ranking by poverty score
- Verification status tracking
### ✅ Poverty Scoring System
- Automatic computation of SKR (socioeconomic score)
- SAG (geographic accessibility score)
- SPI (intervention priority score)
- Priority level assignment (Very High → Low)
### ✅ Verification & Assistance Tracking
- Record field verification results
- Track assistance distribution (food, cash, health, education, etc.)
- Delivery status monitoring
- Evidence & confidence scoring
### ✅ Export & Reporting
- GeoJSON export untuk spatial analysis
- Suitable untuk integration dengan GIS tools
## 🎨 Technology Stack
| Component | Technology |
|-----------|------------|
| **Frontend** | Vanilla HTML5, CSS3, JavaScript |
| **Map Library** | Leaflet.js (OpenStreetMap compatible) |
| **Backend** | PHP 7.4+ |
| **Database** | SQLite 3.0+ |
| **API** | RESTful JSON APIs |
| **Session** | PHP Sessions (cookie-based) |
## 📁 Project Structure
```
project92e4821824385/
├── api/ # 40+ PHP endpoints
│ ├── config.php # Database config & auth functions
│ ├── login.php # Login endpoint
│ ├── logout.php # Logout endpoint
│ ├── get_users.php # User list
│ ├── create_user.php # Create user
│ ├── update_user.php # Update user role
│ ├── delete_user.php # Deactivate user
│ ├── get_dashboard_summary.php
│ ├── get_area_breakdown.php
│ └── ... (30+ more endpoints for spatial/household data)
├── js/
│ └── app.js # Main application logic (~2300 lines)
├── css/
│ └── style.css # Responsive styling
├── db/
│ └── markers.db # SQLite database (auto-created)
├── index.html # Main entry point
├── API_REFERENCE.md # Detailed API documentation
├── USER_GUIDE.md # User manual for all roles
├── IMPLEMENTATION_SUMMARY.md # Tech stack & features
└── README.md # This file
```
## 👥 User Roles & Permissions
### Admin 👑
- Full system access
- User management (create, edit, delete)
- Data input and editing
- View analytics and dashboard
- Compute poverty scores
### Petugas (Data Officer) 📝
- Input/edit household data
- View map and statistics
- Verify households
- Record assistance distribution
- Cannot manage users
### Pimpinan (Manager) 👔
- View-only dashboard access
- View priority rankings
- Cannot input or modify data
## 🔐 Default Credentials
```
Email: admin@system.local
Password: admin123
Role: Admin
```
## 📖 Documentation
- **API Reference:** See `API_REFERENCE.md` for all endpoint Documentation
- **User Guide:** See `USER_GUIDE.md` for complete usage instructions
- **Implementation:** See `IMPLEMENTATION_SUMMARY.md` for technical details
## 🗄️ Database
### Auto-initialization
- Database schema automatically created on first run
- 11+ tables with proper relationships
- Indexes on performance-critical columns
- Cascade delete policies
### Tables
- `users` - User accounts & authentication
- `households` - Family poverty data
- `household_scores` - SKR, SAG, SPI calculations
- `assistance_distributions` - Bantuan tracking
- `verification_logs` - Verification history
- `markers, roads, land_parcels` - Spatial data
- `pontianak_areas, area_statistics` - Administrative boundaries
- `masjids, need_points` - Location data
- `score_metadata` - Scoring configuration
## 🔌 API Endpoints (40+)
### Authentication
- POST `/api/login.php` - Login
- POST `/api/logout.php` - Logout
- GET `/api/get_current_user.php` - Current user info
### User Management (Admin)
- GET `/api/get_users.php` - List users
- POST `/api/create_user.php` - Create user
- PUT `/api/update_user.php` - Update role
- DELETE `/api/delete_user.php` - Deactivate user
### Analytics
- GET `/api/get_dashboard_summary.php` - Dashboard KPIs
- GET `/api/get_area_breakdown.php` - Area statistics
### Household Management
- POST `/api/save_household.php` - Create household
- GET `/api/get_households.php` - List households
- PUT `/api/update_household.php` - Update household
- DELETE `/api/delete_household.php` - Delete household
- POST `/api/compute_household_scores.php` - Compute scores
- GET `/api/get_household_ranking.php` - Priority ranking
### Verification & Assistance
- POST `/api/save_verification_log.php` - Record verification
- GET `/api/get_verification_logs.php` - Verification history
- POST `/api/save_assistance_distribution.php` - Record assistance
- GET `/api/get_assistance_distributions.php` - Assistance history
### Spatial Data (20+)
- Points, Roads, Parcels, Pontianak Areas (CRUD operations)
- Choropleth data aggregation
- GeoJSON export
## ⚙️ Configuration
### Server Requirements
- PHP 7.4 or higher
- Web server (Apache, Nginx, phpd built-in)
- Write permissions for `/db/` folder
### Optional Production Setup
- Set up HTTPS/SSL certificate
- Configure proper session timeout
- Set up automated backups
- Enable database WAL mode for concurrency
## 🚀 Production Deployment
### Using Apache
```apache
<VirtualHost *:80>
ServerName system.local
DocumentRoot /var/www/project
<Directory /var/www/project>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
```
### Using Nginx
```nginx
server {
listen 80;
server_name system.local;
root /var/www/project;
location ~\.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
```
## 📊 Performance
- **Page Load:** < 3 seconds (on 10 Mbps connection)
- **API Response:** < 500ms for most endpoints
- **Map Rendering:** Real-time interactivity
- **Scoring Computation:** < 1 second for 100 households
## 🛠️ Development Guide
### Adding New Endpoint
1. Create new file in `/api/` folder: `your_endpoint.php`
2. Include `config.php` for database access
3. Use `requireLogin()` or `requireRole()` for access control
4. Return JSON response
Example:
```php
<?php
header('Content-Type: application/json');
require_once 'config.php';
requireRole(['Admin', 'Petugas']);
// Your code here
die(json_encode(['success' => true, 'data' => $result]));
?>
```
### Modifying Frontend
1. Edit `js/app.js` for logic changes
2. Edit `css/style.css` for styling
3. Edit `index.html` for structure/layout
4. Refresh browser to see changes (no build required)
## 🐛 Troubleshooting
### Login Issues
- Verify email: `admin@system.local`
- Verify password: `admin123`
- Check browser cookies are enabled
- Clear cache and try again
### Map Not Loading
- Check browser console for errors (F12)
- Verify internet connection (for Leaflet CDN)
- Try refreshing page
### Database Issues
- Ensure `/db/` folder has write permissions
- Check PHP error logs
- Delete `db/markers.db` to reset (will lose data)
### Performance Issues
- Check browser performance (F12 > Performance tab)
- Monitor server CPU/memory usage
- Consider adding pagination for large datasets
## 📝 Changelog
### Version 1.0 (May 2026)
- ✅ Complete authentication system
- ✅ Dashboard with real-time analytics
- ✅ User management for admins
- ✅ Household data management with scoring
- ✅ Interactive Leaflet map
- ✅ Verification and assistance tracking
- ✅ GeoJSON export capability
- ✅ Comprehensive API (40+ endpoints)
- ✅ User guide and API documentation
## 📞 Support
For issues, questions, or feature requests:
1. Check documentation in `USER_GUIDE.md`
2. Review API documentation in `API_REFERENCE.md`
3. Check browser console for error messages
4. Contact system administrator
## 📜 License
Academic Project - Universitas Tanjungpura
Mata Kuliah: Sistem Informasi Geografis
Dosen: Dr. Ir. Yus Sholva, ST., MT.
## 👨‍💻 Authors
**Frederick Ferdyand Halim** - Developer
Semester Genap 2024/2025
---
**Last Updated:** 9 Mei 2026
**Status:** Production Ready v1.0
+88
View File
@@ -0,0 +1,88 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!is_array($data)) {
$data = [];
}
$db = getDB();
$households = [];
if (isset($data['household_id']) && $data['household_id'] !== null && $data['household_id'] !== '') {
if (!is_numeric($data['household_id'])) {
http_response_code(400);
echo json_encode(['error' => 'household_id must be numeric']);
exit;
}
$stmt = $db->prepare('SELECT * FROM households WHERE id = ?');
$stmt->execute([(int)$data['household_id']]);
$household = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$household) {
http_response_code(404);
echo json_encode(['error' => 'Household not found']);
exit;
}
$households[] = $household;
} else {
$stmt = $db->query('SELECT * FROM households ORDER BY id ASC');
$households = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
if (empty($households)) {
echo json_encode([
'success' => true,
'message' => 'No households to score',
'computed_count' => 0,
'scores' => []
]);
exit;
}
$scored = [];
foreach ($households as $household) {
$scoreData = computeHouseholdScores($db, $household);
upsertHouseholdScore($db, $household['id'], $scoreData);
$scored[] = [
'household_id' => (int)$household['id'],
'household_code' => $household['household_code'],
'head_name' => $household['head_name'],
'skr' => $scoreData['skr'],
'sag' => $scoreData['sag'],
'spi' => $scoreData['spi'],
'priority_level' => $scoreData['priority_level']
];
}
usort($scored, function ($a, $b) {
if ($a['spi'] === $b['spi']) {
return 0;
}
return ($a['spi'] < $b['spi']) ? 1 : -1;
});
echo json_encode([
'success' => true,
'computed_count' => count($scored),
'scores' => $scored,
'score_version' => 'v1'
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+1294
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Check authentication and authorization
requireRole(['Admin']);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
}
$data = json_decode(file_get_contents('php://input'), true);
// Validate input
if (!isset($data['username']) || !isset($data['email']) || !isset($data['password']) || !isset($data['role'])) {
http_response_code(400);
die(json_encode(['error' => 'Missing required fields: username, email, password, role']));
}
$result = createUser($data['username'], $data['email'], $data['password'], $data['role']);
if ($result['success']) {
http_response_code(201);
} else {
http_response_code(400);
}
die(json_encode($result));
+50
View File
@@ -0,0 +1,50 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!is_array($data) || !isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing household id']);
exit;
}
if (!is_numeric($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'id must be numeric']);
exit;
}
$id = (int)$data['id'];
$db = getDB();
$checkStmt = $db->prepare('SELECT id FROM households WHERE id = ?');
$checkStmt->execute([$id]);
if (!$checkStmt->fetch(PDO::FETCH_ASSOC)) {
http_response_code(404);
echo json_encode(['error' => 'Household not found']);
exit;
}
$deleteStmt = $db->prepare('DELETE FROM households WHERE id = ?');
$deleteStmt->execute([$id]);
echo json_encode([
'success' => true,
'deleted_id' => $id
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing road id']);
exit;
}
$db = getDB();
$stmt = $db->prepare('DELETE FROM roads WHERE id = ?');
$stmt->execute([$data['id']]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+51
View File
@@ -0,0 +1,51 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing masjid id']);
exit;
}
$id = (int)$data['id'];
$db = getDB();
$checkStmt = $db->prepare('SELECT id FROM masjids WHERE id = ?');
$checkStmt->execute([$id]);
if (!$checkStmt->fetch(PDO::FETCH_ASSOC)) {
http_response_code(404);
echo json_encode(['error' => 'Masjid not found']);
exit;
}
$childStmt = $db->prepare('SELECT id FROM need_points WHERE masjid_id = ?');
$childStmt->execute([$id]);
$childIds = array_map(function($row) {
return (int)$row['id'];
}, $childStmt->fetchAll(PDO::FETCH_ASSOC));
$deleteStmt = $db->prepare('DELETE FROM masjids WHERE id = ?');
$deleteStmt->execute([$id]);
echo json_encode([
'success' => true,
'deleted_need_points' => count($childIds),
'deleted_need_point_ids' => $childIds
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+39
View File
@@ -0,0 +1,39 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing point id']);
exit;
}
$id = (int)$data['id'];
$db = getDB();
$deleteStmt = $db->prepare('DELETE FROM need_points WHERE id = ?');
$deleteStmt->execute([$id]);
if ($deleteStmt->rowCount() === 0) {
http_response_code(404);
echo json_encode(['error' => 'Point berkebutuhan not found']);
exit;
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing marker id']);
exit;
}
$db = getDB();
$stmt = $db->prepare('DELETE FROM markers WHERE id = ?');
$stmt->execute([$data['id']]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing parcel id']);
exit;
}
$db = getDB();
$stmt = $db->prepare('DELETE FROM land_parcels WHERE id = ?');
$stmt->execute([$data['id']]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+58
View File
@@ -0,0 +1,58 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Check authentication and authorization
requireRole(['Admin']);
if ($_SERVER['REQUEST_METHOD'] !== 'DELETE') {
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
}
$data = json_decode(file_get_contents('php://input'), true);
// Validate input
if (!isset($data['id'])) {
http_response_code(400);
die(json_encode(['error' => 'Missing required field: id']));
}
try {
$db = getDB();
// Prevent deactivating the only admin
$adminCount = $db->query('SELECT COUNT(*) as cnt FROM users WHERE role = \'Admin\' AND is_active = 1')->fetch(PDO::FETCH_ASSOC)['cnt'];
$stmt = $db->prepare('SELECT role FROM users WHERE id = ?');
$stmt->execute([$data['id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
http_response_code(404);
die(json_encode(['error' => 'User not found']));
}
if ($user['role'] === 'Admin' && $adminCount <= 1) {
http_response_code(400);
die(json_encode(['error' => 'Cannot deactivate the only admin user']));
}
// Deactivate user (soft delete)
$updateStmt = $db->prepare('
UPDATE users
SET is_active = 0, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
');
$updateStmt->execute([$data['id']]);
http_response_code(200);
die(json_encode([
'success' => true,
'message' => 'User deactivated successfully'
]));
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['error' => 'Database error: ' . $e->getMessage()]));
}
+133
View File
@@ -0,0 +1,133 @@
<?php
header('Content-Type: application/geo+json');
header('Content-Disposition: attachment; filename="export.geojson"');
require_once 'config.php';
try {
$db = getDB();
// Get type parameter
$type = isset($_GET['type']) ? $_GET['type'] : 'roads';
// Validate type
$valid_types = ['roads', 'parcels', 'pontianak_areas'];
if (!in_array($type, $valid_types)) {
throw new Exception('Invalid type. Must be: ' . implode(', ', $valid_types));
}
$features = [];
if ($type === 'roads') {
// Export roads as LineString features
$stmt = $db->prepare('SELECT id, name, road_type, length_meters, condition_status, coordinates, created_at FROM roads');
$stmt->execute();
$roads = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($roads as $road) {
$coordinates = json_decode($road['coordinates'], true);
// Convert [lat, lng] to [lng, lat] for GeoJSON
$geojson_coords = [];
foreach ($coordinates as $coord) {
$geojson_coords[] = [$coord[1], $coord[0]];
}
$features[] = [
'type' => 'Feature',
'geometry' => [
'type' => 'LineString',
'coordinates' => $geojson_coords
],
'properties' => [
'id' => $road['id'],
'name' => $road['name'],
'road_type' => $road['road_type'],
'length_meters' => floatval($road['length_meters']),
'length_km' => floatval($road['length_meters']) / 1000,
'condition_status' => $road['condition_status'],
'created_at' => $road['created_at']
]
];
}
} else if ($type === 'parcels') {
// Export parcels as Polygon features
$stmt = $db->prepare('SELECT id, name, certificate_type, area_sqm, coordinates, created_at FROM land_parcels');
$stmt->execute();
$parcels = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($parcels as $parcel) {
$coordinates = json_decode($parcel['coordinates'], true);
// Convert [lat, lng] to [lng, lat] for GeoJSON
$geojson_coords = [];
foreach ($coordinates as $coord) {
$geojson_coords[] = [$coord[1], $coord[0]];
}
// Close the polygon
if ($geojson_coords[0] !== end($geojson_coords)) {
$geojson_coords[] = $geojson_coords[0];
}
$features[] = [
'type' => 'Feature',
'geometry' => [
'type' => 'Polygon',
'coordinates' => [$geojson_coords]
],
'properties' => [
'id' => $parcel['id'],
'name' => $parcel['name'],
'certificate_type' => $parcel['certificate_type'],
'area_sqm' => floatval($parcel['area_sqm']),
'area_hectares' => floatval($parcel['area_sqm']) / 10000,
'created_at' => $parcel['created_at']
]
];
}
} else if ($type === 'pontianak_areas') {
// Export Pontianak areas as features
$stmt = $db->prepare('SELECT id, name, area_type, geometry, population FROM pontianak_areas ORDER BY area_type, name');
$stmt->execute();
$areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($areas as $area) {
$geometry = json_decode($area['geometry'], true);
$features[] = [
'type' => 'Feature',
'geometry' => $geometry,
'properties' => [
'id' => $area['id'],
'name' => $area['name'],
'area_type' => $area['area_type'],
'population' => intval($area['population'])
]
];
}
}
// Build FeatureCollection
$geojson = [
'type' => 'FeatureCollection',
'features' => $features
];
// Output GeoJSON
echo json_encode($geojson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>
+51
View File
@@ -0,0 +1,51 @@
<?php
// Backfill missing household fields for existing need_points
header('Content-Type: application/json');
require_once 'config.php';
try {
$db = getDB();
// Fetch points missing any of the new fields
$stmt = $db->query("SELECT id, name FROM need_points WHERE household_name IS NULL OR TRIM(household_name) = '' OR head_name IS NULL OR TRIM(head_name) = '' OR economic_status IS NULL OR TRIM(economic_status) = ''");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) {
echo json_encode(['success' => true, 'updated' => 0, 'message' => 'No need_points required backfill.']);
exit;
}
$updateStmt = $db->prepare('UPDATE need_points SET household_name = ?, head_name = ?, economic_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?');
$firstNames = ['Ahmad','Budi','Siti','Aulia','Rizal','Dewi','Agus','Fitri','Hendra','Yulia'];
$lastNames = ['Santoso','Wijaya','Pratama','Hidayat','Saputra','Nur','Ibrahim','Setiawan','Rahman','Putri'];
$statuses = ['sangat_miskin','miskin','rentan','cukup','baik'];
$db->beginTransaction();
$updated = 0;
$examples = [];
foreach ($rows as $r) {
$id = (int)$r['id'];
$hhName = 'Keluarga ' . substr(md5($r['name'] . $id . microtime(true)), 0, 8);
$headName = $firstNames[array_rand($firstNames)] . ' ' . $lastNames[array_rand($lastNames)];
$econ = $statuses[array_rand($statuses)];
$updateStmt->execute([$hhName, $headName, $econ, $id]);
$updated++;
if (count($examples) < 8) {
$examples[] = ['id' => $id, 'household_name' => $hhName, 'head_name' => $headName, 'economic_status' => $econ];
}
}
$db->commit();
echo json_encode(['success' => true, 'updated' => $updated, 'examples' => $examples]);
} catch (Exception $e) {
if (isset($db) && $db->inTransaction()) $db->rollBack();
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
// If called from CLI, also print a newline
if (php_sapi_name() === 'cli') echo "\n";
?>
+79
View File
@@ -0,0 +1,79 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
try {
$db = getDB();
// Derive province totals from seeded need-point names.
// The peta seed data stores the region name inside need_points.name, so we
// map those region labels back to provinces and rank them by count.
$provinceMap = [
'DKI Jakarta' => ['Jakarta'],
'Jawa Barat' => ['Bandung', 'Bekasi'],
'Jawa Tengah' => ['Semarang'],
'DI Yogyakarta' => ['Yogyakarta'],
'Jawa Timur' => ['Surabaya', 'Malang'],
'Sumatera Utara' => ['Medan'],
'Sumatera Selatan' => ['Palembang'],
'Riau' => ['Pekanbaru'],
'Jambi' => ['Jambi'],
'Sumatera Barat' => ['Padang'],
'Sulawesi Selatan' => ['Makassar'],
'Sulawesi Utara' => ['Manado'],
'Sulawesi Tenggara' => ['Kendari'],
'Sulawesi Tengah' => ['Palu'],
'Kalimantan Selatan' => ['Banjarmasin'],
'Kalimantan Barat' => ['Pontianak'],
'Kalimantan Timur' => ['Samarinda'],
'Kalimantan Tengah' => ['Palangkaraya'],
'Papua' => ['Jayapura'],
'Nusa Tenggara Barat' => ['Mataram'],
'Nusa Tenggara Timur' => ['Kupang'],
'Maluku' => ['Ambon'],
'Bali' => ['Denpasar']
];
$needPoints = $db->query('SELECT name FROM need_points')->fetchAll(PDO::FETCH_COLUMN);
$provinceCounts = [];
foreach ($provinceMap as $province => $regions) {
$provinceCounts[$province] = 0;
foreach ($needPoints as $pointName) {
foreach ($regions as $region) {
if (stripos($pointName, $region) !== false) {
$provinceCounts[$province]++;
break;
}
}
}
}
arsort($provinceCounts);
$topProvinces = array_slice($provinceCounts, 0, 5, true);
$areaStats = [];
foreach ($topProvinces as $provinceName => $count) {
$areaStats[] = [
'id' => null,
'area_name' => $provinceName,
'area_type' => 'province',
'household_count' => (int)$count,
'avg_poverty_score' => 0,
'verified_count' => 0,
'assisted_count' => 0
];
}
$response = [
'success' => true,
'areas' => $areaStats
];
http_response_code(200);
die(json_encode($response));
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['error' => 'Database error: ' . $e->getMessage()]));
}
+128
View File
@@ -0,0 +1,128 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$sql = '
SELECT
ad.*,
h.household_code,
h.head_name,
h.verification_status AS household_verification_status
FROM assistance_distributions ad
JOIN households h ON h.id = ad.household_id
WHERE 1 = 1
';
$params = [];
if (isset($_GET['household_id']) && $_GET['household_id'] !== '') {
if (!is_numeric($_GET['household_id'])) {
http_response_code(400);
echo json_encode(['error' => 'household_id must be numeric']);
exit;
}
$sql .= ' AND ad.household_id = ?';
$params[] = (int)$_GET['household_id'];
}
if (isset($_GET['assistance_type']) && $_GET['assistance_type'] !== '') {
$error = null;
$assistanceType = normalizeAssistanceType($_GET['assistance_type'], $error);
if ($assistanceType === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$sql .= ' AND ad.assistance_type = ?';
$params[] = $assistanceType;
}
if (isset($_GET['delivery_status']) && $_GET['delivery_status'] !== '') {
$error = null;
$deliveryStatus = normalizeDeliveryStatus($_GET['delivery_status'], $error);
if ($deliveryStatus === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$sql .= ' AND ad.delivery_status = ?';
$params[] = $deliveryStatus;
}
if (isset($_GET['date_from']) && $_GET['date_from'] !== '') {
$dateFrom = trim((string)$_GET['date_from']);
$dateObj = DateTime::createFromFormat('Y-m-d', $dateFrom);
if (!$dateObj || $dateObj->format('Y-m-d') !== $dateFrom) {
http_response_code(400);
echo json_encode(['error' => 'date_from must be in YYYY-MM-DD format']);
exit;
}
$sql .= ' AND ad.distribution_date >= ?';
$params[] = $dateFrom;
}
if (isset($_GET['date_to']) && $_GET['date_to'] !== '') {
$dateTo = trim((string)$_GET['date_to']);
$dateObj = DateTime::createFromFormat('Y-m-d', $dateTo);
if (!$dateObj || $dateObj->format('Y-m-d') !== $dateTo) {
http_response_code(400);
echo json_encode(['error' => 'date_to must be in YYYY-MM-DD format']);
exit;
}
$sql .= ' AND ad.distribution_date <= ?';
$params[] = $dateTo;
}
$limit = 200;
if (isset($_GET['limit']) && $_GET['limit'] !== '') {
if (!is_numeric($_GET['limit'])) {
http_response_code(400);
echo json_encode(['error' => 'limit must be numeric']);
exit;
}
$limit = max(1, min((int)$_GET['limit'], 1000));
}
$sql .= ' ORDER BY ad.distribution_date DESC, ad.created_at DESC LIMIT ' . $limit;
$stmt = $db->prepare($sql);
$stmt->execute($params);
$distributions = $stmt->fetchAll(PDO::FETCH_ASSOC);
$summarySql = '
SELECT
COUNT(*) AS total_records,
COALESCE(SUM(assistance_value), 0) AS total_assistance_value,
COALESCE(SUM(CASE WHEN delivery_status = \'delivered\' THEN assistance_value ELSE 0 END), 0) AS delivered_assistance_value
FROM assistance_distributions
';
$summaryStmt = $db->query($summarySql);
$summary = $summaryStmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'count' => count($distributions),
'distributions' => $distributions,
'summary' => $summary
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+479
View File
@@ -0,0 +1,479 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
function decodeGeometry($rawGeometry) {
if (is_array($rawGeometry)) {
return $rawGeometry;
}
$decoded = json_decode($rawGeometry, true);
return is_array($decoded) ? $decoded : null;
}
function geometryRings($geometry) {
if (!is_array($geometry) || !isset($geometry['type']) || !isset($geometry['coordinates'])) {
return [];
}
if ($geometry['type'] === 'Polygon') {
return [$geometry['coordinates']];
}
if ($geometry['type'] === 'MultiPolygon') {
return $geometry['coordinates'];
}
return [];
}
function projectLonLatToMeters($lng, $lat, $referenceLat) {
$earthRadius = 6371000.0;
$x = deg2rad((float)$lng) * $earthRadius * cos(deg2rad((float)$referenceLat));
$y = deg2rad((float)$lat) * $earthRadius;
return [$x, $y];
}
function ringAreaSquareMeters($ring) {
if (!is_array($ring) || count($ring) < 4) {
return 0.0;
}
$sumLat = 0.0;
$count = 0;
foreach ($ring as $point) {
if (is_array($point) && count($point) >= 2) {
$sumLat += (float)$point[1];
$count++;
}
}
if ($count === 0) {
return 0.0;
}
$referenceLat = $sumLat / $count;
$projected = [];
foreach ($ring as $point) {
if (is_array($point) && count($point) >= 2) {
$projected[] = projectLonLatToMeters((float)$point[0], (float)$point[1], $referenceLat);
}
}
if (count($projected) < 4) {
return 0.0;
}
$area = 0.0;
$maxIndex = count($projected) - 1;
for ($i = 0; $i < $maxIndex; $i++) {
$area += ($projected[$i][0] * $projected[$i + 1][1]) - ($projected[$i + 1][0] * $projected[$i][1]);
}
return abs($area / 2.0);
}
function geometryAreaKm2($geometry) {
$polygons = geometryRings($geometry);
if (empty($polygons)) {
return 0.0;
}
$totalSquareMeters = 0.0;
foreach ($polygons as $polygonRings) {
if (!is_array($polygonRings) || empty($polygonRings)) {
continue;
}
$outerArea = ringAreaSquareMeters($polygonRings[0]);
$holeArea = 0.0;
for ($i = 1; $i < count($polygonRings); $i++) {
$holeArea += ringAreaSquareMeters($polygonRings[$i]);
}
$totalSquareMeters += max($outerArea - $holeArea, 0.0);
}
return $totalSquareMeters / 1000000.0;
}
function pointInRing($lng, $lat, $ring) {
if (!is_array($ring) || count($ring) < 4) {
return false;
}
$inside = false;
$lastIndex = count($ring) - 1;
for ($i = 0, $j = $lastIndex; $i <= $lastIndex; $j = $i++) {
$xi = (float)$ring[$i][0];
$yi = (float)$ring[$i][1];
$xj = (float)$ring[$j][0];
$yj = (float)$ring[$j][1];
$intersect = (($yi > $lat) !== ($yj > $lat)) &&
($lng < ($xj - $xi) * ($lat - $yi) / (($yj - $yi) ?: 1e-12) + $xi);
if ($intersect) {
$inside = !$inside;
}
}
return $inside;
}
function pointInGeometry($lng, $lat, $geometry) {
$polygons = geometryRings($geometry);
if (empty($polygons)) {
return false;
}
foreach ($polygons as $polygonRings) {
if (!is_array($polygonRings) || empty($polygonRings)) {
continue;
}
if (!pointInRing($lng, $lat, $polygonRings[0])) {
continue;
}
$insideHole = false;
for ($i = 1; $i < count($polygonRings); $i++) {
if (pointInRing($lng, $lat, $polygonRings[$i])) {
$insideHole = true;
break;
}
}
if (!$insideHole) {
return true;
}
}
return false;
}
function geometryCentroid($geometry) {
$polygons = geometryRings($geometry);
$sumLng = 0.0;
$sumLat = 0.0;
$count = 0;
foreach ($polygons as $polygonRings) {
if (!is_array($polygonRings) || empty($polygonRings) || !is_array($polygonRings[0])) {
continue;
}
foreach ($polygonRings[0] as $point) {
if (is_array($point) && count($point) >= 2) {
$sumLng += (float)$point[0];
$sumLat += (float)$point[1];
$count++;
}
}
}
if ($count === 0) {
return ['lng' => 109.33, 'lat' => -0.02];
}
return [
'lng' => $sumLng / $count,
'lat' => $sumLat / $count
];
}
function haversineMeters($lat1, $lng1, $lat2, $lng2) {
$earthRadius = 6371000.0;
$dLat = deg2rad((float)$lat2 - (float)$lat1);
$dLng = deg2rad((float)$lng2 - (float)$lng1);
$a = sin($dLat / 2) * sin($dLat / 2)
+ cos(deg2rad((float)$lat1)) * cos(deg2rad((float)$lat2))
* sin($dLng / 2) * sin($dLng / 2);
$c = 2 * atan2(sqrt($a), sqrt(max(1 - $a, 0.0)));
return $earthRadius * $c;
}
function roadCentroidLngLat($coordinates) {
if (!is_array($coordinates) || empty($coordinates)) {
return null;
}
$sumLat = 0.0;
$sumLng = 0.0;
$count = 0;
foreach ($coordinates as $pair) {
if (is_array($pair) && count($pair) >= 2 && is_numeric($pair[0]) && is_numeric($pair[1])) {
// Road coordinates are stored as [lat, lng]
$sumLat += (float)$pair[0];
$sumLng += (float)$pair[1];
$count++;
}
}
if ($count === 0) {
return null;
}
return [
'lng' => $sumLng / $count,
'lat' => $sumLat / $count
];
}
function deterministicRoadDensity($populationDensity, $areaId) {
$seed = ($areaId * 1103515245 + 12345) & 0x7fffffff;
$variation = 0.8 + (($seed % 1000) / 1000.0) * 0.6;
$base = 0.9 + (log(max($populationDensity, 1.0) + 1.0) / log(10)) * 1.4;
return round($base * $variation, 6);
}
function deterministicDamagedDensity($roadDensity, $areaId) {
$seed = ($areaId * 214013 + 2531011) & 0x7fffffff;
$ratio = 0.14 + (($seed % 1000) / 1000.0) * 0.18;
return round($roadDensity * $ratio, 6);
}
try {
$db = getDB();
// Get layer type parameter
$layer_type = isset($_GET['layer_type']) ? $_GET['layer_type'] : 'population';
$admin_level = isset($_GET['admin_level']) ? strtolower($_GET['admin_level']) : 'kecamatan';
// Validate layer type
$valid_layers = ['population', 'road_density', 'damaged_roads_density', 'city_boundary'];
if (!in_array($layer_type, $valid_layers)) {
throw new Exception('Invalid layer type. Must be: ' . implode(', ', $valid_layers));
}
// Special handling for city boundary
if ($layer_type === 'city_boundary') {
$stmt = $db->prepare('SELECT id, name, geometry FROM pontianak_areas WHERE area_type = ?');
$stmt->execute(['city']);
$cities = $stmt->fetchAll(PDO::FETCH_ASSOC);
$data_items = [];
foreach ($cities as $city) {
$data_items[] = [
'area_id' => $city['id'],
'area_name' => $city['name'],
'value' => 0, // No numeric value for boundary
'geometry' => json_decode($city['geometry'], true)
];
}
http_response_code(200);
echo json_encode([
'success' => true,
'layer_type' => 'city_boundary',
'data' => $data_items,
'min_value' => 0.0,
'max_value' => 1.0,
'admin_level' => 'city',
'value_source' => 'boundary'
]);
exit;
}
$valid_admin_levels = ['kecamatan', 'kelurahan'];
if (!in_array($admin_level, $valid_admin_levels)) {
throw new Exception('Invalid admin_level. Must be: ' . implode(', ', $valid_admin_levels));
}
// Get Pontianak areas by selected admin level
$stmt = $db->prepare('SELECT id, name, geometry, population FROM pontianak_areas WHERE area_type = ? ORDER BY name');
$stmt->execute([$admin_level]);
$areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($areas)) {
// Fallback to kecamatan when kelurahan data does not exist.
if ($admin_level === 'kelurahan') {
$stmt->execute(['kecamatan']);
$areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
$admin_level = 'kecamatan';
}
}
if (empty($areas)) {
throw new Exception('No Pontianak areas found for selected admin level. Run seed_pontianak_data.php?force=true first.');
}
$areaMetrics = [];
foreach ($areas as $area) {
$geometry = decodeGeometry($area['geometry']);
if ($geometry === null) {
continue;
}
$areaKm2 = geometryAreaKm2($geometry);
if ($areaKm2 <= 0) {
$areaKm2 = 0.01;
}
$population = (float)$area['population'];
$populationDensity = $population / $areaKm2;
$areaMetrics[(int)$area['id']] = [
'id' => (int)$area['id'],
'name' => $area['name'],
'geometry' => $geometry,
'area_km2' => $areaKm2,
'centroid' => geometryCentroid($geometry),
'population' => $population,
'population_density' => $populationDensity
];
}
if (empty($areaMetrics)) {
throw new Exception('Area geometry data is invalid for selected admin level.');
}
$areaRoadCounts = [];
$areaDamagedRoadCounts = [];
foreach ($areaMetrics as $metric) {
$areaRoadCounts[$metric['id']] = 0;
$areaDamagedRoadCounts[$metric['id']] = 0;
}
$roadsExist = false;
$valueSource = 'computed';
if ($layer_type === 'road_density' || $layer_type === 'damaged_roads_density') {
$roadStmt = $db->query('SELECT id, coordinates, condition_status FROM roads');
$roads = $roadStmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($roads as $road) {
$coordinates = json_decode($road['coordinates'], true);
$centroid = roadCentroidLngLat($coordinates);
if ($centroid === null) {
continue;
}
$roadsExist = true;
$assignedAreaId = null;
foreach ($areaMetrics as $areaMetric) {
if (pointInGeometry($centroid['lng'], $centroid['lat'], $areaMetric['geometry'])) {
$assignedAreaId = $areaMetric['id'];
break;
}
}
if ($assignedAreaId === null) {
$nearestDistance = null;
foreach ($areaMetrics as $areaMetric) {
$distance = haversineMeters(
$centroid['lat'],
$centroid['lng'],
$areaMetric['centroid']['lat'],
$areaMetric['centroid']['lng']
);
if ($nearestDistance === null || $distance < $nearestDistance) {
$nearestDistance = $distance;
$assignedAreaId = $areaMetric['id'];
}
}
}
if ($assignedAreaId !== null) {
$areaRoadCounts[$assignedAreaId]++;
if (in_array($road['condition_status'], ['minor_damage', 'major_damage'])) {
$areaDamagedRoadCounts[$assignedAreaId]++;
}
}
}
if (!$roadsExist) {
$valueSource = 'dummy';
}
}
$data_items = [];
$values = [];
foreach ($areaMetrics as $areaMetric) {
$area_id = $areaMetric['id'];
$geometry = $areaMetric['geometry'];
$value = 0;
// Calculate value based on layer type
if ($layer_type === 'population') {
$value = $areaMetric['population_density'];
} else if ($layer_type === 'road_density') {
if ($roadsExist) {
$value = $areaRoadCounts[$area_id] / $areaMetric['area_km2'];
} else {
$value = deterministicRoadDensity($areaMetric['population_density'], $area_id);
}
} else if ($layer_type === 'damaged_roads_density') {
if ($roadsExist) {
$value = $areaDamagedRoadCounts[$area_id] / $areaMetric['area_km2'];
} else {
$fallbackRoadDensity = deterministicRoadDensity($areaMetric['population_density'], $area_id);
$value = deterministicDamagedDensity($fallbackRoadDensity, $area_id);
}
}
$value = (float)$value;
$values[] = $value;
$data_items[] = [
'area_id' => $area_id,
'area_name' => $areaMetric['name'],
'value' => $value,
'geometry' => $geometry,
'area_km2' => round($areaMetric['area_km2'], 6),
'population' => $areaMetric['population'],
'population_density' => round($areaMetric['population_density'], 6),
'value_source' => $valueSource
];
}
// Calculate min and max values
$min_value = count($values) > 0 ? min($values) : 0;
$max_value = count($values) > 0 ? max($values) : 0;
// If all values are same, set reasonable range
if ($min_value === $max_value && $min_value > 0) {
$min_value = $min_value * 0.8;
$max_value = $max_value * 1.2;
} else if ($min_value === $max_value) {
$min_value = 0;
$max_value = 1;
}
// Return success response
http_response_code(200);
echo json_encode([
'success' => true,
'layer_type' => $layer_type,
'data' => $data_items,
'min_value' => floatval($min_value),
'max_value' => floatval($max_value),
'admin_level' => $admin_level,
'value_source' => $valueSource
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>
+49
View File
@@ -0,0 +1,49 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
}
try {
$db = getDB();
$stmt = $db->prepare('SELECT key_value FROM score_metadata WHERE key_name = ? LIMIT 1');
$stmt->execute(['target_nasional']);
$targetNasional = $stmt->fetchColumn();
$stmt->execute(['population_reference']);
$populationReference = $stmt->fetchColumn();
if ($targetNasional === false || $targetNasional === null || $targetNasional === '') {
$targetNasional = '7.50';
}
if ($populationReference === false || $populationReference === null || $populationReference === '') {
$populationReference = '288315899';
}
echo json_encode([
'success' => true,
'configuration' => [
'target_nasional' => (float)$targetNasional,
'population_reference' => (int)$populationReference
]
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
?>
+28
View File
@@ -0,0 +1,28 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Handle CORS
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$user = getCurrentUser();
if ($user) {
http_response_code(200);
die(json_encode(['success' => true, 'user' => $user]));
} else {
http_response_code(401);
die(json_encode(['success' => false, 'user' => null]));
}
}
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
+83
View File
@@ -0,0 +1,83 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Get current user to restrict access (optional for MVP)
$user = getCurrentUser();
try {
$db = getDB();
// Get total households
$householdTotal = $db->query('SELECT COUNT(*) as count FROM households')->fetch(PDO::FETCH_ASSOC)['count'];
// Get households by verification status
$statusBreakdown = $db->query('
SELECT verification_status, COUNT(*) as count
FROM households
GROUP BY verification_status
')->fetchAll(PDO::FETCH_ASSOC);
$statusMap = [];
foreach ($statusBreakdown as $row) {
$statusMap[$row['verification_status']] = $row['count'];
}
// Get average SKR (poverty score)
$skrAvg = $db->query('
SELECT
AVG(skr) as avg_skr,
COUNT(*) as scored_count
FROM household_scores
')->fetch(PDO::FETCH_ASSOC);
// Get households assisted
$assistedCount = $db->query('
SELECT COUNT(DISTINCT household_id) as count
FROM assistance_distributions
WHERE delivery_status = \'delivered\'
')->fetch(PDO::FETCH_ASSOC)['count'];
// Get priority level breakdown
$priorityBreakdown = $db->query('
SELECT priority_level, COUNT(*) as count
FROM household_scores
GROUP BY priority_level
')->fetchAll(PDO::FETCH_ASSOC);
$priorityMap = [];
foreach ($priorityBreakdown as $row) {
$priorityMap[$row['priority_level']] = $row['count'];
}
// Get assistance type breakdown
$assistanceTypes = $db->query('
SELECT
assistance_type,
COUNT(*) as count,
SUM(assistance_value) as total_value
FROM assistance_distributions
GROUP BY assistance_type
')->fetchAll(PDO::FETCH_ASSOC);
$response = [
'success' => true,
'summary' => [
'total_households' => $householdTotal,
'avg_poverty_score' => round($skrAvg['avg_skr'] ?? 0, 2),
'households_scored' => $skrAvg['scored_count'] ?? 0,
'households_assisted' => $assistedCount,
'percentage_assisted' => $householdTotal > 0 ? round(($assistedCount / $householdTotal) * 100, 1) : 0
],
'status_breakdown' => $statusMap,
'priority_breakdown' => $priorityMap,
'assistance_by_type' => $assistanceTypes
];
http_response_code(200);
die(json_encode($response));
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['error' => 'Database error: ' . $e->getMessage()]));
}
+98
View File
@@ -0,0 +1,98 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Get current user to restrict access
$user = getCurrentUser();
try {
$db = getDB();
// Get actual need points count from database (real poor population data)
$needPointsStats = $db->query('SELECT COUNT(*) as total FROM need_points')->fetch(PDO::FETCH_ASSOC);
$totalNeedPoints = (int)$needPointsStats['total'];
// Get configuration from database: population reference and target nasional
$configStmt = $db->prepare('SELECT key_value FROM score_metadata WHERE key_name = ? LIMIT 1');
$configStmt->execute(['population_reference']);
$populationReference = (int)$configStmt->fetchColumn();
if (!$populationReference || $populationReference <= 0) {
$populationReference = 288315899; // Default fallback
}
$configStmt->execute(['target_nasional']);
$targetNasional = (float)$configStmt->fetchColumn();
if (!$targetNasional || $targetNasional <= 0) {
$targetNasional = 7.50; // Default fallback
}
// Calculate poverty percentage from real data
$currentPovertyPercentage = ($totalNeedPoints / $populationReference) * 100;
$response = [
'success' => true,
'charts' => [
// Chart 1: Absolute population comparison (real data from system)
'chart1' => [
'title' => 'Total Penduduk Miskin vs Total Populasi Indonesia',
'type' => 'bar',
'labels' => [
'Penduduk Miskin',
'Total Populasi Indonesia'
],
'data' => [
$totalNeedPoints,
$populationReference
],
'units' => [
'jiwa',
'jiwa'
]
],
// Chart 2: Percentage comparison against national target (real data)
'chart2' => [
'title' => 'Persentase Kemiskinan vs Target Nasional',
'type' => 'bar',
'labels' => [
'Persentase Kemiskinan',
'Target Nasional'
],
'data' => [
round($currentPovertyPercentage, 6),
$targetNasional
],
'units' => [
'%',
'%'
],
'current_poverty' => round($currentPovertyPercentage, 6),
'target' => $targetNasional
]
],
'statistics' => [
'total_need_points' => $totalNeedPoints,
'population_reference' => $populationReference,
'current_poverty_percentage' => round($currentPovertyPercentage, 6),
'national_target' => $targetNasional
]
];
http_response_code(200);
die(json_encode($response));
} catch (PDOException $e) {
http_response_code(500);
die(json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]));
} catch (Exception $e) {
http_response_code(400);
die(json_encode([
'success' => false,
'error' => 'Error: ' . $e->getMessage()
]));
}
?>
+123
View File
@@ -0,0 +1,123 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
syncNeedPointsToHouseholds($db);
ensureHouseholdScoresForAllHouseholds($db);
$scoreCountStmt = $db->query('SELECT COUNT(*) AS count FROM household_scores');
$scoreCount = (int)$scoreCountStmt->fetch(PDO::FETCH_ASSOC)['count'];
$sql = '
SELECT
hs.household_id,
hs.skr,
hs.sag,
hs.spi,
hs.priority_level,
hs.score_version,
hs.computed_at,
h.household_code,
h.head_name,
h.economic_status,
h.monthly_income,
h.dependents_count,
h.verification_status,
CASE WHEN EXISTS (
SELECT 1
FROM assistance_distributions ad
WHERE ad.household_id = hs.household_id
AND ad.delivery_status = \'delivered\'
) THEN 1 ELSE 0 END AS assisted,
m.name AS masjid_name,
pa.name AS area_name
FROM household_scores hs
JOIN households h ON h.id = hs.household_id
LEFT JOIN masjids m ON m.id = h.masjid_id
LEFT JOIN pontianak_areas pa ON pa.id = h.pontianak_area_id
WHERE 1 = 1
';
$params = [];
if (isset($_GET['priority_level']) && $_GET['priority_level'] !== '') {
$allowed = ['very_high', 'high', 'medium', 'low'];
$priority = strtolower(trim((string)$_GET['priority_level']));
if (!in_array($priority, $allowed, true)) {
http_response_code(400);
echo json_encode(['error' => 'priority_level is invalid']);
exit;
}
$sql .= ' AND hs.priority_level = ?';
$params[] = $priority;
}
if (isset($_GET['min_spi']) && $_GET['min_spi'] !== '') {
if (!is_numeric($_GET['min_spi'])) {
http_response_code(400);
echo json_encode(['error' => 'min_spi must be numeric']);
exit;
}
$minSpi = (float)$_GET['min_spi'];
$sql .= ' AND hs.spi >= ?';
$params[] = $minSpi;
}
$includeAssisted = isset($_GET['include_assisted']) && $_GET['include_assisted'] !== '' && $_GET['include_assisted'] !== '0';
if (!$includeAssisted) {
$sql .= ' AND NOT EXISTS (SELECT 1 FROM assistance_distributions ad WHERE ad.household_id = hs.household_id AND ad.delivery_status = \'delivered\')';
}
$limit = 100;
if (isset($_GET['limit']) && $_GET['limit'] !== '') {
if (!is_numeric($_GET['limit'])) {
http_response_code(400);
echo json_encode(['error' => 'limit must be numeric']);
exit;
}
$limit = max(1, min((int)$_GET['limit'], 5000));
}
$offset = 0;
if (isset($_GET['offset']) && $_GET['offset'] !== '') {
if (!is_numeric($_GET['offset'])) {
http_response_code(400);
echo json_encode(['error' => 'offset must be numeric']);
exit;
}
$offset = max(0, (int)$_GET['offset']);
}
$sql .= ' ORDER BY hs.spi DESC, hs.skr DESC, hs.computed_at DESC LIMIT ' . $limit . ' OFFSET ' . $offset;
$stmt = $db->prepare($sql);
$stmt->execute($params);
$ranking = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'count' => count($ranking),
'limit' => $limit,
'offset' => $offset,
'assisted_count' => (int)$db->query("SELECT COUNT(DISTINCT household_id) AS count FROM assistance_distributions WHERE delivery_status = 'delivered'")->fetch(PDO::FETCH_ASSOC)['count'],
'ranking' => $ranking,
'score_version' => 'v1'
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+104
View File
@@ -0,0 +1,104 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$syncResult = syncNeedPointsToHouseholds($db);
$computedMissing = ensureHouseholdScoresForAllHouseholds($db);
$totalStmt = $db->query('SELECT COUNT(*) AS total_households FROM households');
$totalHouseholds = (int)$totalStmt->fetch(PDO::FETCH_ASSOC)['total_households'];
$scoreTotalStmt = $db->query('SELECT COUNT(*) AS scored_households FROM household_scores');
$scoredHouseholds = (int)$scoreTotalStmt->fetch(PDO::FETCH_ASSOC)['scored_households'];
$assistedStmt = $db->query("\n SELECT COUNT(DISTINCT household_id) AS assisted_households\n FROM assistance_distributions\n WHERE delivery_status = 'delivered'\n ");
$assistedHouseholds = (int)$assistedStmt->fetch(PDO::FETCH_ASSOC)['assisted_households'];
$priorityStmt = $db->query('
SELECT
priority_level,
COUNT(*) AS count
FROM household_scores hs
JOIN households h ON h.id = hs.household_id
WHERE NOT EXISTS (
SELECT 1
FROM assistance_distributions ad
WHERE ad.household_id = hs.household_id
AND ad.delivery_status = \'delivered\'
)
GROUP BY priority_level
');
$priorityRows = $priorityStmt->fetchAll(PDO::FETCH_ASSOC);
$priorityCounts = [
'very_high' => 0,
'high' => 0,
'medium' => 0,
'low' => 0
];
foreach ($priorityRows as $row) {
$priority = $row['priority_level'];
$priorityCounts[$priority] = (int)$row['count'];
}
$avgStmt = $db->query('
SELECT
COALESCE(AVG(skr), 0) AS avg_skr,
COALESCE(AVG(sag), 0) AS avg_sag,
COALESCE(AVG(spi), 0) AS avg_spi,
MAX(computed_at) AS latest_computed_at
FROM household_scores
');
$averages = $avgStmt->fetch(PDO::FETCH_ASSOC);
$topStmt = $db->query('
SELECT
hs.household_id,
h.household_code,
h.head_name,
hs.spi,
hs.priority_level
FROM household_scores hs
JOIN households h ON h.id = hs.household_id
WHERE NOT EXISTS (
SELECT 1
FROM assistance_distributions ad
WHERE ad.household_id = hs.household_id
AND ad.delivery_status = \'delivered\'
)
ORDER BY hs.spi DESC, hs.skr DESC
LIMIT 5
');
$topPriorities = $topStmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'summary' => [
'total_households' => $totalHouseholds,
'scored_households' => $scoredHouseholds,
'assisted_households' => $assistedHouseholds,
'priority_counts' => $priorityCounts,
'averages' => $averages,
'top_priorities' => $topPriorities,
'score_version' => 'v1',
'sync_result' => $syncResult,
'computed_missing_scores' => $computedMissing
]
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+74
View File
@@ -0,0 +1,74 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$totalsStmt = $db->query('
SELECT
COUNT(*) AS total_households,
SUM(CASE WHEN verification_status = \'unverified\' THEN 1 ELSE 0 END) AS unverified_count,
SUM(CASE WHEN verification_status = \'field_verified\' THEN 1 ELSE 0 END) AS field_verified_count,
SUM(CASE WHEN verification_status = \'admin_approved\' THEN 1 ELSE 0 END) AS admin_approved_count,
SUM(CASE WHEN verification_status = \'rejected\' THEN 1 ELSE 0 END) AS rejected_count,
SUM(CASE WHEN verification_status = \'needs_review\' THEN 1 ELSE 0 END) AS needs_review_count,
COALESCE(AVG(monthly_income), 0) AS avg_monthly_income
FROM households
');
$totals = $totalsStmt->fetch(PDO::FETCH_ASSOC);
$assistanceStmt = $db->query('
SELECT
COUNT(*) AS total_distributions,
COUNT(DISTINCT household_id) AS assisted_households,
COALESCE(SUM(assistance_value), 0) AS total_assistance_value,
COALESCE(SUM(CASE WHEN delivery_status = \'delivered\' THEN assistance_value ELSE 0 END), 0) AS delivered_assistance_value
FROM assistance_distributions
');
$assistance = $assistanceStmt->fetch(PDO::FETCH_ASSOC);
$verificationStmt = $db->query('
SELECT
COUNT(*) AS total_verifications,
SUM(CASE WHEN DATE(verified_at) >= DATE(\'now\', \'-7 day\') THEN 1 ELSE 0 END) AS verifications_last_7_days,
COALESCE(AVG(data_confidence_score), 0) AS avg_confidence_score
FROM verification_logs
');
$verification = $verificationStmt->fetch(PDO::FETCH_ASSOC);
$topMasjidStmt = $db->query('
SELECT
m.id,
m.name,
COUNT(h.id) AS household_count
FROM masjids m
LEFT JOIN households h ON h.masjid_id = m.id
GROUP BY m.id, m.name
ORDER BY household_count DESC, m.name ASC
LIMIT 5
');
$topMasjidCoverage = $topMasjidStmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'summary' => [
'totals' => $totals,
'assistance' => $assistance,
'verification' => $verification,
'top_masjid_coverage' => $topMasjidCoverage
]
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+111
View File
@@ -0,0 +1,111 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
syncNeedPointsToHouseholds($db);
ensureHouseholdScoresForAllHouseholds($db);
$sql = '
SELECT
h.*,
np.name AS need_point_name,
m.name AS masjid_name,
pa.name AS area_name,
(
SELECT COUNT(*)
FROM assistance_distributions ad
WHERE ad.household_id = h.id
) AS assistance_count,
(
SELECT MAX(vl.verified_at)
FROM verification_logs vl
WHERE vl.household_id = h.id
) AS last_verified_at
FROM households h
LEFT JOIN need_points np ON np.id = h.need_point_id
LEFT JOIN masjids m ON m.id = h.masjid_id
LEFT JOIN pontianak_areas pa ON pa.id = h.pontianak_area_id
WHERE 1 = 1
';
$params = [];
if (isset($_GET['verification_status']) && $_GET['verification_status'] !== '') {
$error = null;
$status = normalizeVerificationStatus($_GET['verification_status'], $error, true);
if ($status === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$sql .= ' AND h.verification_status = ?';
$params[] = $status;
}
if (isset($_GET['masjid_id']) && $_GET['masjid_id'] !== '') {
if (!is_numeric($_GET['masjid_id'])) {
http_response_code(400);
echo json_encode(['error' => 'masjid_id must be numeric']);
exit;
}
$sql .= ' AND h.masjid_id = ?';
$params[] = (int)$_GET['masjid_id'];
}
if (isset($_GET['pontianak_area_id']) && $_GET['pontianak_area_id'] !== '') {
if (!is_numeric($_GET['pontianak_area_id'])) {
http_response_code(400);
echo json_encode(['error' => 'pontianak_area_id must be numeric']);
exit;
}
$sql .= ' AND h.pontianak_area_id = ?';
$params[] = (int)$_GET['pontianak_area_id'];
}
if (isset($_GET['q']) && trim((string)$_GET['q']) !== '') {
$q = '%' . trim((string)$_GET['q']) . '%';
$sql .= ' AND (h.household_code LIKE ? OR h.head_name LIKE ?)';
$params[] = $q;
$params[] = $q;
}
$limit = 200;
if (isset($_GET['limit']) && $_GET['limit'] !== '') {
if (!is_numeric($_GET['limit'])) {
http_response_code(400);
echo json_encode(['error' => 'limit must be numeric']);
exit;
}
$limit = max(1, min((int)$_GET['limit'], 1000));
}
$sql .= ' ORDER BY h.created_at DESC LIMIT ' . $limit;
$stmt = $db->prepare($sql);
$stmt->execute($params);
$households = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'count' => count($households),
'households' => $households
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query('SELECT id, name, road_type, coordinates, length_meters, created_at, updated_at FROM roads ORDER BY created_at DESC');
$roads = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decode coordinates JSON for each road
foreach ($roads as &$road) {
$road['coordinates'] = json_decode($road['coordinates'], true);
}
echo json_encode([
'success' => true,
'lines' => $roads
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+41
View File
@@ -0,0 +1,41 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query('
SELECT
m.id,
m.name,
m.latitude,
m.longitude,
m.radius_meters,
m.created_at,
m.updated_at,
COUNT(np.id) AS need_point_count
FROM masjids m
LEFT JOIN need_points np ON np.masjid_id = m.id
GROUP BY m.id
ORDER BY m.created_at DESC
');
$masjids = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'masjids' => $masjids
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+36
View File
@@ -0,0 +1,36 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query("\n SELECT\n np.id,\n np.name,\n np.latitude,\n np.longitude,\n np.masjid_id,\n np.household_name,\n np.head_name,\n np.economic_status,\n np.is_verified,\n CASE WHEN EXISTS (\n SELECT 1\n FROM households h\n JOIN assistance_distributions ad ON ad.household_id = h.id\n WHERE h.need_point_id = np.id\n AND ad.delivery_status = 'delivered'\n ) THEN 1 ELSE 0 END AS assisted,\n np.created_at,\n np.updated_at,\n m.name AS masjid_name,\n m.latitude AS masjid_latitude,\n m.longitude AS masjid_longitude,\n m.radius_meters\n FROM need_points np\n JOIN masjids m ON m.id = np.masjid_id\n ORDER BY np.created_at DESC\n ");
$points = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($points as &$point) {
$point['distance_meters'] = haversineDistanceMeters(
(float)$point['latitude'],
(float)$point['longitude'],
(float)$point['masjid_latitude'],
(float)$point['masjid_longitude']
);
}
unset($point);
echo json_encode([
'success' => true,
'points' => $points
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+26
View File
@@ -0,0 +1,26 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query('SELECT id, name, nomor_spbu, latitude, longitude, open_24_hours, created_at FROM markers ORDER BY created_at DESC');
$markers = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'markers' => $markers
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query('SELECT id, name, certificate_type, coordinates, area_sqm, created_at, updated_at FROM land_parcels ORDER BY created_at DESC');
$parcels = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decode coordinates JSON for each parcel
foreach ($parcels as &$parcel) {
$parcel['coordinates'] = json_decode($parcel['coordinates'], true);
}
echo json_encode([
'success' => true,
'polygons' => $parcels
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+50
View File
@@ -0,0 +1,50 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
try {
$db = getDB();
// Get optional filter parameter
$area_type = isset($_GET['area_type']) ? $_GET['area_type'] : '';
// Build SQL query
$sql = 'SELECT id, name, area_type, geometry, population, created_at, updated_at FROM pontianak_areas';
if ($area_type) {
$sql .= ' WHERE area_type = ?';
$stmt = $db->prepare($sql);
$stmt->execute([$area_type]);
} else {
$stmt = $db->prepare($sql);
$stmt->execute();
}
$areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decode geometry JSON for each area
foreach ($areas as &$area) {
$area['geometry'] = json_decode($area['geometry'], true);
}
// Return success response
http_response_code(200);
echo json_encode([
'success' => true,
'areas' => $areas
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>
+33
View File
@@ -0,0 +1,33 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Check authentication and authorization
requireRole(['Admin']);
try {
$db = getDB();
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Get all users
$stmt = $db->query('
SELECT id, username, email, role, is_active, created_at
FROM users
ORDER BY created_at DESC
');
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
http_response_code(200);
die(json_encode([
'success' => true,
'users' => $users
]));
}
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['error' => 'Database error: ' . $e->getMessage()]));
}
+109
View File
@@ -0,0 +1,109 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$sql = '
SELECT
vl.*,
h.household_code,
h.head_name,
h.verification_status AS current_household_status
FROM verification_logs vl
JOIN households h ON h.id = vl.household_id
WHERE 1 = 1
';
$params = [];
if (isset($_GET['household_id']) && $_GET['household_id'] !== '') {
if (!is_numeric($_GET['household_id'])) {
http_response_code(400);
echo json_encode(['error' => 'household_id must be numeric']);
exit;
}
$sql .= ' AND vl.household_id = ?';
$params[] = (int)$_GET['household_id'];
}
if (isset($_GET['verification_status']) && $_GET['verification_status'] !== '') {
$error = null;
$status = normalizeVerificationStatus($_GET['verification_status'], $error, false);
if ($status === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$sql .= ' AND vl.verification_status = ?';
$params[] = $status;
}
if (isset($_GET['verifier_role']) && trim((string)$_GET['verifier_role']) !== '') {
$sql .= ' AND vl.verifier_role = ?';
$params[] = trim((string)$_GET['verifier_role']);
}
if (isset($_GET['date_from']) && $_GET['date_from'] !== '') {
$dateFrom = trim((string)$_GET['date_from']);
$dateObj = DateTime::createFromFormat('Y-m-d', $dateFrom);
if (!$dateObj || $dateObj->format('Y-m-d') !== $dateFrom) {
http_response_code(400);
echo json_encode(['error' => 'date_from must be in YYYY-MM-DD format']);
exit;
}
$sql .= ' AND DATE(vl.verified_at) >= ?';
$params[] = $dateFrom;
}
if (isset($_GET['date_to']) && $_GET['date_to'] !== '') {
$dateTo = trim((string)$_GET['date_to']);
$dateObj = DateTime::createFromFormat('Y-m-d', $dateTo);
if (!$dateObj || $dateObj->format('Y-m-d') !== $dateTo) {
http_response_code(400);
echo json_encode(['error' => 'date_to must be in YYYY-MM-DD format']);
exit;
}
$sql .= ' AND DATE(vl.verified_at) <= ?';
$params[] = $dateTo;
}
$limit = 200;
if (isset($_GET['limit']) && $_GET['limit'] !== '') {
if (!is_numeric($_GET['limit'])) {
http_response_code(400);
echo json_encode(['error' => 'limit must be numeric']);
exit;
}
$limit = max(1, min((int)$_GET['limit'], 1000));
}
$sql .= ' ORDER BY vl.verified_at DESC, vl.created_at DESC LIMIT ' . $limit;
$stmt = $db->prepare($sql);
$stmt->execute($params);
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'count' => count($logs),
'verification_logs' => $logs
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+35
View File
@@ -0,0 +1,35 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Handle CORS
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['email']) || !isset($data['password'])) {
http_response_code(400);
die(json_encode(['error' => 'Email and password are required']));
}
$result = loginUser($data['email'], $data['password']);
if ($result['success']) {
http_response_code(200);
} else {
http_response_code(401);
}
die(json_encode($result));
}
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
+22
View File
@@ -0,0 +1,22 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Handle CORS
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = logoutUser();
http_response_code(200);
die(json_encode($result));
}
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
+134
View File
@@ -0,0 +1,134 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!is_array($data)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON body']);
exit;
}
if (!isset($data['household_id']) || !is_numeric($data['household_id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid household_id']);
exit;
}
$householdId = (int)$data['household_id'];
$error = null;
$assistanceType = normalizeAssistanceType($data['assistance_type'] ?? null, $error);
if ($assistanceType === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$assistanceValue = normalizeAssistanceValue($data['assistance_value'] ?? null, $error);
if ($assistanceValue === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$assistanceFrequency = normalizeAssistanceFrequency($data['assistance_frequency'] ?? null, $error);
if ($assistanceFrequency === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$deliveryStatus = normalizeDeliveryStatus($data['delivery_status'] ?? null, $error);
if ($deliveryStatus === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$distributionDate = isset($data['distribution_date']) && trim((string)$data['distribution_date']) !== ''
? trim((string)$data['distribution_date'])
: date('Y-m-d');
$dateObj = DateTime::createFromFormat('Y-m-d', $distributionDate);
if (!$dateObj || $dateObj->format('Y-m-d') !== $distributionDate) {
http_response_code(400);
echo json_encode(['error' => 'distribution_date must be in YYYY-MM-DD format']);
exit;
}
$deliveredBy = isset($data['delivered_by']) ? trim((string)$data['delivered_by']) : null;
if ($deliveredBy === '') {
$deliveredBy = null;
}
$notes = isset($data['notes']) ? trim((string)$data['notes']) : null;
if ($notes === '') {
$notes = null;
}
$db = getDB();
if (!householdExists($db, $householdId)) {
http_response_code(404);
echo json_encode(['error' => 'Household not found']);
exit;
}
$insertStmt = $db->prepare('
INSERT INTO assistance_distributions (
household_id,
assistance_type,
assistance_value,
assistance_frequency,
distribution_date,
delivery_status,
delivered_by,
notes,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
');
$insertStmt->execute([
$householdId,
$assistanceType,
$assistanceValue,
$assistanceFrequency,
$distributionDate,
$deliveryStatus,
$deliveredBy,
$notes
]);
$id = (int)$db->lastInsertId();
$resultStmt = $db->prepare('
SELECT
ad.*,
h.household_code,
h.head_name
FROM assistance_distributions ad
JOIN households h ON h.id = ad.household_id
WHERE ad.id = ?
');
$resultStmt->execute([$id]);
$distribution = $resultStmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'distribution' => $distribution
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+53
View File
@@ -0,0 +1,53 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
}
requireRole('Admin');
try {
$data = json_decode(file_get_contents('php://input'), true);
$targetNasional = isset($data['target_nasional']) ? (float)$data['target_nasional'] : null;
$populationReference = isset($data['population_reference']) ? (int)$data['population_reference'] : null;
if ($targetNasional === null || $targetNasional < 0 || $targetNasional > 100) {
http_response_code(400);
die(json_encode(['error' => 'Target nasional must be between 0 and 100']));
}
if ($populationReference === null || $populationReference < 1) {
http_response_code(400);
die(json_encode(['error' => 'Population reference must be greater than 0']));
}
$db = getDB();
$stmt = $db->prepare('INSERT INTO score_metadata (key_name, key_value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key_name) DO UPDATE SET key_value = excluded.key_value, updated_at = CURRENT_TIMESTAMP');
$stmt->execute(['target_nasional', number_format($targetNasional, 2, '.', '')]);
$stmt->execute(['population_reference', (string)$populationReference]);
echo json_encode([
'success' => true,
'configuration' => [
'target_nasional' => $targetNasional,
'population_reference' => $populationReference
]
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
?>
+240
View File
@@ -0,0 +1,240 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!is_array($data)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON body']);
exit;
}
if (!isset($data['head_name']) || trim((string)$data['head_name']) === '') {
http_response_code(400);
echo json_encode(['error' => 'Missing required field: head_name']);
exit;
}
if (!isset($data['latitude']) || !isset($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: latitude and longitude']);
exit;
}
$error = null;
if (!validateCoordinates($data['latitude'], $data['longitude'], $error)) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$db = getDB();
$headName = trim((string)$data['head_name']);
$latitude = (float)$data['latitude'];
$longitude = (float)$data['longitude'];
$monthlyIncome = normalizeMonthlyIncome($data['monthly_income'] ?? null, $error);
if ($monthlyIncome === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$dependentsCount = normalizeDependentsCount($data['dependents_count'] ?? null, $error);
if ($dependentsCount === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$housingConditionScore = normalizeHousingConditionScore($data['housing_condition_score'] ?? null, $error);
if ($housingConditionScore === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$employmentStatus = normalizeEmploymentStatus($data['employment_status'] ?? null, $error);
if ($employmentStatus === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$economicStatus = isset($data['economic_status']) ? strtolower(trim((string)$data['economic_status'])) : 'unknown';
$allowedEconomicStatuses = ['sangat_miskin', 'miskin', 'rentan', 'cukup', 'baik', 'unknown'];
if (!in_array($economicStatus, $allowedEconomicStatuses, true)) {
http_response_code(400);
echo json_encode(['error' => 'economic_status is invalid']);
exit;
}
$verificationStatus = normalizeVerificationStatus($data['verification_status'] ?? null, $error, true);
if ($verificationStatus === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$needPointId = null;
if (array_key_exists('need_point_id', $data) && $data['need_point_id'] !== null && $data['need_point_id'] !== '') {
if (!is_numeric($data['need_point_id'])) {
http_response_code(400);
echo json_encode(['error' => 'need_point_id must be numeric']);
exit;
}
$needPointId = (int)$data['need_point_id'];
$needPointStmt = $db->prepare('SELECT id, masjid_id FROM need_points WHERE id = ?');
$needPointStmt->execute([$needPointId]);
$needPoint = $needPointStmt->fetch(PDO::FETCH_ASSOC);
if (!$needPoint) {
http_response_code(400);
echo json_encode(['error' => 'need_point_id is not found']);
exit;
}
}
$masjidId = null;
if (array_key_exists('masjid_id', $data) && $data['masjid_id'] !== null && $data['masjid_id'] !== '') {
if (!is_numeric($data['masjid_id'])) {
http_response_code(400);
echo json_encode(['error' => 'masjid_id must be numeric']);
exit;
}
$masjidId = (int)$data['masjid_id'];
$masjidStmt = $db->prepare('SELECT id FROM masjids WHERE id = ?');
$masjidStmt->execute([$masjidId]);
if (!$masjidStmt->fetch(PDO::FETCH_ASSOC)) {
http_response_code(400);
echo json_encode(['error' => 'masjid_id is not found']);
exit;
}
}
if ($masjidId === null) {
if ($needPointId !== null) {
$npMasjidStmt = $db->prepare('SELECT masjid_id FROM need_points WHERE id = ?');
$npMasjidStmt->execute([$needPointId]);
$npMasjid = $npMasjidStmt->fetch(PDO::FETCH_ASSOC);
if ($npMasjid && isset($npMasjid['masjid_id'])) {
$masjidId = (int)$npMasjid['masjid_id'];
}
}
if ($masjidId === null) {
$nearestMasjid = findNearestMasjidCoveringPoint($db, $latitude, $longitude);
if ($nearestMasjid) {
$masjidId = (int)$nearestMasjid['id'];
}
}
}
$pontianakAreaId = null;
if (array_key_exists('pontianak_area_id', $data) && $data['pontianak_area_id'] !== null && $data['pontianak_area_id'] !== '') {
if (!is_numeric($data['pontianak_area_id'])) {
http_response_code(400);
echo json_encode(['error' => 'pontianak_area_id must be numeric']);
exit;
}
$pontianakAreaId = (int)$data['pontianak_area_id'];
$areaStmt = $db->prepare('SELECT id FROM pontianak_areas WHERE id = ?');
$areaStmt->execute([$pontianakAreaId]);
if (!$areaStmt->fetch(PDO::FETCH_ASSOC)) {
http_response_code(400);
echo json_encode(['error' => 'pontianak_area_id is not found']);
exit;
}
}
$householdCode = normalizeHouseholdCode($db, $data['household_code'] ?? null);
$nikHash = null;
if (array_key_exists('nik', $data)) {
$nikHash = normalizeNikHashFromInput($data['nik']);
} else if (array_key_exists('nik_hash', $data)) {
$nikHash = trim((string)$data['nik_hash']);
if ($nikHash === '') {
$nikHash = null;
}
}
$vulnerabilityNotes = isset($data['vulnerability_notes']) ? trim((string)$data['vulnerability_notes']) : null;
if ($vulnerabilityNotes === '') {
$vulnerabilityNotes = null;
}
$insertStmt = $db->prepare('
INSERT INTO households (
household_code,
head_name,
nik_hash,
latitude,
longitude,
need_point_id,
masjid_id,
pontianak_area_id,
monthly_income,
dependents_count,
housing_condition_score,
employment_status,
economic_status,
verification_status,
vulnerability_notes
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
');
$insertStmt->execute([
$householdCode,
$headName,
$nikHash,
$latitude,
$longitude,
$needPointId,
$masjidId,
$pontianakAreaId,
$monthlyIncome,
$dependentsCount,
$housingConditionScore,
$employmentStatus,
$economicStatus,
$verificationStatus,
$vulnerabilityNotes
]);
$id = (int)$db->lastInsertId();
$household = getHouseholdById($db, $id);
echo json_encode([
'success' => true,
'household' => $household
]);
} catch (PDOException $e) {
if ($e->getCode() === '23000') {
http_response_code(409);
echo json_encode(['error' => 'Duplicate household_code or need_point_id mapping']);
exit;
}
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+113
View File
@@ -0,0 +1,113 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['name']) || !isset($data['road_type']) || !isset($data['coordinates']) || !isset($data['length_meters'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: name, road_type, coordinates, or length_meters']);
exit;
}
// Validate road_type
$validTypes = ['Nasional', 'Provinsi', 'Kabupaten'];
if (!in_array($data['road_type'], $validTypes)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid road_type. Must be: Nasional, Provinsi, or Kabupaten']);
exit;
}
// Validate coordinates array
if (!is_array($data['coordinates']) || count($data['coordinates']) < 2) {
http_response_code(400);
echo json_encode(['error' => 'Coordinates must be an array with at least 2 points']);
exit;
}
// Validate each coordinate
foreach ($data['coordinates'] as $coord) {
if (!is_array($coord) || count($coord) !== 2) {
http_response_code(400);
echo json_encode(['error' => 'Each coordinate must be [latitude, longitude]']);
exit;
}
$lat = floatval($coord[0]);
$lng = floatval($coord[1]);
if (!is_numeric($coord[0]) || !is_numeric($coord[1])) {
http_response_code(400);
echo json_encode(['error' => 'Coordinates must be numeric values']);
exit;
}
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinate range. Latitude: -90 to 90, Longitude: -180 to 180']);
exit;
}
}
// Validate name is not empty
if (empty(trim($data['name']))) {
http_response_code(400);
echo json_encode(['error' => 'Road name cannot be empty']);
exit;
}
// Validate length_meters is positive
if (!is_numeric($data['length_meters']) || $data['length_meters'] <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Length must be a positive number']);
exit;
}
// Validate condition_status if provided
$condition_status = isset($data['condition_status']) ? $data['condition_status'] : 'normal';
$validConditions = ['normal', 'minor_damage', 'major_damage'];
if (!in_array($condition_status, $validConditions)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid condition_status. Must be: normal, minor_damage, or major_damage']);
exit;
}
$db = getDB();
// Store coordinates as JSON
$coordinatesJson = json_encode($data['coordinates']);
$stmt = $db->prepare('INSERT INTO roads (name, road_type, coordinates, length_meters, condition_status) VALUES (?, ?, ?, ?, ?)');
$stmt->execute([
$data['name'],
$data['road_type'],
$coordinatesJson,
floatval($data['length_meters']),
$condition_status
]);
echo json_encode([
'success' => true,
'id' => $db->lastInsertId(),
'name' => $data['name'],
'road_type' => $data['road_type'],
'coordinates' => $data['coordinates'],
'length_meters' => floatval($data['length_meters']),
'condition_status' => $condition_status,
'created_at' => date('Y-m-d H:i:s')
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+76
View File
@@ -0,0 +1,76 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['name']) || !isset($data['latitude']) || !isset($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: name, latitude, or longitude']);
exit;
}
$name = trim($data['name']);
if ($name === '') {
http_response_code(400);
echo json_encode(['error' => 'Name cannot be empty']);
exit;
}
if (!is_numeric($data['latitude']) || !is_numeric($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Latitude and longitude must be numeric values']);
exit;
}
$latitude = (float)$data['latitude'];
$longitude = (float)$data['longitude'];
if ($latitude < -90 || $latitude > 90) {
http_response_code(400);
echo json_encode(['error' => 'Latitude must be between -90 and 90']);
exit;
}
if ($longitude < -180 || $longitude > 180) {
http_response_code(400);
echo json_encode(['error' => 'Longitude must be between -180 and 180']);
exit;
}
$radiusError = null;
$radius = normalizeMasjidRadius(isset($data['radius_meters']) ? $data['radius_meters'] : null, $radiusError);
if ($radius === null) {
http_response_code(400);
echo json_encode(['error' => $radiusError]);
exit;
}
$db = getDB();
$stmt = $db->prepare('INSERT INTO masjids (name, latitude, longitude, radius_meters) VALUES (?, ?, ?, ?)');
$stmt->execute([$name, $latitude, $longitude, $radius]);
echo json_encode([
'success' => true,
'masjid' => [
'id' => (int)$db->lastInsertId(),
'name' => $name,
'latitude' => $latitude,
'longitude' => $longitude,
'radius_meters' => $radius
]
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+91
View File
@@ -0,0 +1,91 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['name']) || !isset($data['latitude']) || !isset($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: name, latitude, or longitude']);
exit;
}
$name = trim($data['name']);
if ($name === '') {
http_response_code(400);
echo json_encode(['error' => 'Name cannot be empty']);
exit;
}
if (!is_numeric($data['latitude']) || !is_numeric($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Latitude and longitude must be numeric values']);
exit;
}
$latitude = (float)$data['latitude'];
$longitude = (float)$data['longitude'];
if ($latitude < -90 || $latitude > 90) {
http_response_code(400);
echo json_encode(['error' => 'Latitude must be between -90 and 90']);
exit;
}
if ($longitude < -180 || $longitude > 180) {
http_response_code(400);
echo json_encode(['error' => 'Longitude must be between -180 and 180']);
exit;
}
$db = getDB();
$nearestMasjid = findNearestMasjidCoveringPoint($db, $latitude, $longitude);
if ($nearestMasjid === null) {
http_response_code(400);
echo json_encode(['error' => 'Point must be inside at least one masjid radius']);
exit;
}
// optional household details
$householdName = isset($data['household_name']) ? trim($data['household_name']) : null;
$headName = isset($data['head_name']) ? trim($data['head_name']) : null;
$economicStatus = isset($data['economic_status']) ? trim($data['economic_status']) : null;
// generate defaults when missing
if (!$householdName) $householdName = 'Keluarga ' . substr(md5(uniqid('', true)), 0, 8);
if (!$headName) {
$firstNames = ['Ahmad','Budi','Siti','Aulia','Rizal','Dewi','Agus','Fitri','Hendra','Yulia'];
$lastNames = ['Santoso','Wijaya','Pratama','Hidayat','Saputra','Nur','Ibrahim','Setiawan','Rahman','Putri'];
$headName = $firstNames[array_rand($firstNames)] . ' ' . $lastNames[array_rand($lastNames)];
}
if (!$economicStatus) {
$statuses = ['sangat_miskin','miskin','rentan','cukup','baik'];
$economicStatus = $statuses[array_rand($statuses)];
}
$insertStmt = $db->prepare('INSERT INTO need_points (name, latitude, longitude, masjid_id, household_name, head_name, economic_status) VALUES (?, ?, ?, ?, ?, ?, ?)');
$insertStmt->execute([$name, $latitude, $longitude, (int)$nearestMasjid['id'], $householdName, $headName, $economicStatus]);
$pointId = (int)$db->lastInsertId();
$point = getNeedPointWithMasjid($db, $pointId);
$point['distance_meters'] = $nearestMasjid['distance_meters'];
echo json_encode([
'success' => true,
'point' => $point
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+56
View File
@@ -0,0 +1,56 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0); // Don't display errors in output
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['name']) || !isset($data['latitude']) || !isset($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: name, latitude, or longitude']);
exit;
}
// Validate coordinates are numeric
if (!is_numeric($data['latitude']) || !is_numeric($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Latitude and longitude must be numeric values']);
exit;
}
$db = getDB();
$stmt = $db->prepare('INSERT INTO markers (name, nomor_spbu, latitude, longitude, open_24_hours) VALUES (?, ?, ?, ?, ?)');
$open_24_hours = isset($data['open_24_hours']) ? (int)$data['open_24_hours'] : 0;
$nomor_spbu = isset($data['nomor_spbu']) ? $data['nomor_spbu'] : null;
$stmt->execute([
$data['name'],
$nomor_spbu,
(float)$data['latitude'],
(float)$data['longitude'],
$open_24_hours
]);
echo json_encode([
'success' => true,
'id' => $db->lastInsertId(),
'name' => $data['name'],
'nomor_spbu' => $nomor_spbu,
'latitude' => (float)$data['latitude'],
'longitude' => (float)$data['longitude'],
'open_24_hours' => $open_24_hours
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+102
View File
@@ -0,0 +1,102 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['name']) || !isset($data['certificate_type']) || !isset($data['coordinates']) || !isset($data['area_sqm'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: name, certificate_type, coordinates, or area_sqm']);
exit;
}
// Validate certificate_type
$validTypes = ['SHM', 'HGB', 'HGU', 'HP'];
if (!in_array($data['certificate_type'], $validTypes)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid certificate_type. Must be: SHM, HGB, HGU, or HP']);
exit;
}
// Validate coordinates array (minimum 3 for polygon)
if (!is_array($data['coordinates']) || count($data['coordinates']) < 3) {
http_response_code(400);
echo json_encode(['error' => 'Coordinates must be an array with at least 3 points for a polygon']);
exit;
}
// Validate each coordinate
foreach ($data['coordinates'] as $coord) {
if (!is_array($coord) || count($coord) !== 2) {
http_response_code(400);
echo json_encode(['error' => 'Each coordinate must be [latitude, longitude]']);
exit;
}
$lat = floatval($coord[0]);
$lng = floatval($coord[1]);
if (!is_numeric($coord[0]) || !is_numeric($coord[1])) {
http_response_code(400);
echo json_encode(['error' => 'Coordinates must be numeric values']);
exit;
}
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinate range. Latitude: -90 to 90, Longitude: -180 to 180']);
exit;
}
}
// Validate name is not empty
if (empty(trim($data['name']))) {
http_response_code(400);
echo json_encode(['error' => 'Parcel name cannot be empty']);
exit;
}
// Validate area_sqm is positive
if (!is_numeric($data['area_sqm']) || $data['area_sqm'] <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Area must be a positive number']);
exit;
}
$db = getDB();
// Store coordinates as JSON
$coordinatesJson = json_encode($data['coordinates']);
$stmt = $db->prepare('INSERT INTO land_parcels (name, certificate_type, coordinates, area_sqm) VALUES (?, ?, ?, ?)');
$stmt->execute([
$data['name'],
$data['certificate_type'],
$coordinatesJson,
floatval($data['area_sqm'])
]);
echo json_encode([
'success' => true,
'id' => $db->lastInsertId(),
'name' => $data['name'],
'certificate_type' => $data['certificate_type'],
'coordinates' => $data['coordinates'],
'area_sqm' => floatval($data['area_sqm']),
'created_at' => date('Y-m-d H:i:s')
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+138
View File
@@ -0,0 +1,138 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!is_array($data)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON body']);
exit;
}
if (!isset($data['household_id']) || !is_numeric($data['household_id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid household_id']);
exit;
}
if (!isset($data['verifier_name']) || trim((string)$data['verifier_name']) === '') {
http_response_code(400);
echo json_encode(['error' => 'Missing required field: verifier_name']);
exit;
}
$error = null;
$verificationStatus = normalizeVerificationStatus($data['verification_status'] ?? null, $error, false);
if ($verificationStatus === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$evidencePhotoCount = normalizeEvidencePhotoCount($data['evidence_photo_count'] ?? null, $error);
if ($evidencePhotoCount === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$dataConfidenceScore = normalizeDataConfidenceScore($data['data_confidence_score'] ?? null, $error);
if ($dataConfidenceScore === null && $error !== null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$householdId = (int)$data['household_id'];
$verifierName = trim((string)$data['verifier_name']);
$verifierRole = isset($data['verifier_role']) ? trim((string)$data['verifier_role']) : null;
if ($verifierRole === '') {
$verifierRole = null;
}
$fieldNote = isset($data['field_note']) ? trim((string)$data['field_note']) : null;
if ($fieldNote === '') {
$fieldNote = null;
}
$verifiedAt = isset($data['verified_at']) && trim((string)$data['verified_at']) !== ''
? trim((string)$data['verified_at'])
: date('Y-m-d H:i:s');
$verifiedDateTime = DateTime::createFromFormat('Y-m-d H:i:s', $verifiedAt);
if (!$verifiedDateTime || $verifiedDateTime->format('Y-m-d H:i:s') !== $verifiedAt) {
http_response_code(400);
echo json_encode(['error' => 'verified_at must be in YYYY-MM-DD HH:MM:SS format']);
exit;
}
$db = getDB();
if (!householdExists($db, $householdId)) {
http_response_code(404);
echo json_encode(['error' => 'Household not found']);
exit;
}
$insertStmt = $db->prepare('
INSERT INTO verification_logs (
household_id,
verification_status,
verifier_name,
verifier_role,
field_note,
evidence_photo_count,
data_confidence_score,
verified_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
');
$insertStmt->execute([
$householdId,
$verificationStatus,
$verifierName,
$verifierRole,
$fieldNote,
$evidencePhotoCount,
$dataConfidenceScore,
$verifiedAt
]);
$updateHouseholdStmt = $db->prepare('UPDATE households SET verification_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?');
$updateHouseholdStmt->execute([$verificationStatus, $householdId]);
$id = (int)$db->lastInsertId();
$resultStmt = $db->prepare('
SELECT
vl.*,
h.household_code,
h.head_name
FROM verification_logs vl
JOIN households h ON h.id = vl.household_id
WHERE vl.id = ?
');
$resultStmt->execute([$id]);
$verificationLog = $resultStmt->fetch(PDO::FETCH_ASSOC);
$household = getHouseholdById($db, $householdId);
echo json_encode([
'success' => true,
'verification_log' => $verificationLog,
'household' => $household
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+99
View File
@@ -0,0 +1,99 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
try {
$db = getDB();
// Get filter parameters
$search = isset($_GET['search']) ? '%' . $_GET['search'] . '%' : '';
$from_date = isset($_GET['from_date']) ? $_GET['from_date'] : '';
$to_date = isset($_GET['to_date']) ? $_GET['to_date'] : '';
$certificate_type = isset($_GET['certificate_type']) ? $_GET['certificate_type'] : '';
// Build dynamic WHERE clause
$where_clauses = [];
$params = [];
if ($search !== '%' && $search !== '%') {
$where_clauses[] = '(name LIKE ?)';
$params[] = $search;
}
if ($from_date) {
$where_clauses[] = '(DATE(created_at) >= ?)';
$params[] = $from_date;
}
if ($to_date) {
$where_clauses[] = '(DATE(created_at) <= ?)';
$params[] = $to_date;
}
if ($certificate_type) {
$where_clauses[] = '(certificate_type = ?)';
$params[] = $certificate_type;
}
// Build SQL query
$sql = 'SELECT id, name, certificate_type, coordinates, area_sqm, created_at, updated_at FROM land_parcels';
if (count($where_clauses) > 0) {
$sql .= ' WHERE ' . implode(' AND ', $where_clauses);
}
$sql .= ' ORDER BY created_at DESC';
// Execute query
$stmt = $db->prepare($sql);
$stmt->execute($params);
$parcels = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decode coordinates JSON for each parcel
foreach ($parcels as &$parcel) {
$parcel['coordinates'] = json_decode($parcel['coordinates'], true);
}
// Count total for query
$count_sql = 'SELECT COUNT(*) as total FROM land_parcels' . (count($where_clauses) > 0 ? ' WHERE ' . implode(' AND ', $where_clauses) : '');
$count_stmt = $db->prepare($count_sql);
$count_stmt->execute($params);
$total = $count_stmt->fetch(PDO::FETCH_ASSOC)['total'];
// Prepare filters applied info
$filters_applied = [];
if ($search !== '%' && $search !== '%') {
$filters_applied['search'] = str_replace('%', '', $search);
}
if ($from_date || $to_date) {
$filters_applied['date_range'] = [];
if ($from_date) $filters_applied['date_range'][] = $from_date;
if ($to_date) $filters_applied['date_range'][] = $to_date;
}
if ($certificate_type) {
$filters_applied['certificate_type'] = $certificate_type;
}
// Return success response
http_response_code(200);
echo json_encode([
'success' => true,
'polygons' => $parcels,
'total' => $total,
'filters_applied' => $filters_applied
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>
+99
View File
@@ -0,0 +1,99 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
try {
$db = getDB();
// Get filter parameters
$search = isset($_GET['search']) ? '%' . $_GET['search'] . '%' : '';
$from_date = isset($_GET['from_date']) ? $_GET['from_date'] : '';
$to_date = isset($_GET['to_date']) ? $_GET['to_date'] : '';
$condition_status = isset($_GET['condition_status']) ? $_GET['condition_status'] : '';
// Build dynamic WHERE clause
$where_clauses = [];
$params = [];
if ($search !== '%' && $search !== '%') {
$where_clauses[] = '(name LIKE ?)';
$params[] = $search;
}
if ($from_date) {
$where_clauses[] = '(DATE(created_at) >= ?)';
$params[] = $from_date;
}
if ($to_date) {
$where_clauses[] = '(DATE(created_at) <= ?)';
$params[] = $to_date;
}
if ($condition_status) {
$where_clauses[] = '(condition_status = ?)';
$params[] = $condition_status;
}
// Build SQL query
$sql = 'SELECT id, name, road_type, coordinates, length_meters, condition_status, created_at, updated_at FROM roads';
if (count($where_clauses) > 0) {
$sql .= ' WHERE ' . implode(' AND ', $where_clauses);
}
$sql .= ' ORDER BY created_at DESC';
// Execute query
$stmt = $db->prepare($sql);
$stmt->execute($params);
$roads = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decode coordinates JSON for each road
foreach ($roads as &$road) {
$road['coordinates'] = json_decode($road['coordinates'], true);
}
// Count total for unfiltered query
$count_sql = 'SELECT COUNT(*) as total FROM roads' . (count($where_clauses) > 0 ? ' WHERE ' . implode(' AND ', $where_clauses) : '');
$count_stmt = $db->prepare($count_sql);
$count_stmt->execute($params);
$total = $count_stmt->fetch(PDO::FETCH_ASSOC)['total'];
// Prepare filters applied info
$filters_applied = [];
if ($search !== '%' && $search !== '%') {
$filters_applied['search'] = str_replace('%', '', $search);
}
if ($from_date || $to_date) {
$filters_applied['date_range'] = [];
if ($from_date) $filters_applied['date_range'][] = $from_date;
if ($to_date) $filters_applied['date_range'][] = $to_date;
}
if ($condition_status) {
$filters_applied['condition_status'] = $condition_status;
}
// Return success response
http_response_code(200);
echo json_encode([
'success' => true,
'lines' => $roads,
'total' => $total,
'filters_applied' => $filters_applied
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>
+136
View File
@@ -0,0 +1,136 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
try {
$db = getDB();
// Check if force parameter is set
$force = isset($_POST['force']) || isset($_GET['force']);
// Pontianak administrative data (dummy but more realistic non-rectangular polygons)
// GeoJSON coordinate order is [longitude, latitude].
$pontianak_data = [
[
'name' => 'Pontianak',
'area_type' => 'city',
'population' => 690000,
'geometry' => '{"type":"Polygon","coordinates":[[[109.320000,-0.005000],[109.330000,-0.004000],[109.340000,-0.005500],[109.345000,-0.008000],[109.348000,-0.012000],[109.350000,-0.016000],[109.348000,-0.020000],[109.340000,-0.023000],[109.330000,-0.024000],[109.320000,-0.023500],[109.310000,-0.022000],[109.305000,-0.019000],[109.303000,-0.016000],[109.302000,-0.012000],[109.305000,-0.008000],[109.310000,-0.006000],[109.318000,-0.004500],[109.325000,-0.003500],[109.329000,-0.004000],[109.320000,-0.005000]]]}'
],
[
'name' => 'Pontianak Barat',
'area_type' => 'kecamatan',
'population' => 112000,
'geometry' => '{"type":"Polygon","coordinates":[[[109.305000,-0.010000],[109.315000,-0.009500],[109.318000,-0.013000],[109.315000,-0.015000],[109.305000,-0.016000],[109.302000,-0.013000],[109.305000,-0.010000]]]}'
],
[
'name' => 'Pontianak Utara',
'area_type' => 'kecamatan',
'population' => 126000,
'geometry' => '{"type":"Polygon","coordinates":[[[109.318000,-0.005000],[109.327000,-0.004500],[109.330000,-0.007000],[109.328000,-0.009500],[109.320000,-0.010000],[109.315000,-0.007500],[109.318000,-0.005000]]]}'
],
[
'name' => 'Pontianak Kota',
'area_type' => 'kecamatan',
'population' => 148000,
'geometry' => '{"type":"Polygon","coordinates":[[[109.320000,-0.010000],[109.327000,-0.009000],[109.330000,-0.011000],[109.328000,-0.015000],[109.320000,-0.015500],[109.315000,-0.013000],[109.320000,-0.010000]]]}'
],
[
'name' => 'Pontianak Timur',
'area_type' => 'kecamatan',
'population' => 94000,
'geometry' => '{"type":"Polygon","coordinates":[[[109.328000,-0.009500],[109.340000,-0.010000],[109.345000,-0.012500],[109.338000,-0.016000],[109.328000,-0.015000],[109.328000,-0.009500]]]}'
],
[
'name' => 'Pontianak Selatan',
'area_type' => 'kecamatan',
'population' => 132000,
'geometry' => '{"type":"Polygon","coordinates":[[[109.315000,-0.015000],[109.328000,-0.014500],[109.330000,-0.018000],[109.318000,-0.021000],[109.308000,-0.020000],[109.315000,-0.015000]]]}'
],
[
'name' => 'Pontianak Tenggara',
'area_type' => 'kecamatan',
'population' => 91000,
'geometry' => '{"type":"Polygon","coordinates":[[[109.328000,-0.015000],[109.338000,-0.016000],[109.340000,-0.019000],[109.330000,-0.021000],[109.320000,-0.020000],[109.328000,-0.015000]]]}'
]
];
// Check if data already exists
$stmt = $db->prepare('SELECT COUNT(*) as count FROM pontianak_areas');
$stmt->execute();
$count = $stmt->fetch(PDO::FETCH_ASSOC)['count'];
if ($count > 0 && !$force) {
http_response_code(200);
echo json_encode([
'success' => true,
'message' => 'Pontianak data already exists. Use force=true to reseed.'
]);
exit;
}
// Delete existing data if force is true
if ($force && $count > 0) {
$db->exec('DELETE FROM area_statistics');
$db->exec('DELETE FROM pontianak_areas');
}
// Insert Pontianak areas
$stmt = $db->prepare('
INSERT OR IGNORE INTO pontianak_areas (name, area_type, geometry, population)
VALUES (?, ?, ?, ?)
');
$today = date('Y-m-d');
$stats_stmt = $db->prepare('
INSERT OR IGNORE INTO area_statistics (pontianak_area_id, statistic_type, value, data_date)
VALUES (?, ?, ?, ?)
');
$inserted = 0;
foreach ($pontianak_data as $data) {
$stmt->execute([
$data['name'],
$data['area_type'],
$data['geometry'],
$data['population']
]);
$inserted++;
}
// Insert initial area statistics
$areas_stmt = $db->prepare('SELECT id, population FROM pontianak_areas');
$areas_stmt->execute();
$areas = $areas_stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($areas as $area) {
// Insert population density statistic
$stats_stmt->execute([
$area['id'],
'population_density',
(float)$area['population'],
$today
]);
}
http_response_code(200);
echo json_encode([
'success' => true,
'message' => 'Pontianak data seeded successfully',
'areas_inserted' => $inserted
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>
+256
View File
@@ -0,0 +1,256 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
function randomFloat($min, $max) {
return $min + (mt_rand() / mt_getrandmax()) * ($max - $min);
}
function buildGridLocation($bbox, $index, $total) {
$rows = max(1, (int)ceil(sqrt($total)));
$cols = max(1, (int)ceil($total / $rows));
$row = intdiv($index, $cols);
$col = $index % $cols;
$latStep = ($bbox['max_lat'] - $bbox['min_lat']) / $rows;
$lngStep = ($bbox['max_lng'] - $bbox['min_lng']) / $cols;
$baseLat = $bbox['min_lat'] + ($row + 0.5) * $latStep;
$baseLng = $bbox['min_lng'] + ($col + 0.5) * $lngStep;
$latMin = max($bbox['min_lat'], $baseLat - $latStep * 0.35);
$latMax = min($bbox['max_lat'], $baseLat + $latStep * 0.35);
$lngMin = max($bbox['min_lng'], $baseLng - $lngStep * 0.35);
$lngMax = min($bbox['max_lng'], $baseLng + $lngStep * 0.35);
return [
'latitude' => randomFloat($latMin, $latMax),
'longitude' => randomFloat($lngMin, $lngMax)
];
}
function distanceSquared($lat1, $lng1, $lat2, $lng2) {
$dLat = $lat1 - $lat2;
$dLng = $lng1 - $lng2;
return ($dLat * $dLat) + ($dLng * $dLng);
}
function findNearestMasjidId($latitude, $longitude, $masjids) {
$nearestId = null;
$nearestDistance = null;
foreach ($masjids as $masjid) {
$distance = distanceSquared($latitude, $longitude, $masjid['latitude'], $masjid['longitude']);
if ($nearestDistance === null || $distance < $nearestDistance) {
$nearestDistance = $distance;
$nearestId = $masjid['id'];
}
}
return $nearestId;
}
try {
$db = getDB();
$force = isset($_POST['force']) || isset($_GET['force']);
// Load GeoJSON land polygons to avoid placing points at sea
$geojsonPath = __DIR__ . '/../export.geojson';
$landPolygons = [];
if (file_exists($geojsonPath)) {
$gj = json_decode(file_get_contents($geojsonPath), true);
if ($gj && isset($gj['features']) && is_array($gj['features'])) {
foreach ($gj['features'] as $feature) {
if (!isset($feature['geometry'])) continue;
$geom = $feature['geometry'];
if ($geom['type'] === 'Polygon') {
$landPolygons[] = $geom['coordinates'];
} elseif ($geom['type'] === 'MultiPolygon') {
foreach ($geom['coordinates'] as $poly) $landPolygons[] = $poly;
}
}
}
}
function pointInRing($lat, $lng, $ring) {
$inside = false;
$j = count($ring) - 1;
for ($i = 0; $i < count($ring); $i++) {
$xi = $ring[$i][0]; $yi = $ring[$i][1];
$xj = $ring[$j][0]; $yj = $ring[$j][1];
$intersect = (($yi > $lat) != ($yj > $lat)) && ($lng < ($xj - $xi) * ($lat - $yi) / ($yj - $yi + 0.0) + $xi);
if ($intersect) $inside = !$inside;
$j = $i;
}
return $inside;
}
function isPointOnLand($lat, $lng, $landPolygons) {
if (empty($landPolygons)) return true; // if no polygons available, default to true
foreach ($landPolygons as $poly) {
// poly is array of rings; test outer ring first
$outer = $poly[0];
if (pointInRing($lat, $lng, $outer)) {
// ensure not in hole
$isInHole = false;
for ($r = 1; $r < count($poly); $r++) {
if (pointInRing($lat, $lng, $poly[$r])) {
$isInHole = true;
break;
}
}
if (!$isInHole) return true;
}
}
return false;
}
$regional_data = [
['region' => 'Jakarta', 'province' => 'DKI Jakarta', 'bbox' => ['min_lat' => -6.40, 'max_lat' => -5.90, 'min_lng' => 106.60, 'max_lng' => 106.95], 'masjids' => 15, 'need_points' => 2435],
['region' => 'Bandung', 'province' => 'Jawa Barat', 'bbox' => ['min_lat' => -7.50, 'max_lat' => -5.90, 'min_lng' => 106.00, 'max_lng' => 108.10], 'masjids' => 10, 'need_points' => 1500],
['region' => 'Bekasi', 'province' => 'Jawa Barat', 'bbox' => ['min_lat' => -7.20, 'max_lat' => -6.00, 'min_lng' => 106.50, 'max_lng' => 108.00], 'masjids' => 6, 'need_points' => 800],
['region' => 'Semarang', 'province' => 'Jawa Tengah', 'bbox' => ['min_lat' => -8.10, 'max_lat' => -5.90, 'min_lng' => 108.30, 'max_lng' => 111.20], 'masjids' => 9, 'need_points' => 1400],
['region' => 'Yogyakarta', 'province' => 'DI Yogyakarta', 'bbox' => ['min_lat' => -8.20, 'max_lat' => -7.30, 'min_lng' => 109.90, 'max_lng' => 110.90], 'masjids' => 7, 'need_points' => 900],
['region' => 'Surabaya', 'province' => 'Jawa Timur', 'bbox' => ['min_lat' => -8.80, 'max_lat' => -6.80, 'min_lng' => 111.00, 'max_lng' => 114.50], 'masjids' => 12, 'need_points' => 1800],
['region' => 'Malang', 'province' => 'Jawa Timur', 'bbox' => ['min_lat' => -8.60, 'max_lat' => -7.30, 'min_lng' => 110.80, 'max_lng' => 113.20], 'masjids' => 5, 'need_points' => 700],
['region' => 'Medan', 'province' => 'Sumatera Utara', 'bbox' => ['min_lat' => 1.00, 'max_lat' => 4.50, 'min_lng' => 98.00, 'max_lng' => 100.50], 'masjids' => 11, 'need_points' => 1600],
['region' => 'Palembang', 'province' => 'Sumatera Selatan', 'bbox' => ['min_lat' => -4.20, 'max_lat' => -2.00, 'min_lng' => 103.00, 'max_lng' => 106.10], 'masjids' => 8, 'need_points' => 1100],
['region' => 'Pekanbaru', 'province' => 'Riau', 'bbox' => ['min_lat' => -0.60, 'max_lat' => 2.20, 'min_lng' => 100.00, 'max_lng' => 102.90], 'masjids' => 6, 'need_points' => 850],
['region' => 'Jambi', 'province' => 'Jambi', 'bbox' => ['min_lat' => -2.30, 'max_lat' => 0.50, 'min_lng' => 101.00, 'max_lng' => 104.20], 'masjids' => 4, 'need_points' => 600],
['region' => 'Padang', 'province' => 'Sumatera Barat', 'bbox' => ['min_lat' => -2.90, 'max_lat' => 1.00, 'min_lng' => 99.40, 'max_lng' => 101.80], 'masjids' => 5, 'need_points' => 700],
['region' => 'Makassar', 'province' => 'Sulawesi Selatan', 'bbox' => ['min_lat' => -6.50, 'max_lat' => -2.50, 'min_lng' => 118.00, 'max_lng' => 121.80], 'masjids' => 9, 'need_points' => 1300],
['region' => 'Manado', 'province' => 'Sulawesi Utara', 'bbox' => ['min_lat' => 0.80, 'max_lat' => 4.00, 'min_lng' => 123.00, 'max_lng' => 125.80], 'masjids' => 4, 'need_points' => 550],
['region' => 'Kendari', 'province' => 'Sulawesi Tenggara', 'bbox' => ['min_lat' => -6.50, 'max_lat' => -2.00, 'min_lng' => 120.00, 'max_lng' => 124.50], 'masjids' => 3, 'need_points' => 400],
['region' => 'Palu', 'province' => 'Sulawesi Tengah', 'bbox' => ['min_lat' => -3.00, 'max_lat' => 2.00, 'min_lng' => 118.00, 'max_lng' => 123.50], 'masjids' => 4, 'need_points' => 500],
['region' => 'Banjarmasin', 'province' => 'Kalimantan Selatan', 'bbox' => ['min_lat' => -4.20, 'max_lat' => -1.00, 'min_lng' => 114.00, 'max_lng' => 116.80], 'masjids' => 7, 'need_points' => 1000],
['region' => 'Pontianak', 'province' => 'Kalimantan Barat', 'bbox' => ['min_lat' => -3.50, 'max_lat' => 3.00, 'min_lng' => 108.00, 'max_lng' => 112.80], 'masjids' => 5, 'need_points' => 700],
['region' => 'Samarinda', 'province' => 'Kalimantan Timur', 'bbox' => ['min_lat' => -2.80, 'max_lat' => 2.00, 'min_lng' => 115.00, 'max_lng' => 119.80], 'masjids' => 6, 'need_points' => 850],
['region' => 'Palangkaraya', 'province' => 'Kalimantan Tengah', 'bbox' => ['min_lat' => -3.80, 'max_lat' => 1.20, 'min_lng' => 112.00, 'max_lng' => 116.50], 'masjids' => 3, 'need_points' => 450],
['region' => 'Jayapura', 'province' => 'Papua', 'bbox' => ['min_lat' => -9.50, 'max_lat' => -1.00, 'min_lng' => 130.00, 'max_lng' => 141.50], 'masjids' => 4, 'need_points' => 700],
['region' => 'Mataram', 'province' => 'Nusa Tenggara Barat', 'bbox' => ['min_lat' => -9.20, 'max_lat' => -7.00, 'min_lng' => 115.20, 'max_lng' => 119.00], 'masjids' => 3, 'need_points' => 450],
['region' => 'Kupang', 'province' => 'Nusa Tenggara Timur', 'bbox' => ['min_lat' => -11.20, 'max_lat' => -8.00, 'min_lng' => 118.50, 'max_lng' => 125.50], 'masjids' => 2, 'need_points' => 300],
['region' => 'Ambon', 'province' => 'Maluku', 'bbox' => ['min_lat' => -8.50, 'max_lat' => -1.00, 'min_lng' => 125.00, 'max_lng' => 134.50], 'masjids' => 2, 'need_points' => 250],
['region' => 'Denpasar', 'province' => 'Bali', 'bbox' => ['min_lat' => -9.20, 'max_lat' => -8.00, 'min_lng' => 114.50, 'max_lng' => 115.80], 'masjids' => 1, 'need_points' => 100]
];
$stmt = $db->prepare('SELECT COUNT(*) as count FROM masjids');
$stmt->execute();
$masjidCount = (int)$stmt->fetch(PDO::FETCH_ASSOC)['count'];
if ($masjidCount > 20 && !$force) {
http_response_code(200);
echo json_encode([
'success' => true,
'message' => 'Regional poverty data already exists. Use force=true to reseed.',
'current_masjid_count' => $masjidCount
]);
exit;
}
if ($force && $masjidCount > 0) {
$db->exec('DELETE FROM need_points');
$db->exec('DELETE FROM masjids');
}
$db->beginTransaction();
$masjidStmt = $db->prepare('INSERT INTO masjids (name, latitude, longitude, radius_meters) VALUES (?, ?, ?, ?)');
$needPointStmt = $db->prepare('INSERT INTO need_points (name, latitude, longitude, masjid_id, household_name, head_name, economic_status) VALUES (?, ?, ?, ?, ?, ?, ?)');
$insertedMasjids = 0;
$insertedNeedPoints = 0;
foreach ($regional_data as $region) {
$provinceMasjids = [];
for ($i = 0; $i < $region['masjids']; $i++) {
// ensure masjid sits on land (try a few attempts)
$attempt = 0;
do {
$location = buildGridLocation($region['bbox'], $i, $region['masjids']);
$attempt++;
} while ($attempt < 8 && !isPointOnLand($location['latitude'], $location['longitude'], $landPolygons));
$latSpan = $region['bbox']['max_lat'] - $region['bbox']['min_lat'];
$lngSpan = $region['bbox']['max_lng'] - $region['bbox']['min_lng'];
$coverageEstimate = (int)round((($latSpan + $lngSpan) * 111000) / max(1, $region['masjids']) * 0.55);
$radiusMeters = max(20000, min(180000, $coverageEstimate + rand(3000, 12000)));
$masjidStmt->execute([
'Masjid ' . $region['region'] . ' - ' . ($i + 1),
$location['latitude'],
$location['longitude'],
$radiusMeters
]);
$provinceMasjids[] = [
'id' => (int)$db->lastInsertId(),
'latitude' => $location['latitude'],
'longitude' => $location['longitude']
];
$insertedMasjids++;
}
for ($i = 0; $i < $region['need_points']; $i++) {
// ensure need point lands on land; allow several tries
$attempt = 0;
do {
$pointLocation = buildGridLocation($region['bbox'], $i, $region['need_points']);
$attempt++;
} while ($attempt < 12 && !isPointOnLand($pointLocation['latitude'], $pointLocation['longitude'], $landPolygons));
$nearestMasjidId = findNearestMasjidId($pointLocation['latitude'], $pointLocation['longitude'], $provinceMasjids);
// generate household and head names and economic status
$hhName = 'Keluarga ' . $region['region'] . ' - ' . ($i + 1);
$firstNames = ['Ahmad','Budi','Siti','Aulia','Rizal','Dewi','Agus','Fitri','Hendra','Yulia'];
$lastNames = ['Santoso','Wijaya','Pratama','Hidayat','Saputra','Nur','Ibrahim','Setiawan','Rahman','Putri'];
$headName = $firstNames[array_rand($firstNames)] . ' ' . $lastNames[array_rand($lastNames)];
$statuses = ['sangat_miskin','miskin','rentan','cukup','baik'];
$econ = $statuses[array_rand($statuses)];
$needPointStmt->execute([
'Keluarga Miskin ' . $region['region'] . ' - ' . ($i + 1),
$pointLocation['latitude'],
$pointLocation['longitude'],
$nearestMasjidId,
$hhName,
$headName,
$econ
]);
$insertedNeedPoints++;
}
}
$db->commit();
http_response_code(200);
echo json_encode([
'success' => true,
'message' => 'Regional poverty assistance data seeded successfully',
'masjids_inserted' => $insertedMasjids,
'need_points_inserted' => $insertedNeedPoints,
'regions_covered' => count($regional_data),
'total_need_points_capacity' => 21935
]);
} catch (PDOException $e) {
if (isset($db) && $db->inTransaction()) {
$db->rollBack();
}
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]);
} catch (Exception $e) {
if (isset($db) && $db->inTransaction()) {
$db->rollBack();
}
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>
+24
View File
@@ -0,0 +1,24 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
try {
$db = getDB();
$syncResult = syncNeedPointsToHouseholds($db);
$computedScores = ensureHouseholdScoresForAllHouseholds($db);
echo json_encode([
'success' => true,
'sync_result' => $syncResult,
'computed_missing_scores' => $computedScores
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
+48
View File
@@ -0,0 +1,48 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required field: id']);
exit;
}
$id = (int)$data['id'];
$db = getDB();
$existing = getNeedPointWithMasjid($db, $id);
if (!$existing) {
http_response_code(404);
echo json_encode(['error' => 'Need point not found']);
exit;
}
$current = (int)($existing['is_verified'] ?? 0);
$next = $current === 1 ? 0 : 1;
$stmt = $db->prepare('UPDATE need_points SET is_verified = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?');
$stmt->execute([$next, $id]);
$updated = getNeedPointWithMasjid($db, $id);
echo json_encode([
'success' => true,
'point' => $updated
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
?>
+269
View File
@@ -0,0 +1,269 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!is_array($data) || !isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required field: id']);
exit;
}
$id = (int)$data['id'];
$db = getDB();
$existingStmt = $db->prepare('SELECT * FROM households WHERE id = ?');
$existingStmt->execute([$id]);
$existing = $existingStmt->fetch(PDO::FETCH_ASSOC);
if (!$existing) {
http_response_code(404);
echo json_encode(['error' => 'Household not found']);
exit;
}
$updates = [];
$params = [];
$error = null;
if (array_key_exists('household_code', $data)) {
$householdCode = trim((string)$data['household_code']);
if ($householdCode === '') {
http_response_code(400);
echo json_encode(['error' => 'household_code cannot be empty']);
exit;
}
$updates[] = 'household_code = ?';
$params[] = $householdCode;
}
if (array_key_exists('head_name', $data)) {
$headName = trim((string)$data['head_name']);
if ($headName === '') {
http_response_code(400);
echo json_encode(['error' => 'head_name cannot be empty']);
exit;
}
$updates[] = 'head_name = ?';
$params[] = $headName;
}
if (array_key_exists('nik', $data) || array_key_exists('nik_hash', $data)) {
if (array_key_exists('nik', $data)) {
$nikHash = normalizeNikHashFromInput($data['nik']);
} else {
$nikHash = trim((string)$data['nik_hash']);
if ($nikHash === '') {
$nikHash = null;
}
}
$updates[] = 'nik_hash = ?';
$params[] = $nikHash;
}
$hasLatitude = array_key_exists('latitude', $data);
$hasLongitude = array_key_exists('longitude', $data);
if ($hasLatitude || $hasLongitude) {
$latitude = $hasLatitude ? $data['latitude'] : $existing['latitude'];
$longitude = $hasLongitude ? $data['longitude'] : $existing['longitude'];
if (!validateCoordinates($latitude, $longitude, $error)) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$updates[] = 'latitude = ?';
$params[] = (float)$latitude;
$updates[] = 'longitude = ?';
$params[] = (float)$longitude;
}
if (array_key_exists('need_point_id', $data)) {
if ($data['need_point_id'] === null || $data['need_point_id'] === '') {
$needPointId = null;
} else {
if (!is_numeric($data['need_point_id'])) {
http_response_code(400);
echo json_encode(['error' => 'need_point_id must be numeric']);
exit;
}
$needPointId = (int)$data['need_point_id'];
$npStmt = $db->prepare('SELECT id FROM need_points WHERE id = ?');
$npStmt->execute([$needPointId]);
if (!$npStmt->fetch(PDO::FETCH_ASSOC)) {
http_response_code(400);
echo json_encode(['error' => 'need_point_id is not found']);
exit;
}
}
$updates[] = 'need_point_id = ?';
$params[] = $needPointId;
}
if (array_key_exists('masjid_id', $data)) {
if ($data['masjid_id'] === null || $data['masjid_id'] === '') {
$masjidId = null;
} else {
if (!is_numeric($data['masjid_id'])) {
http_response_code(400);
echo json_encode(['error' => 'masjid_id must be numeric']);
exit;
}
$masjidId = (int)$data['masjid_id'];
$masjidStmt = $db->prepare('SELECT id FROM masjids WHERE id = ?');
$masjidStmt->execute([$masjidId]);
if (!$masjidStmt->fetch(PDO::FETCH_ASSOC)) {
http_response_code(400);
echo json_encode(['error' => 'masjid_id is not found']);
exit;
}
}
$updates[] = 'masjid_id = ?';
$params[] = $masjidId;
}
if (array_key_exists('pontianak_area_id', $data)) {
if ($data['pontianak_area_id'] === null || $data['pontianak_area_id'] === '') {
$areaId = null;
} else {
if (!is_numeric($data['pontianak_area_id'])) {
http_response_code(400);
echo json_encode(['error' => 'pontianak_area_id must be numeric']);
exit;
}
$areaId = (int)$data['pontianak_area_id'];
$areaStmt = $db->prepare('SELECT id FROM pontianak_areas WHERE id = ?');
$areaStmt->execute([$areaId]);
if (!$areaStmt->fetch(PDO::FETCH_ASSOC)) {
http_response_code(400);
echo json_encode(['error' => 'pontianak_area_id is not found']);
exit;
}
}
$updates[] = 'pontianak_area_id = ?';
$params[] = $areaId;
}
if (array_key_exists('monthly_income', $data)) {
$monthlyIncome = normalizeMonthlyIncome($data['monthly_income'], $error);
if ($monthlyIncome === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$updates[] = 'monthly_income = ?';
$params[] = $monthlyIncome;
}
if (array_key_exists('dependents_count', $data)) {
$dependentsCount = normalizeDependentsCount($data['dependents_count'], $error);
if ($dependentsCount === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$updates[] = 'dependents_count = ?';
$params[] = $dependentsCount;
}
if (array_key_exists('housing_condition_score', $data)) {
$housingConditionScore = normalizeHousingConditionScore($data['housing_condition_score'], $error);
if ($housingConditionScore === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$updates[] = 'housing_condition_score = ?';
$params[] = $housingConditionScore;
}
if (array_key_exists('employment_status', $data)) {
$employmentStatus = normalizeEmploymentStatus($data['employment_status'], $error);
if ($employmentStatus === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$updates[] = 'employment_status = ?';
$params[] = $employmentStatus;
}
if (array_key_exists('verification_status', $data)) {
$verificationStatus = normalizeVerificationStatus($data['verification_status'], $error, true);
if ($verificationStatus === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$updates[] = 'verification_status = ?';
$params[] = $verificationStatus;
}
if (array_key_exists('vulnerability_notes', $data)) {
$vulnerabilityNotes = trim((string)$data['vulnerability_notes']);
if ($vulnerabilityNotes === '') {
$vulnerabilityNotes = null;
}
$updates[] = 'vulnerability_notes = ?';
$params[] = $vulnerabilityNotes;
}
if (empty($updates)) {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
exit;
}
$updates[] = 'updated_at = CURRENT_TIMESTAMP';
$params[] = $id;
$sql = 'UPDATE households SET ' . implode(', ', $updates) . ' WHERE id = ?';
$updateStmt = $db->prepare($sql);
$updateStmt->execute($params);
$household = getHouseholdById($db, $id);
echo json_encode([
'success' => true,
'household' => $household
]);
} catch (PDOException $e) {
if ($e->getCode() === '23000') {
http_response_code(409);
echo json_encode(['error' => 'Duplicate household_code or need_point_id mapping']);
exit;
}
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+143
View File
@@ -0,0 +1,143 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required field: id']);
exit;
}
$id = intval($data['id']);
$db = getDB();
// Determine what type of update this is
$hasCoordinates = isset($data['coordinates']) && isset($data['length_meters']);
$hasName = isset($data['name']);
$hasRoadType = isset($data['road_type']);
$hasConditionStatus = isset($data['condition_status']);
// Full edit (multiple fields including name or road_type)
if ($hasName || $hasRoadType || $hasCoordinates || $hasConditionStatus) {
$updates = [];
$params = [];
if ($hasName) {
if (empty($data['name'])) {
http_response_code(400);
echo json_encode(['error' => 'Name cannot be empty']);
exit;
}
$updates[] = 'name = ?';
$params[] = $data['name'];
}
if ($hasRoadType) {
$validTypes = ['Nasional', 'Provinsi', 'Kabupaten'];
if (!in_array($data['road_type'], $validTypes)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid road_type. Must be: Nasional, Provinsi, or Kabupaten']);
exit;
}
$updates[] = 'road_type = ?';
$params[] = $data['road_type'];
}
if ($hasConditionStatus) {
$validConditions = ['normal', 'minor_damage', 'major_damage'];
if (!in_array($data['condition_status'], $validConditions)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid condition_status. Must be: normal, minor_damage, or major_damage']);
exit;
}
$updates[] = 'condition_status = ?';
$params[] = $data['condition_status'];
}
if ($hasCoordinates) {
// Validate coordinates array
if (!is_array($data['coordinates']) || count($data['coordinates']) < 2) {
http_response_code(400);
echo json_encode(['error' => 'Coordinates must be an array with at least 2 points']);
exit;
}
// Validate each coordinate
foreach ($data['coordinates'] as $coord) {
if (!is_array($coord) || count($coord) !== 2 || !is_numeric($coord[0]) || !is_numeric($coord[1])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinate format']);
exit;
}
$lat = floatval($coord[0]);
$lng = floatval($coord[1]);
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinate range']);
exit;
}
}
// Validate length_meters is positive
if (!is_numeric($data['length_meters']) || $data['length_meters'] <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Length must be a positive number']);
exit;
}
$updates[] = 'coordinates = ?';
$updates[] = 'length_meters = ?';
$params[] = json_encode($data['coordinates']);
$params[] = floatval($data['length_meters']);
}
if (empty($updates)) {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
exit;
}
// Add updated_at and id to params
$updates[] = 'updated_at = CURRENT_TIMESTAMP';
$params[] = $id;
$sql = 'UPDATE roads SET ' . implode(', ', $updates) . ' WHERE id = ?';
$stmt = $db->prepare($sql);
$stmt->execute($params);
// Fetch updated record
$getStmt = $db->prepare('SELECT * FROM roads WHERE id = ?');
$getStmt->execute([$id]);
$road = $getStmt->fetch(PDO::FETCH_ASSOC);
if ($road) {
$road['coordinates'] = json_decode($road['coordinates'], true);
}
echo json_encode([
'success' => true,
'line' => $road
]);
} else {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+125
View File
@@ -0,0 +1,125 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required field: id']);
exit;
}
$id = (int)$data['id'];
$db = getDB();
$existingStmt = $db->prepare('SELECT id, name, latitude, longitude, radius_meters FROM masjids WHERE id = ?');
$existingStmt->execute([$id]);
$existing = $existingStmt->fetch(PDO::FETCH_ASSOC);
if (!$existing) {
http_response_code(404);
echo json_encode(['error' => 'Masjid not found']);
exit;
}
$updates = [];
$params = [];
if (isset($data['name'])) {
$name = trim($data['name']);
if ($name === '') {
http_response_code(400);
echo json_encode(['error' => 'Name cannot be empty']);
exit;
}
$updates[] = 'name = ?';
$params[] = $name;
}
$hasLatitude = array_key_exists('latitude', $data);
$hasLongitude = array_key_exists('longitude', $data);
if ($hasLatitude || $hasLongitude) {
$latitude = $hasLatitude ? $data['latitude'] : $existing['latitude'];
$longitude = $hasLongitude ? $data['longitude'] : $existing['longitude'];
if (!is_numeric($latitude) || !is_numeric($longitude)) {
http_response_code(400);
echo json_encode(['error' => 'Latitude and longitude must be numeric values']);
exit;
}
$latitude = (float)$latitude;
$longitude = (float)$longitude;
if ($latitude < -90 || $latitude > 90) {
http_response_code(400);
echo json_encode(['error' => 'Latitude must be between -90 and 90']);
exit;
}
if ($longitude < -180 || $longitude > 180) {
http_response_code(400);
echo json_encode(['error' => 'Longitude must be between -180 and 180']);
exit;
}
$updates[] = 'latitude = ?';
$updates[] = 'longitude = ?';
$params[] = $latitude;
$params[] = $longitude;
}
if (array_key_exists('radius_meters', $data)) {
$radiusError = null;
$radius = normalizeMasjidRadius($data['radius_meters'], $radiusError);
if ($radius === null) {
http_response_code(400);
echo json_encode(['error' => $radiusError]);
exit;
}
$updates[] = 'radius_meters = ?';
$params[] = $radius;
}
if (empty($updates)) {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
exit;
}
$updates[] = 'updated_at = CURRENT_TIMESTAMP';
$params[] = $id;
$updateSql = 'UPDATE masjids SET ' . implode(', ', $updates) . ' WHERE id = ?';
$updateStmt = $db->prepare($updateSql);
$updateStmt->execute($params);
$resultStmt = $db->prepare('SELECT id, name, latitude, longitude, radius_meters, created_at, updated_at FROM masjids WHERE id = ?');
$resultStmt->execute([$id]);
$masjid = $resultStmt->fetch(PDO::FETCH_ASSOC);
$cleanupResult = cleanupNeedPointsOutsideMasjidCoverage($db, $id);
echo json_encode([
'success' => true,
'masjid' => $masjid,
'deleted_need_points' => $cleanupResult['deleted_count'],
'deleted_need_point_ids' => $cleanupResult['deleted_ids']
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+102
View File
@@ -0,0 +1,102 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required field: id']);
exit;
}
$id = (int)$data['id'];
$db = getDB();
$existing = getNeedPointWithMasjid($db, $id);
if (!$existing) {
http_response_code(404);
echo json_encode(['error' => 'Point berkebutuhan not found']);
exit;
}
$newName = isset($data['name']) ? trim($data['name']) : $existing['name'];
if ($newName === '') {
http_response_code(400);
echo json_encode(['error' => 'Name cannot be empty']);
exit;
}
$newLatitude = array_key_exists('latitude', $data) ? $data['latitude'] : $existing['latitude'];
$newLongitude = array_key_exists('longitude', $data) ? $data['longitude'] : $existing['longitude'];
if (!is_numeric($newLatitude) || !is_numeric($newLongitude)) {
http_response_code(400);
echo json_encode(['error' => 'Latitude and longitude must be numeric values']);
exit;
}
$newLatitude = (float)$newLatitude;
$newLongitude = (float)$newLongitude;
if ($newLatitude < -90 || $newLatitude > 90) {
http_response_code(400);
echo json_encode(['error' => 'Latitude must be between -90 and 90']);
exit;
}
if ($newLongitude < -180 || $newLongitude > 180) {
http_response_code(400);
echo json_encode(['error' => 'Longitude must be between -180 and 180']);
exit;
}
$nearestMasjid = findNearestMasjidCoveringPoint($db, $newLatitude, $newLongitude);
if ($nearestMasjid === null) {
http_response_code(400);
echo json_encode(['error' => 'Point must remain inside at least one masjid radius']);
exit;
}
// allow updating household fields as well
$newHouseholdName = array_key_exists('household_name', $data) ? trim($data['household_name']) : $existing['household_name'];
$newHeadName = array_key_exists('head_name', $data) ? trim($data['head_name']) : $existing['head_name'];
$newEconomic = array_key_exists('economic_status', $data) ? trim($data['economic_status']) : $existing['economic_status'];
$updateStmt = $db->prepare('
UPDATE need_points
SET name = ?, latitude = ?, longitude = ?, masjid_id = ?, household_name = ?, head_name = ?, economic_status = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
');
$updateStmt->execute([
$newName,
$newLatitude,
$newLongitude,
(int)$nearestMasjid['id'],
$newHouseholdName,
$newHeadName,
$newEconomic,
$id
]);
$updated = getNeedPointWithMasjid($db, $id);
$updated['distance_meters'] = $nearestMasjid['distance_meters'];
echo json_encode([
'success' => true,
'point' => $updated
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+154
View File
@@ -0,0 +1,154 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required field: id']);
exit;
}
$id = (int)$data['id'];
$db = getDB();
// Determine what type of update this is
$hasCoordinates = isset($data['latitude']) && isset($data['longitude']);
$hasName = isset($data['name']);
$hasNomorSpbu = isset($data['nomor_spbu']);
$hasStatus = isset($data['open_24_hours']);
// Full edit (multiple fields including coordinates or name)
if ($hasName || ($hasCoordinates && ($hasName || $hasNomorSpbu || $hasStatus))) {
$updates = [];
$params = [];
if ($hasName) {
if (empty($data['name'])) {
http_response_code(400);
echo json_encode(['error' => 'Name cannot be empty']);
exit;
}
$updates[] = 'name = ?';
$params[] = $data['name'];
}
if ($hasNomorSpbu) {
$updates[] = 'nomor_spbu = ?';
$params[] = $data['nomor_spbu'] ?: null;
}
if ($hasCoordinates) {
if (!is_numeric($data['latitude']) || !is_numeric($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Latitude and longitude must be numeric values']);
exit;
}
$latitude = (float)$data['latitude'];
$longitude = (float)$data['longitude'];
if ($latitude < -90 || $latitude > 90 || $longitude < -180 || $longitude > 180) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinates']);
exit;
}
$updates[] = 'latitude = ?';
$updates[] = 'longitude = ?';
$params[] = $latitude;
$params[] = $longitude;
}
if ($hasStatus) {
$updates[] = 'open_24_hours = ?';
$params[] = (int)$data['open_24_hours'];
}
if (empty($updates)) {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
exit;
}
// Add id to params
$params[] = $id;
$sql = 'UPDATE markers SET ' . implode(', ', $updates) . ' WHERE id = ?';
$stmt = $db->prepare($sql);
$stmt->execute($params);
// Fetch updated record
$getStmt = $db->prepare('SELECT * FROM markers WHERE id = ?');
$getStmt->execute([$id]);
$marker = $getStmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'marker' => $marker
]);
}
// Coordinates-only update (drag)
else if ($hasCoordinates) {
if (!is_numeric($data['latitude']) || !is_numeric($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Latitude and longitude must be numeric values']);
exit;
}
$latitude = (float)$data['latitude'];
$longitude = (float)$data['longitude'];
if ($latitude < -90 || $latitude > 90) {
http_response_code(400);
echo json_encode(['error' => 'Latitude must be between -90 and 90']);
exit;
}
if ($longitude < -180 || $longitude > 180) {
http_response_code(400);
echo json_encode(['error' => 'Longitude must be between -180 and 180']);
exit;
}
$stmt = $db->prepare('UPDATE markers SET latitude = ?, longitude = ? WHERE id = ?');
$stmt->execute([$latitude, $longitude, $id]);
echo json_encode([
'success' => true,
'id' => $id,
'latitude' => $latitude,
'longitude' => $longitude
]);
}
// Status-only update (toggle)
else if ($hasStatus) {
$open_24_hours = (int)$data['open_24_hours'];
$stmt = $db->prepare('UPDATE markers SET open_24_hours = ? WHERE id = ?');
$stmt->execute([$open_24_hours, $id]);
echo json_encode([
'success' => true,
'id' => $id,
'open_24_hours' => $open_24_hours
]);
} else {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+131
View File
@@ -0,0 +1,131 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required field: id']);
exit;
}
$id = intval($data['id']);
$db = getDB();
// Determine what type of update this is
$hasCoordinates = isset($data['coordinates']) && isset($data['area_sqm']);
$hasName = isset($data['name']);
$hasCertificateType = isset($data['certificate_type']);
// Full edit (multiple fields including name or certificate_type)
if ($hasName || $hasCertificateType || $hasCoordinates) {
$updates = [];
$params = [];
if ($hasName) {
if (empty($data['name'])) {
http_response_code(400);
echo json_encode(['error' => 'Name cannot be empty']);
exit;
}
$updates[] = 'name = ?';
$params[] = $data['name'];
}
if ($hasCertificateType) {
$validTypes = ['SHM', 'HGB', 'HGU', 'HP'];
if (!in_array($data['certificate_type'], $validTypes)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid certificate_type. Must be: SHM, HGB, HGU, or HP']);
exit;
}
$updates[] = 'certificate_type = ?';
$params[] = $data['certificate_type'];
}
if ($hasCoordinates) {
// Validate coordinates array (minimum 3 for polygon)
if (!is_array($data['coordinates']) || count($data['coordinates']) < 3) {
http_response_code(400);
echo json_encode(['error' => 'Coordinates must be an array with at least 3 points']);
exit;
}
// Validate each coordinate
foreach ($data['coordinates'] as $coord) {
if (!is_array($coord) || count($coord) !== 2 || !is_numeric($coord[0]) || !is_numeric($coord[1])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinate format']);
exit;
}
$lat = floatval($coord[0]);
$lng = floatval($coord[1]);
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinate range']);
exit;
}
}
// Validate area_sqm is positive
if (!is_numeric($data['area_sqm']) || $data['area_sqm'] <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Area must be a positive number']);
exit;
}
$updates[] = 'coordinates = ?';
$updates[] = 'area_sqm = ?';
$params[] = json_encode($data['coordinates']);
$params[] = floatval($data['area_sqm']);
}
if (empty($updates)) {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
exit;
}
// Add updated_at and id to params
$updates[] = 'updated_at = CURRENT_TIMESTAMP';
$params[] = $id;
$sql = 'UPDATE land_parcels SET ' . implode(', ', $updates) . ' WHERE id = ?';
$stmt = $db->prepare($sql);
$stmt->execute($params);
// Fetch updated record
$getStmt = $db->prepare('SELECT * FROM land_parcels WHERE id = ?');
$getStmt->execute([$id]);
$parcel = $getStmt->fetch(PDO::FETCH_ASSOC);
if ($parcel) {
$parcel['coordinates'] = json_decode($parcel['coordinates'], true);
}
echo json_encode([
'success' => true,
'polygon' => $parcel
]);
} else {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+51
View File
@@ -0,0 +1,51 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Check authentication and authorization
requireRole(['Admin']);
if ($_SERVER['REQUEST_METHOD'] !== 'PUT') {
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
}
$data = json_decode(file_get_contents('php://input'), true);
// Validate input
if (!isset($data['id']) || !isset($data['role'])) {
http_response_code(400);
die(json_encode(['error' => 'Missing required fields: id, role']));
}
// Validate role
$validRoles = ['Admin', 'Petugas', 'Pimpinan'];
if (!in_array($data['role'], $validRoles)) {
http_response_code(400);
die(json_encode(['error' => 'Invalid role. Must be Admin, Petugas, or Pimpinan']));
}
try {
$db = getDB();
$stmt = $db->prepare('
UPDATE users
SET role = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
');
$stmt->execute([$data['role'], $data['id']]);
if ($stmt->rowCount() > 0) {
http_response_code(200);
die(json_encode([
'success' => true,
'message' => 'User role updated successfully'
]));
} else {
http_response_code(404);
die(json_encode(['error' => 'User not found']));
}
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['error' => 'Database error: ' . $e->getMessage()]));
}
+3845
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1265
View File
File diff suppressed because it is too large Load Diff
+1555
View File
File diff suppressed because it is too large Load Diff
+5611
View File
File diff suppressed because it is too large Load Diff
+473
View File
@@ -0,0 +1,473 @@
LAPORAN RANCANGAN SISTEM GEOSPASIAL UNTUK PENGENTASAN KEMISKINAN
Studi Kasus: Integrasi Data Masjid, Poin Berkebutuhan, dan Wilayah Administratif Pontianak
ABSTRAK
Kemiskinan merupakan persoalan multidimensi yang memerlukan intervensi terarah berbasis bukti. Laporan ini merancang sistem geospasial untuk mendukung pengentasan kemiskinan pada tingkat kota melalui integrasi titik layanan komunitas (masjid), titik rumah tangga/penerima berkebutuhan, data aksesibilitas jalan, dan batas wilayah administratif. Fokus utama laporan adalah analisis pihak-pihak yang terlibat serta rancangan atribut data yang dibutuhkan oleh masing-masing pihak agar proses perencanaan, verifikasi, penyaluran bantuan, dan evaluasi program dapat berjalan terukur. Rancangan dilengkapi kerangka kuantitatif berupa skor kerentanan, skor aksesibilitas, skor prioritas intervensi, dan indikator kinerja program. Hasil rancangan menunjukkan bahwa pendekatan geospasial dapat mengurangi salah sasaran, mempercepat verifikasi lapangan, dan meningkatkan transparansi lintas pemangku kepentingan.
Kata kunci: kemiskinan multidimensi, geospasial, targeting bantuan sosial, Pontianak, manajemen data, evaluasi kuantitatif.
1. PENDAHULUAN
1.1 Latar Belakang
Program pengentasan kemiskinan sering menghadapi tiga masalah utama: (1) ketidaktepatan sasaran rumah tangga prioritas, (2) keterlambatan verifikasi data lapangan, dan (3) lemahnya evaluasi spasial dampak program. Pada konteks perkotaan, masalah tersebut semakin kompleks karena heterogenitas kondisi sosial-ekonomi dan disparitas akses layanan antarwilayah.
Sistem geospasial memberi peluang untuk menggabungkan dimensi lokasi (where), kebutuhan (what), aktor (who), waktu (when), dan intervensi (how much). Dalam studi kasus ini, titik masjid diposisikan sebagai simpul komunitas untuk menjangkau titik berkebutuhan dalam radius layanan tertentu, kemudian dipadukan dengan batas wilayah administratif dan indikator kepadatan/infrastruktur.
1.2 Rumusan Masalah
1. Data atribut apa yang diperlukan masing-masing pihak agar pengentasan kemiskinan berbasis geospasial dapat berjalan efektif?
2. Bagaimana merancang atribut data yang konsisten untuk kebutuhan operasional lapangan dan kebutuhan analitik kebijakan?
3. Bagaimana membangun kerangka kuantitatif formal agar prioritas bantuan dapat ditetapkan secara objektif dan terukur?
1.3 Tujuan
1. Menyusun rancangan sistem data geospasial untuk pengentasan kemiskinan berbasis komunitas.
2. Menganalisis pihak terlibat dan kebutuhan informasi spesifik per pihak.
3. Merancang atribut data inti, pendukung, dan strategis untuk setiap pihak.
4. Menyusun model kuantitatif formal untuk penetapan prioritas intervensi dan evaluasi hasil.
1.4 Batasan
1. Laporan berfokus pada rancangan data dan tata kelola informasi.
2. Laporan tidak membahas implementasi penuh dashboard baru.
3. Rancangan mengacu pada konteks data yang sudah tersedia pada sistem (masjid, poin berkebutuhan, jalan, wilayah administratif, statistik area).
2. LANDASAN KONSEPTUAL
2.1 Kemiskinan Multidimensi
Kemiskinan tidak cukup diukur oleh pendapatan semata, melainkan juga keterbatasan akses pendidikan, kesehatan, perumahan layak, infrastruktur dasar, dan perlindungan sosial. Karena itu, desain data harus mendukung indikator multidimensi agar keputusan bantuan lebih adil.
2.2 Peran Data Geospasial
Data geospasial memungkinkan:
1. Pemetaan konsentrasi kerentanan antarwilayah.
2. Analisis jarak dan aksesibilitas terhadap titik layanan.
3. Verifikasi konsistensi lokasi terhadap wilayah administrasi.
4. Monitoring cakupan intervensi dari waktu ke waktu.
2.3 Prinsip Tata Kelola Data
1. Akurat: data tervalidasi, dapat ditelusuri sumbernya.
2. Terkini: pembaruan periodik dengan jadwal jelas.
3. Konsisten: definisi atribut dan satuan baku.
4. Aman: perlindungan data pribadi (khusus NIK/identitas sensitif).
5. Akuntabel: jelas siapa input, verifikasi, dan persetujuan.
3. PIHAK TERLIBAT DAN PERAN KEPUTUSAN
Bagian ini memetakan pemangku kepentingan utama dan jenis keputusan yang didukung data.
3.1 Pemerintah Daerah (Pemda/Bappeda)
Peran:
1. Menetapkan prioritas wilayah intervensi.
2. Menyusun alokasi anggaran lintas kecamatan/kelurahan.
3. Mengawasi kinerja program secara makro.
Keputusan berbasis data:
1. Wilayah prioritas tinggi untuk penambahan program.
2. Penyesuaian anggaran berdasarkan beban kerentanan.
3. Evaluasi efektivitas kebijakan antarperiode.
3.2 Dinas Sosial
Peran:
1. Menetapkan rumah tangga sasaran.
2. Mengelola program bantuan (jenis, nilai, jadwal, frekuensi).
3. Menjamin verifikasi administrasi penerima.
Keputusan berbasis data:
1. Daftar penerima prioritas dan waiting list.
2. Jenis bantuan paling relevan per rumah tangga.
3. Jadwal distribusi dan monitoring kepatuhan.
3.3 Kelurahan/RT/RW
Peran:
1. Verifikasi kondisi faktual rumah tangga.
2. Validasi domisili, komposisi keluarga, kondisi hunian.
3. Pelaporan perubahan cepat (migrasi, bencana, kehilangan pekerjaan).
Keputusan berbasis data:
1. Validasi kelayakan calon penerima.
2. Rekomendasi perubahan status prioritas.
3. Eskalasi kasus mendesak ke Dinas Sosial.
3.4 Pengelola Masjid (Simpul Komunitas)
Peran:
1. Menjadi titik koordinasi bantuan berbasis radius layanan.
2. Menyediakan informasi kebutuhan lokal berbasis komunitas.
3. Memantau sebaran penerima dalam cakupan layanan masjid.
Keputusan berbasis data:
1. Penjadwalan distribusi bantuan komunitas.
2. Penetapan relawan dan wilayah jangkauan.
3. Rujukan kasus rentan ke dinas terkait.
3.5 Petugas Lapangan/Enumerator
Peran:
1. Input geolokasi dan atribut rumah tangga.
2. Verifikasi berkala kondisi sosial-ekonomi.
3. Dokumentasi bukti lapangan (foto, catatan observasi, tanggal kunjungan).
Keputusan berbasis data:
1. Konfirmasi valid/tidak valid data calon penerima.
2. Klasifikasi tingkat kerentanan lapangan.
3. Rekomendasi tindak lanjut cepat.
3.6 Lembaga Penyalur Bantuan (Pemerintah/CSR/NGO/Baznas)
Peran:
1. Menyalurkan bantuan sesuai daftar terverifikasi.
2. Mencatat nilai bantuan, jenis bantuan, waktu penyaluran.
3. Memonitor coverage dan gap distribusi.
Keputusan berbasis data:
1. Penentuan kuota dan paket bantuan per wilayah.
2. Penyesuaian jenis bantuan berdasarkan profil kebutuhan.
3. Penutupan gap wilayah yang belum terlayani.
3.7 Tim Monitoring dan Evaluasi
Peran:
1. Mengukur dampak program secara periodik.
2. Menilai efisiensi, ketepatan sasaran, dan keberlanjutan.
3. Menyusun rekomendasi perbaikan kebijakan.
Keputusan berbasis data:
1. Program dilanjutkan, diperluas, atau diubah.
2. Wilayah mana yang butuh intervensi tambahan.
3. Penyesuaian indikator dan mekanisme evaluasi.
3.8 Penerima Manfaat (Rumah Tangga)
Peran:
1. Memberikan data kondisi aktual rumah tangga.
2. Menyampaikan umpan balik kualitas bantuan.
3. Mengikuti proses verifikasi dan pembaruan data.
Keputusan berbasis data:
1. Konfirmasi penerimaan bantuan.
2. Pelaporan perubahan kondisi ekonomi.
3. Persetujuan penggunaan data sesuai kebijakan privasi.
4. RANCANGAN ENTITAS DATA UTAMA
4.1 Entitas Existing (sudah tersedia pada sistem)
1. Masjid: id, name, latitude, longitude, radius_meters, created_at, updated_at.
2. Poin Berkebutuhan: id, name, latitude, longitude, masjid_id, created_at, updated_at.
3. Jalan: name, road_type, condition_status, coordinates, length_meters.
4. Area Administratif: name, area_type, geometry, population.
5. Statistik Area: statistic_type, value, date_recorded.
4.2 Entitas Baru (usulan untuk pengentasan kemiskinan)
1. Rumah Tangga:
- household_id (unik)
- head_name
- nik_hash (bukan NIK mentah)
- address_text
- latitude
- longitude
- area_id (kelurahan/kecamatan)
- masjid_id_referensi
2. Profil Sosial-Ekonomi:
- monthly_income
- monthly_food_expenditure
- employment_status_kepala
- education_level_kepala
- number_of_dependents
- disability_member_count
- elderly_member_count
- chronic_illness_member_count
- housing_condition_score
- water_access_type
- sanitation_access_type
- electricity_access_type
3. Program Bantuan:
- program_id
- household_id
- assistance_type
- assistance_value
- assistance_frequency
- start_date
- end_date
- funding_source
- delivery_status
- delivered_at
- distribution_channel
4. Verifikasi dan Audit:
- verification_status
- verified_by
- verified_at
- last_field_visit_at
- evidence_photo_count
- data_confidence_score
- audit_note
5. Outcome Program:
- outcome_30d_income_change
- outcome_90d_income_change
- food_security_score_change
- school_attendance_change
- health_service_access_change
- household_exit_flag (keluar dari prioritas bantuan)
5. MATRIKS ATRIBUT DATA PER PIHAK (FORMAL)
Catatan: Matriks ini disusun agar dapat langsung diturunkan menjadi data dictionary teknis.
Format kolom:
Pihak | Atribut Utama | Definisi | Tipe Data | Sumber | Frekuensi Update | Penanggung Jawab | Prioritas
5.1 Pemerintah Daerah (Pemda/Bappeda)
1. poverty_prevalence_area | Proporsi RT miskin per area | Decimal(5,2) | agregasi Dinsos + BPS + verifikasi lapangan | Triwulanan | Bappeda | Wajib
2. budget_allocation_area | Anggaran intervensi per area | Decimal(18,2) | sistem perencanaan daerah | Tahunan/Revisi | Bappeda | Wajib
3. intervention_coverage_ratio | Persentase RT prioritas yang sudah dilayani | Decimal(5,2) | log distribusi bantuan | Bulanan | Dinsos | Wajib
4. high_risk_cluster_count | Jumlah klaster risiko tinggi | Integer | hasil analisis geospasial | Bulanan | Tim Analitik | Penting
5. policy_outcome_index | Indeks hasil kebijakan lintas area | Decimal(5,2) | komposit indikator outcome | Semester | Bappeda | Strategis
5.2 Dinas Sosial
1. household_id | ID unik rumah tangga | Varchar | registrasi sistem | Sekali + saat perubahan | Dinsos | Wajib
2. vulnerability_score | Skor kerentanan rumah tangga (0-100) | Decimal(5,2) | komputasi indikator | Bulanan | Dinsos + Analis | Wajib
3. assistance_type | Jenis bantuan (pangan/tunai/kesehatan/pendidikan) | Enum | kebijakan program | Saat penetapan | Dinsos | Wajib
4. assistance_value | Nilai bantuan per periode | Decimal(18,2) | kebijakan program | Saat penetapan/perubahan | Dinsos | Wajib
5. eligibility_status | Status kelayakan | Enum | verifikasi data | Bulanan | Dinsos | Wajib
6. waiting_list_rank | Urutan prioritas daftar tunggu | Integer | hasil scoring | Bulanan | Dinsos | Penting
7. fraud_risk_flag | Indikator risiko ketidaksesuaian data | Boolean | audit data | Bulanan | Dinsos + Inspektorat | Strategis
5.3 Kelurahan/RT/RW
1. domicile_status | Validitas domisili | Enum(valid, ragu, tidak_valid) | verifikasi lapangan | Bulanan | RT/RW | Wajib
2. family_member_count | Jumlah anggota keluarga aktual | Integer | wawancara + dokumen | Bulanan | RT/RW | Wajib
3. recent_shock_event | Kejadian guncangan (PHK, sakit berat, bencana) | Enum + Text | laporan warga | Real-time/Bulanan | RT/RW | Wajib
4. housing_feasibility_level | Kelayakan hunian | Ordinal(1-5) | observasi lapangan | Triwulanan | Kelurahan | Penting
5. neighborhood_support_index | Dukungan sosial lingkungan | Decimal(5,2) | asesmen lokal | Semester | Kelurahan | Strategis
5.4 Pengelola Masjid
1. masjid_id | ID masjid | Integer | data master masjid | Tetap | Admin Sistem | Wajib
2. radius_meters | Radius layanan masjid | Decimal(8,2) | data masjid | Saat perubahan | Pengelola Masjid | Wajib
3. served_household_count | Jumlah RT prioritas dalam radius | Integer | join rumah tangga geospasial | Bulanan | Pengelola Masjid | Wajib
4. volunteer_count | Jumlah relawan aktif | Integer | administrasi masjid | Bulanan | Pengelola Masjid | Penting
5. social_program_capacity | Kapasitas paket bantuan per bulan | Integer | logistik masjid | Bulanan | Pengelola Masjid | Penting
6. stock_food_package | Stok paket pangan | Integer | inventori | Mingguan | Pengelola Masjid | Penting
7. service_gap_flag | Penanda gap layanan (permintaan > kapasitas) | Boolean | komputasi sistem | Mingguan | Pengelola + Dinsos | Strategis
5.5 Petugas Lapangan
1. survey_timestamp | Waktu survei | Datetime | aplikasi lapangan | Setiap kunjungan | Petugas | Wajib
2. gps_accuracy_meters | Akurasi koordinat saat survei | Decimal(6,2) | perangkat GPS | Setiap kunjungan | Petugas | Wajib
3. photo_evidence_count | Jumlah bukti foto | Integer | unggahan lapangan | Setiap kunjungan | Petugas | Penting
4. field_note | Catatan temuan lapangan | Text | observasi | Setiap kunjungan | Petugas | Wajib
5. verification_recommendation | Rekomendasi validasi | Enum(approve/review/reject) | asesmen lapangan | Setiap kunjungan | Petugas | Wajib
6. revisit_due_date | Jadwal kunjungan ulang | Date | workflow verifikasi | Sesuai kasus | Petugas | Penting
5.6 Lembaga Penyalur Bantuan
1. distribution_id | ID penyaluran | Varchar | sistem distribusi | Setiap transaksi | Penyalur | Wajib
2. household_id | RT penerima | Varchar | data target Dinsos | Setiap transaksi | Penyalur | Wajib
3. distribution_date | Tanggal salur | Date | log distribusi | Setiap transaksi | Penyalur | Wajib
4. package_type | Jenis paket bantuan | Enum | program penyalur | Setiap transaksi | Penyalur | Wajib
5. package_value | Nilai paket | Decimal(18,2) | program penyalur | Setiap transaksi | Penyalur | Wajib
6. handover_status | Status serah terima | Enum(success/pending/failed) | dokumentasi salur | Setiap transaksi | Penyalur | Wajib
7. failed_delivery_reason | Alasan gagal salur | Text | dokumentasi salur | Jika gagal | Penyalur | Penting
8. repeat_assistance_interval_days | Interval bantuan ulang | Integer | kebijakan program | Saat penetapan | Penyalur + Dinsos | Strategis
5.7 Tim Monitoring dan Evaluasi
1. baseline_score | Nilai dasar sebelum intervensi | Decimal(6,2) | data awal rumah tangga | Awal program | Tim Monev | Wajib
2. endline_score | Nilai setelah intervensi | Decimal(6,2) | data akhir periode | Akhir periode | Tim Monev | Wajib
3. score_delta | Perubahan skor | Decimal(6,2) | komputasi | Bulanan/Triwulan | Tim Monev | Wajib
4. targeting_precision | Akurasi sasaran bantuan (%) | Decimal(5,2) | audit sampel | Triwulanan | Tim Monev | Penting
5. leakage_rate | Tingkat kebocoran bantuan (%) | Decimal(5,2) | audit transaksi | Triwulanan | Tim Monev | Penting
6. timeliness_index | Ketepatan waktu distribusi | Decimal(5,2) | log operasional | Bulanan | Tim Monev | Penting
7. outcome_sustainability_index | Keberlanjutan dampak | Decimal(5,2) | follow-up 3-6 bulan | Semester | Tim Monev | Strategis
5.8 Penerima Manfaat
1. consent_status | Persetujuan penggunaan data | Boolean | formulir persetujuan | Awal + saat perubahan | Petugas | Wajib
2. self_report_income_change | Perubahan pendapatan menurut pelaporan warga | Decimal(6,2) | wawancara | Bulanan | Petugas | Penting
3. service_satisfaction_score | Kepuasan layanan (1-5) | Integer | survei feedback | Bulanan | Tim Monev | Penting
4. complaint_status | Status pengaduan | Enum(open/in_review/closed) | kanal pengaduan | Real-time | Dinsos | Penting
5. participation_status | Keikutsertaan program pemberdayaan | Enum | catatan program | Bulanan | Dinsos + Penyalur | Strategis
6. KERANGKA KUANTITATIF FORMAL
6.1 Normalisasi Nilai
Semua indikator dinormalisasi ke skala 0 sampai 1 agar dapat digabungkan.
Rumus normalisasi min-max:
X_norm = (X - X_min) / (X_max - X_min)
Untuk indikator negatif (semakin besar semakin buruk), gunakan:
X_norm_neg = 1 - ((X - X_min) / (X_max - X_min))
6.2 Skor Kerentanan Rumah Tangga (SKR)
SKR = 0.25*I + 0.20*E + 0.15*D + 0.15*H + 0.15*A + 0.10*S
Keterangan komponen:
I: Deprivasi pendapatan.
E: Kerentanan ketenagakerjaan.
D: Beban tanggungan (anak, lansia, disabilitas).
H: Kualitas hunian dan sanitasi.
A: Akses layanan dasar (air, listrik, kesehatan, pendidikan).
S: Riwayat guncangan sosial-ekonomi.
Rentang SKR: 0 sampai 1.
6.3 Skor Aksesibilitas Geospasial (SAG)
SAG = 0.40*J + 0.35*R + 0.25*T
J: Jarak ke titik layanan bantuan (dinormalisasi negatif).
R: Kualitas akses jalan (berdasarkan condition_status dan road_density lokal).
T: Estimasi waktu tempuh layanan.
Interpretasi:
Nilai lebih tinggi = aksesibilitas lebih baik.
6.4 Skor Prioritas Intervensi (SPI)
SPI = 0.60*SKR + 0.30*(1 - SAG) + 0.10*K
K: Faktor kejadian guncangan terbaru (0 atau 1, atau skala 0 sampai 1).
Klasifikasi:
1. SPI >= 0.75: Prioritas sangat tinggi.
2. 0.60 <= SPI < 0.75: Prioritas tinggi.
3. 0.45 <= SPI < 0.60: Prioritas menengah.
4. SPI < 0.45: Prioritas rendah.
6.5 Indikator Kinerja Program (IKP)
1. Coverage Ratio (CR) = jumlah RT terbantu / jumlah RT prioritas.
2. Targeting Precision (TP) = RT sasaran benar / total RT yang menerima bantuan.
3. On-Time Delivery (OTD) = bantuan tepat waktu / total bantuan tersalurkan.
4. Outcome Improvement Rate (OIR) = RT dengan delta outcome positif / total RT yang dievaluasi.
7. PEMETAAN EXISTING DATA VS GAP DATA
7.1 Atribut yang sudah tersedia
1. Titik masjid dan radius layanan.
2. Titik berkebutuhan beserta relasi ke masjid.
3. Batas wilayah administratif (kecamatan/kota).
4. Statistik area (kepadatan penduduk/jalan/kerusakan jalan).
5. Data infrastruktur jalan (jenis dan kondisi).
7.2 Atribut yang belum tersedia (gap utama)
1. Profil sosial-ekonomi rumah tangga detail.
2. Status kemiskinan multidimensi tingkat rumah tangga.
3. Riwayat bantuan per rumah tangga (jenis, nilai, frekuensi).
4. Status verifikasi, bukti lapangan, dan confidence score.
5. Data outcome pasca-bantuan (30 hari, 90 hari, semester).
6. Data pengaduan dan kepuasan layanan.
7.3 Dampak jika gap tidak ditutup
1. Penetapan sasaran rentan bias.
2. Sulit membedakan rumah tangga prioritas antarwilayah.
3. Evaluasi program hanya berhenti pada output, bukan outcome.
4. Risiko kebocoran dan duplikasi bantuan meningkat.
8. TAHAP IMPLEMENTASI DATA (REKOMENDASI)
Tahap 1 (0-3 bulan): Pendataan dasar dan verifikasi spasial
1. Tambah atribut rumah tangga inti.
2. Validasi koordinat dan area administrasi.
3. Integrasi relasi RT-ke-masjid dalam radius layanan.
Tahap 2 (3-6 bulan): Operasional penyaluran dan audit
1. Tambah modul riwayat bantuan dan status distribusi.
2. Tambah atribut verifikasi lapangan dan confidence score.
3. Jalankan dashboard monitoring coverage dan ketepatan sasaran.
Tahap 3 (6-12 bulan): Evaluasi outcome dan optimasi kebijakan
1. Tambah atribut outcome 30/90 hari.
2. Hitung IKP rutin per wilayah.
3. Lakukan evaluasi pembobotan SPI dan revisi kebijakan.
9. RISIKO DAN MITIGASI
9.1 Risiko kualitas data
Risiko: data tidak mutakhir atau tidak konsisten antarpetugas.
Mitigasi:
1. SOP input baku.
2. Validasi otomatis pada rentang nilai.
3. Audit sampel bulanan.
9.2 Risiko privasi data
Risiko: kebocoran data identitas sensitif.
Mitigasi:
1. Simpan identitas sensitif dalam bentuk hash/token.
2. Batasi akses berbasis peran.
3. Log akses dan log perubahan data.
9.3 Risiko bias penargetan
Risiko: ketergantungan pada satu indikator.
Mitigasi:
1. Gunakan indikator multidimensi.
2. Verifikasi silang RT/RW dan petugas lapangan.
3. Review bobot skor secara periodik.
10. KESIMPULAN
Rancangan sistem geospasial pengentasan kemiskinan pada studi kasus ini menempatkan data sebagai fondasi keputusan lintas aktor. Dengan merinci atribut per pihak, sistem dapat menjembatani kebutuhan operasional lapangan dan kebutuhan strategis kebijakan. Kerangka kuantitatif yang diusulkan (SKR, SAG, SPI, IKP) memberi landasan formal untuk prioritisasi intervensi, peningkatan akurasi sasaran, dan evaluasi hasil program secara objektif.
Rekomendasi utama:
1. Prioritaskan penutupan gap atribut rumah tangga dan riwayat bantuan.
2. Terapkan verifikasi spasial dan audit data berkala.
3. Gunakan indikator outcome agar kebijakan tidak hanya mengejar jumlah bantuan, tetapi juga perubahan kesejahteraan nyata.
DAFTAR PUSTAKA (FORMAT IEEE)
Catatan: Detail metadata pada daftar ini disusun dalam format IEEE dan perlu verifikasi akhir (judul/edisi/tahun/URL/DOI) sebelum pengumpulan final.
[1] A. Sen, "Poverty: An Ordinal Approach to Measurement," Econometrica, vol. 44, no. 2, pp. 219-231, 1976.
[2] S. Alkire and J. Foster, "Counting and Multidimensional Poverty Measurement," Journal of Public Economics, vol. 95, no. 7-8, pp. 476-487, 2011.
[3] World Bank, Poverty and Shared Prosperity 2022: Correcting Course. Washington, DC, USA: World Bank, 2022.
[4] United Nations Development Programme (UNDP) and Oxford Poverty and Human Development Initiative (OPHI), Global Multidimensional Poverty Index 2023. New York, NY, USA: UNDP, 2023.
[5] Badan Pusat Statistik (BPS), Profil Kemiskinan di Indonesia (rilis berkala). Jakarta, Indonesia: BPS, 2024.
[6] P. A. Longley, M. F. Goodchild, D. J. Maguire, and D. W. Rhind, Geographic Information Systems and Science, 4th ed. Hoboken, NJ, USA: Wiley, 2015.
[7] Republik Indonesia, Undang-Undang Nomor 4 Tahun 2011 tentang Informasi Geospasial. Jakarta, Indonesia, 2011.
[8] Republik Indonesia, Peraturan Presiden Nomor 39 Tahun 2019 tentang Satu Data Indonesia. Jakarta, Indonesia, 2019.
LAMPIRAN A. RINGKASAN KODE ATRIBUT PRIORITAS (MVP)
A1. Data Rumah Tangga
1. household_id (varchar)
2. latitude (decimal)
3. longitude (decimal)
4. monthly_income (decimal)
5. employment_status_kepala (enum)
6. number_of_dependents (int)
7. housing_condition_score (int 1-5)
8. verification_status (enum)
9. vulnerability_score (decimal)
10. priority_score (decimal)
A2. Data Bantuan
1. assistance_type (enum)
2. assistance_value (decimal)
3. distribution_date (date)
4. delivery_status (enum)
5. delivered_by (varchar)
A3. Data Monitoring
1. baseline_score (decimal)
2. endline_score (decimal)
3. score_delta (decimal)
4. satisfaction_score (int)
5. complaint_status (enum)
LAMPIRAN B. CONTOH STANDAR ENUM
B1. assistance_type
1. pangan
2. tunai
3. kesehatan
4. pendidikan
5. usaha_mikro
B2. verification_status
1. unverified
2. field_verified
3. admin_approved
4. rejected
B3. delivery_status
1. pending
2. delivered
3. failed
SELESAI
+31
View File
@@ -0,0 +1,31 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing marker id']);
exit;
}
$db = getDB();
$stmt = $db->prepare('DELETE FROM markers WHERE id = ?');
$stmt->execute([$data['id']]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+26
View File
@@ -0,0 +1,26 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query('SELECT id, name, nomor_spbu, latitude, longitude, open_24_hours, created_at FROM markers ORDER BY created_at DESC');
$markers = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'markers' => $markers
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+56
View File
@@ -0,0 +1,56 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0); // Don't display errors in output
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['name']) || !isset($data['latitude']) || !isset($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: name, latitude, or longitude']);
exit;
}
// Validate coordinates are numeric
if (!is_numeric($data['latitude']) || !is_numeric($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Latitude and longitude must be numeric values']);
exit;
}
$db = getDB();
$stmt = $db->prepare('INSERT INTO markers (name, nomor_spbu, latitude, longitude, open_24_hours) VALUES (?, ?, ?, ?, ?)');
$open_24_hours = isset($data['open_24_hours']) ? (int)$data['open_24_hours'] : 0;
$nomor_spbu = isset($data['nomor_spbu']) ? $data['nomor_spbu'] : null;
$stmt->execute([
$data['name'],
$nomor_spbu,
(float)$data['latitude'],
(float)$data['longitude'],
$open_24_hours
]);
echo json_encode([
'success' => true,
'id' => $db->lastInsertId(),
'name' => $data['name'],
'nomor_spbu' => $nomor_spbu,
'latitude' => (float)$data['latitude'],
'longitude' => (float)$data['longitude'],
'open_24_hours' => $open_24_hours
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+154
View File
@@ -0,0 +1,154 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required field: id']);
exit;
}
$id = (int)$data['id'];
$db = getDB();
// Determine what type of update this is
$hasCoordinates = isset($data['latitude']) && isset($data['longitude']);
$hasName = isset($data['name']);
$hasNomorSpbu = isset($data['nomor_spbu']);
$hasStatus = isset($data['open_24_hours']);
// Full edit (multiple fields including coordinates or name)
if ($hasName || ($hasCoordinates && ($hasName || $hasNomorSpbu || $hasStatus))) {
$updates = [];
$params = [];
if ($hasName) {
if (empty($data['name'])) {
http_response_code(400);
echo json_encode(['error' => 'Name cannot be empty']);
exit;
}
$updates[] = 'name = ?';
$params[] = $data['name'];
}
if ($hasNomorSpbu) {
$updates[] = 'nomor_spbu = ?';
$params[] = $data['nomor_spbu'] ?: null;
}
if ($hasCoordinates) {
if (!is_numeric($data['latitude']) || !is_numeric($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Latitude and longitude must be numeric values']);
exit;
}
$latitude = (float)$data['latitude'];
$longitude = (float)$data['longitude'];
if ($latitude < -90 || $latitude > 90 || $longitude < -180 || $longitude > 180) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinates']);
exit;
}
$updates[] = 'latitude = ?';
$updates[] = 'longitude = ?';
$params[] = $latitude;
$params[] = $longitude;
}
if ($hasStatus) {
$updates[] = 'open_24_hours = ?';
$params[] = (int)$data['open_24_hours'];
}
if (empty($updates)) {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
exit;
}
// Add id to params
$params[] = $id;
$sql = 'UPDATE markers SET ' . implode(', ', $updates) . ' WHERE id = ?';
$stmt = $db->prepare($sql);
$stmt->execute($params);
// Fetch updated record
$getStmt = $db->prepare('SELECT * FROM markers WHERE id = ?');
$getStmt->execute([$id]);
$marker = $getStmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'marker' => $marker
]);
}
// Coordinates-only update (drag)
else if ($hasCoordinates) {
if (!is_numeric($data['latitude']) || !is_numeric($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Latitude and longitude must be numeric values']);
exit;
}
$latitude = (float)$data['latitude'];
$longitude = (float)$data['longitude'];
if ($latitude < -90 || $latitude > 90) {
http_response_code(400);
echo json_encode(['error' => 'Latitude must be between -90 and 90']);
exit;
}
if ($longitude < -180 || $longitude > 180) {
http_response_code(400);
echo json_encode(['error' => 'Longitude must be between -180 and 180']);
exit;
}
$stmt = $db->prepare('UPDATE markers SET latitude = ?, longitude = ? WHERE id = ?');
$stmt->execute([$latitude, $longitude, $id]);
echo json_encode([
'success' => true,
'id' => $id,
'latitude' => $latitude,
'longitude' => $longitude
]);
}
// Status-only update (toggle)
else if ($hasStatus) {
$open_24_hours = (int)$data['open_24_hours'];
$stmt = $db->prepare('UPDATE markers SET open_24_hours = ? WHERE id = ?');
$stmt->execute([$open_24_hours, $id]);
echo json_encode([
'success' => true,
'id' => $id,
'open_24_hours' => $open_24_hours
]);
} else {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,31 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing road id']);
exit;
}
$db = getDB();
$stmt = $db->prepare('DELETE FROM roads WHERE id = ?');
$stmt->execute([$data['id']]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,31 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing parcel id']);
exit;
}
$db = getDB();
$stmt = $db->prepare('DELETE FROM land_parcels WHERE id = ?');
$stmt->execute([$data['id']]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,31 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query('SELECT id, name, road_type, coordinates, length_meters, created_at, updated_at FROM roads ORDER BY created_at DESC');
$roads = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decode coordinates JSON for each road
foreach ($roads as &$road) {
$road['coordinates'] = json_decode($road['coordinates'], true);
}
echo json_encode([
'success' => true,
'lines' => $roads
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,31 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query('SELECT id, name, certificate_type, coordinates, area_sqm, created_at, updated_at FROM land_parcels ORDER BY created_at DESC');
$parcels = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decode coordinates JSON for each parcel
foreach ($parcels as &$parcel) {
$parcel['coordinates'] = json_decode($parcel['coordinates'], true);
}
echo json_encode([
'success' => true,
'polygons' => $parcels
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,113 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['name']) || !isset($data['road_type']) || !isset($data['coordinates']) || !isset($data['length_meters'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: name, road_type, coordinates, or length_meters']);
exit;
}
// Validate road_type
$validTypes = ['Nasional', 'Provinsi', 'Kabupaten'];
if (!in_array($data['road_type'], $validTypes)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid road_type. Must be: Nasional, Provinsi, or Kabupaten']);
exit;
}
// Validate coordinates array
if (!is_array($data['coordinates']) || count($data['coordinates']) < 2) {
http_response_code(400);
echo json_encode(['error' => 'Coordinates must be an array with at least 2 points']);
exit;
}
// Validate each coordinate
foreach ($data['coordinates'] as $coord) {
if (!is_array($coord) || count($coord) !== 2) {
http_response_code(400);
echo json_encode(['error' => 'Each coordinate must be [latitude, longitude]']);
exit;
}
$lat = floatval($coord[0]);
$lng = floatval($coord[1]);
if (!is_numeric($coord[0]) || !is_numeric($coord[1])) {
http_response_code(400);
echo json_encode(['error' => 'Coordinates must be numeric values']);
exit;
}
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinate range. Latitude: -90 to 90, Longitude: -180 to 180']);
exit;
}
}
// Validate name is not empty
if (empty(trim($data['name']))) {
http_response_code(400);
echo json_encode(['error' => 'Road name cannot be empty']);
exit;
}
// Validate length_meters is positive
if (!is_numeric($data['length_meters']) || $data['length_meters'] <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Length must be a positive number']);
exit;
}
// Validate condition_status if provided
$condition_status = isset($data['condition_status']) ? $data['condition_status'] : 'normal';
$validConditions = ['normal', 'minor_damage', 'major_damage'];
if (!in_array($condition_status, $validConditions)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid condition_status. Must be: normal, minor_damage, or major_damage']);
exit;
}
$db = getDB();
// Store coordinates as JSON
$coordinatesJson = json_encode($data['coordinates']);
$stmt = $db->prepare('INSERT INTO roads (name, road_type, coordinates, length_meters, condition_status) VALUES (?, ?, ?, ?, ?)');
$stmt->execute([
$data['name'],
$data['road_type'],
$coordinatesJson,
floatval($data['length_meters']),
$condition_status
]);
echo json_encode([
'success' => true,
'id' => $db->lastInsertId(),
'name' => $data['name'],
'road_type' => $data['road_type'],
'coordinates' => $data['coordinates'],
'length_meters' => floatval($data['length_meters']),
'condition_status' => $condition_status,
'created_at' => date('Y-m-d H:i:s')
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,102 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['name']) || !isset($data['certificate_type']) || !isset($data['coordinates']) || !isset($data['area_sqm'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: name, certificate_type, coordinates, or area_sqm']);
exit;
}
// Validate certificate_type
$validTypes = ['SHM', 'HGB', 'HGU', 'HP'];
if (!in_array($data['certificate_type'], $validTypes)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid certificate_type. Must be: SHM, HGB, HGU, or HP']);
exit;
}
// Validate coordinates array (minimum 3 for polygon)
if (!is_array($data['coordinates']) || count($data['coordinates']) < 3) {
http_response_code(400);
echo json_encode(['error' => 'Coordinates must be an array with at least 3 points for a polygon']);
exit;
}
// Validate each coordinate
foreach ($data['coordinates'] as $coord) {
if (!is_array($coord) || count($coord) !== 2) {
http_response_code(400);
echo json_encode(['error' => 'Each coordinate must be [latitude, longitude]']);
exit;
}
$lat = floatval($coord[0]);
$lng = floatval($coord[1]);
if (!is_numeric($coord[0]) || !is_numeric($coord[1])) {
http_response_code(400);
echo json_encode(['error' => 'Coordinates must be numeric values']);
exit;
}
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinate range. Latitude: -90 to 90, Longitude: -180 to 180']);
exit;
}
}
// Validate name is not empty
if (empty(trim($data['name']))) {
http_response_code(400);
echo json_encode(['error' => 'Parcel name cannot be empty']);
exit;
}
// Validate area_sqm is positive
if (!is_numeric($data['area_sqm']) || $data['area_sqm'] <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Area must be a positive number']);
exit;
}
$db = getDB();
// Store coordinates as JSON
$coordinatesJson = json_encode($data['coordinates']);
$stmt = $db->prepare('INSERT INTO land_parcels (name, certificate_type, coordinates, area_sqm) VALUES (?, ?, ?, ?)');
$stmt->execute([
$data['name'],
$data['certificate_type'],
$coordinatesJson,
floatval($data['area_sqm'])
]);
echo json_encode([
'success' => true,
'id' => $db->lastInsertId(),
'name' => $data['name'],
'certificate_type' => $data['certificate_type'],
'coordinates' => $data['coordinates'],
'area_sqm' => floatval($data['area_sqm']),
'created_at' => date('Y-m-d H:i:s')
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,99 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
try {
$db = getDB();
// Get filter parameters
$search = isset($_GET['search']) ? '%' . $_GET['search'] . '%' : '';
$from_date = isset($_GET['from_date']) ? $_GET['from_date'] : '';
$to_date = isset($_GET['to_date']) ? $_GET['to_date'] : '';
$certificate_type = isset($_GET['certificate_type']) ? $_GET['certificate_type'] : '';
// Build dynamic WHERE clause
$where_clauses = [];
$params = [];
if ($search !== '%' && $search !== '%') {
$where_clauses[] = '(name LIKE ?)';
$params[] = $search;
}
if ($from_date) {
$where_clauses[] = '(DATE(created_at) >= ?)';
$params[] = $from_date;
}
if ($to_date) {
$where_clauses[] = '(DATE(created_at) <= ?)';
$params[] = $to_date;
}
if ($certificate_type) {
$where_clauses[] = '(certificate_type = ?)';
$params[] = $certificate_type;
}
// Build SQL query
$sql = 'SELECT id, name, certificate_type, coordinates, area_sqm, created_at, updated_at FROM land_parcels';
if (count($where_clauses) > 0) {
$sql .= ' WHERE ' . implode(' AND ', $where_clauses);
}
$sql .= ' ORDER BY created_at DESC';
// Execute query
$stmt = $db->prepare($sql);
$stmt->execute($params);
$parcels = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decode coordinates JSON for each parcel
foreach ($parcels as &$parcel) {
$parcel['coordinates'] = json_decode($parcel['coordinates'], true);
}
// Count total for query
$count_sql = 'SELECT COUNT(*) as total FROM land_parcels' . (count($where_clauses) > 0 ? ' WHERE ' . implode(' AND ', $where_clauses) : '');
$count_stmt = $db->prepare($count_sql);
$count_stmt->execute($params);
$total = $count_stmt->fetch(PDO::FETCH_ASSOC)['total'];
// Prepare filters applied info
$filters_applied = [];
if ($search !== '%' && $search !== '%') {
$filters_applied['search'] = str_replace('%', '', $search);
}
if ($from_date || $to_date) {
$filters_applied['date_range'] = [];
if ($from_date) $filters_applied['date_range'][] = $from_date;
if ($to_date) $filters_applied['date_range'][] = $to_date;
}
if ($certificate_type) {
$filters_applied['certificate_type'] = $certificate_type;
}
// Return success response
http_response_code(200);
echo json_encode([
'success' => true,
'polygons' => $parcels,
'total' => $total,
'filters_applied' => $filters_applied
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>
@@ -0,0 +1,99 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
try {
$db = getDB();
// Get filter parameters
$search = isset($_GET['search']) ? '%' . $_GET['search'] . '%' : '';
$from_date = isset($_GET['from_date']) ? $_GET['from_date'] : '';
$to_date = isset($_GET['to_date']) ? $_GET['to_date'] : '';
$condition_status = isset($_GET['condition_status']) ? $_GET['condition_status'] : '';
// Build dynamic WHERE clause
$where_clauses = [];
$params = [];
if ($search !== '%' && $search !== '%') {
$where_clauses[] = '(name LIKE ?)';
$params[] = $search;
}
if ($from_date) {
$where_clauses[] = '(DATE(created_at) >= ?)';
$params[] = $from_date;
}
if ($to_date) {
$where_clauses[] = '(DATE(created_at) <= ?)';
$params[] = $to_date;
}
if ($condition_status) {
$where_clauses[] = '(condition_status = ?)';
$params[] = $condition_status;
}
// Build SQL query
$sql = 'SELECT id, name, road_type, coordinates, length_meters, condition_status, created_at, updated_at FROM roads';
if (count($where_clauses) > 0) {
$sql .= ' WHERE ' . implode(' AND ', $where_clauses);
}
$sql .= ' ORDER BY created_at DESC';
// Execute query
$stmt = $db->prepare($sql);
$stmt->execute($params);
$roads = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decode coordinates JSON for each road
foreach ($roads as &$road) {
$road['coordinates'] = json_decode($road['coordinates'], true);
}
// Count total for unfiltered query
$count_sql = 'SELECT COUNT(*) as total FROM roads' . (count($where_clauses) > 0 ? ' WHERE ' . implode(' AND ', $where_clauses) : '');
$count_stmt = $db->prepare($count_sql);
$count_stmt->execute($params);
$total = $count_stmt->fetch(PDO::FETCH_ASSOC)['total'];
// Prepare filters applied info
$filters_applied = [];
if ($search !== '%' && $search !== '%') {
$filters_applied['search'] = str_replace('%', '', $search);
}
if ($from_date || $to_date) {
$filters_applied['date_range'] = [];
if ($from_date) $filters_applied['date_range'][] = $from_date;
if ($to_date) $filters_applied['date_range'][] = $to_date;
}
if ($condition_status) {
$filters_applied['condition_status'] = $condition_status;
}
// Return success response
http_response_code(200);
echo json_encode([
'success' => true,
'lines' => $roads,
'total' => $total,
'filters_applied' => $filters_applied
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>
@@ -0,0 +1,143 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required field: id']);
exit;
}
$id = intval($data['id']);
$db = getDB();
// Determine what type of update this is
$hasCoordinates = isset($data['coordinates']) && isset($data['length_meters']);
$hasName = isset($data['name']);
$hasRoadType = isset($data['road_type']);
$hasConditionStatus = isset($data['condition_status']);
// Full edit (multiple fields including name or road_type)
if ($hasName || $hasRoadType || $hasCoordinates || $hasConditionStatus) {
$updates = [];
$params = [];
if ($hasName) {
if (empty($data['name'])) {
http_response_code(400);
echo json_encode(['error' => 'Name cannot be empty']);
exit;
}
$updates[] = 'name = ?';
$params[] = $data['name'];
}
if ($hasRoadType) {
$validTypes = ['Nasional', 'Provinsi', 'Kabupaten'];
if (!in_array($data['road_type'], $validTypes)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid road_type. Must be: Nasional, Provinsi, or Kabupaten']);
exit;
}
$updates[] = 'road_type = ?';
$params[] = $data['road_type'];
}
if ($hasConditionStatus) {
$validConditions = ['normal', 'minor_damage', 'major_damage'];
if (!in_array($data['condition_status'], $validConditions)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid condition_status. Must be: normal, minor_damage, or major_damage']);
exit;
}
$updates[] = 'condition_status = ?';
$params[] = $data['condition_status'];
}
if ($hasCoordinates) {
// Validate coordinates array
if (!is_array($data['coordinates']) || count($data['coordinates']) < 2) {
http_response_code(400);
echo json_encode(['error' => 'Coordinates must be an array with at least 2 points']);
exit;
}
// Validate each coordinate
foreach ($data['coordinates'] as $coord) {
if (!is_array($coord) || count($coord) !== 2 || !is_numeric($coord[0]) || !is_numeric($coord[1])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinate format']);
exit;
}
$lat = floatval($coord[0]);
$lng = floatval($coord[1]);
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinate range']);
exit;
}
}
// Validate length_meters is positive
if (!is_numeric($data['length_meters']) || $data['length_meters'] <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Length must be a positive number']);
exit;
}
$updates[] = 'coordinates = ?';
$updates[] = 'length_meters = ?';
$params[] = json_encode($data['coordinates']);
$params[] = floatval($data['length_meters']);
}
if (empty($updates)) {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
exit;
}
// Add updated_at and id to params
$updates[] = 'updated_at = CURRENT_TIMESTAMP';
$params[] = $id;
$sql = 'UPDATE roads SET ' . implode(', ', $updates) . ' WHERE id = ?';
$stmt = $db->prepare($sql);
$stmt->execute($params);
// Fetch updated record
$getStmt = $db->prepare('SELECT * FROM roads WHERE id = ?');
$getStmt->execute([$id]);
$road = $getStmt->fetch(PDO::FETCH_ASSOC);
if ($road) {
$road['coordinates'] = json_decode($road['coordinates'], true);
}
echo json_encode([
'success' => true,
'line' => $road
]);
} else {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,131 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required field: id']);
exit;
}
$id = intval($data['id']);
$db = getDB();
// Determine what type of update this is
$hasCoordinates = isset($data['coordinates']) && isset($data['area_sqm']);
$hasName = isset($data['name']);
$hasCertificateType = isset($data['certificate_type']);
// Full edit (multiple fields including name or certificate_type)
if ($hasName || $hasCertificateType || $hasCoordinates) {
$updates = [];
$params = [];
if ($hasName) {
if (empty($data['name'])) {
http_response_code(400);
echo json_encode(['error' => 'Name cannot be empty']);
exit;
}
$updates[] = 'name = ?';
$params[] = $data['name'];
}
if ($hasCertificateType) {
$validTypes = ['SHM', 'HGB', 'HGU', 'HP'];
if (!in_array($data['certificate_type'], $validTypes)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid certificate_type. Must be: SHM, HGB, HGU, or HP']);
exit;
}
$updates[] = 'certificate_type = ?';
$params[] = $data['certificate_type'];
}
if ($hasCoordinates) {
// Validate coordinates array (minimum 3 for polygon)
if (!is_array($data['coordinates']) || count($data['coordinates']) < 3) {
http_response_code(400);
echo json_encode(['error' => 'Coordinates must be an array with at least 3 points']);
exit;
}
// Validate each coordinate
foreach ($data['coordinates'] as $coord) {
if (!is_array($coord) || count($coord) !== 2 || !is_numeric($coord[0]) || !is_numeric($coord[1])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinate format']);
exit;
}
$lat = floatval($coord[0]);
$lng = floatval($coord[1]);
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
http_response_code(400);
echo json_encode(['error' => 'Invalid coordinate range']);
exit;
}
}
// Validate area_sqm is positive
if (!is_numeric($data['area_sqm']) || $data['area_sqm'] <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Area must be a positive number']);
exit;
}
$updates[] = 'coordinates = ?';
$updates[] = 'area_sqm = ?';
$params[] = json_encode($data['coordinates']);
$params[] = floatval($data['area_sqm']);
}
if (empty($updates)) {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
exit;
}
// Add updated_at and id to params
$updates[] = 'updated_at = CURRENT_TIMESTAMP';
$params[] = $id;
$sql = 'UPDATE land_parcels SET ' . implode(', ', $updates) . ' WHERE id = ?';
$stmt = $db->prepare($sql);
$stmt->execute($params);
// Fetch updated record
$getStmt = $db->prepare('SELECT * FROM land_parcels WHERE id = ?');
$getStmt->execute([$id]);
$parcel = $getStmt->fetch(PDO::FETCH_ASSOC);
if ($parcel) {
$parcel['coordinates'] = json_decode($parcel['coordinates'], true);
}
echo json_encode([
'success' => true,
'polygon' => $parcel
]);
} else {
http_response_code(400);
echo json_encode(['error' => 'No fields to update']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,479 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
function decodeGeometry($rawGeometry) {
if (is_array($rawGeometry)) {
return $rawGeometry;
}
$decoded = json_decode($rawGeometry, true);
return is_array($decoded) ? $decoded : null;
}
function geometryRings($geometry) {
if (!is_array($geometry) || !isset($geometry['type']) || !isset($geometry['coordinates'])) {
return [];
}
if ($geometry['type'] === 'Polygon') {
return [$geometry['coordinates']];
}
if ($geometry['type'] === 'MultiPolygon') {
return $geometry['coordinates'];
}
return [];
}
function projectLonLatToMeters($lng, $lat, $referenceLat) {
$earthRadius = 6371000.0;
$x = deg2rad((float)$lng) * $earthRadius * cos(deg2rad((float)$referenceLat));
$y = deg2rad((float)$lat) * $earthRadius;
return [$x, $y];
}
function ringAreaSquareMeters($ring) {
if (!is_array($ring) || count($ring) < 4) {
return 0.0;
}
$sumLat = 0.0;
$count = 0;
foreach ($ring as $point) {
if (is_array($point) && count($point) >= 2) {
$sumLat += (float)$point[1];
$count++;
}
}
if ($count === 0) {
return 0.0;
}
$referenceLat = $sumLat / $count;
$projected = [];
foreach ($ring as $point) {
if (is_array($point) && count($point) >= 2) {
$projected[] = projectLonLatToMeters((float)$point[0], (float)$point[1], $referenceLat);
}
}
if (count($projected) < 4) {
return 0.0;
}
$area = 0.0;
$maxIndex = count($projected) - 1;
for ($i = 0; $i < $maxIndex; $i++) {
$area += ($projected[$i][0] * $projected[$i + 1][1]) - ($projected[$i + 1][0] * $projected[$i][1]);
}
return abs($area / 2.0);
}
function geometryAreaKm2($geometry) {
$polygons = geometryRings($geometry);
if (empty($polygons)) {
return 0.0;
}
$totalSquareMeters = 0.0;
foreach ($polygons as $polygonRings) {
if (!is_array($polygonRings) || empty($polygonRings)) {
continue;
}
$outerArea = ringAreaSquareMeters($polygonRings[0]);
$holeArea = 0.0;
for ($i = 1; $i < count($polygonRings); $i++) {
$holeArea += ringAreaSquareMeters($polygonRings[$i]);
}
$totalSquareMeters += max($outerArea - $holeArea, 0.0);
}
return $totalSquareMeters / 1000000.0;
}
function pointInRing($lng, $lat, $ring) {
if (!is_array($ring) || count($ring) < 4) {
return false;
}
$inside = false;
$lastIndex = count($ring) - 1;
for ($i = 0, $j = $lastIndex; $i <= $lastIndex; $j = $i++) {
$xi = (float)$ring[$i][0];
$yi = (float)$ring[$i][1];
$xj = (float)$ring[$j][0];
$yj = (float)$ring[$j][1];
$intersect = (($yi > $lat) !== ($yj > $lat)) &&
($lng < ($xj - $xi) * ($lat - $yi) / (($yj - $yi) ?: 1e-12) + $xi);
if ($intersect) {
$inside = !$inside;
}
}
return $inside;
}
function pointInGeometry($lng, $lat, $geometry) {
$polygons = geometryRings($geometry);
if (empty($polygons)) {
return false;
}
foreach ($polygons as $polygonRings) {
if (!is_array($polygonRings) || empty($polygonRings)) {
continue;
}
if (!pointInRing($lng, $lat, $polygonRings[0])) {
continue;
}
$insideHole = false;
for ($i = 1; $i < count($polygonRings); $i++) {
if (pointInRing($lng, $lat, $polygonRings[$i])) {
$insideHole = true;
break;
}
}
if (!$insideHole) {
return true;
}
}
return false;
}
function geometryCentroid($geometry) {
$polygons = geometryRings($geometry);
$sumLng = 0.0;
$sumLat = 0.0;
$count = 0;
foreach ($polygons as $polygonRings) {
if (!is_array($polygonRings) || empty($polygonRings) || !is_array($polygonRings[0])) {
continue;
}
foreach ($polygonRings[0] as $point) {
if (is_array($point) && count($point) >= 2) {
$sumLng += (float)$point[0];
$sumLat += (float)$point[1];
$count++;
}
}
}
if ($count === 0) {
return ['lng' => 109.33, 'lat' => -0.02];
}
return [
'lng' => $sumLng / $count,
'lat' => $sumLat / $count
];
}
function haversineMeters($lat1, $lng1, $lat2, $lng2) {
$earthRadius = 6371000.0;
$dLat = deg2rad((float)$lat2 - (float)$lat1);
$dLng = deg2rad((float)$lng2 - (float)$lng1);
$a = sin($dLat / 2) * sin($dLat / 2)
+ cos(deg2rad((float)$lat1)) * cos(deg2rad((float)$lat2))
* sin($dLng / 2) * sin($dLng / 2);
$c = 2 * atan2(sqrt($a), sqrt(max(1 - $a, 0.0)));
return $earthRadius * $c;
}
function roadCentroidLngLat($coordinates) {
if (!is_array($coordinates) || empty($coordinates)) {
return null;
}
$sumLat = 0.0;
$sumLng = 0.0;
$count = 0;
foreach ($coordinates as $pair) {
if (is_array($pair) && count($pair) >= 2 && is_numeric($pair[0]) && is_numeric($pair[1])) {
// Road coordinates are stored as [lat, lng]
$sumLat += (float)$pair[0];
$sumLng += (float)$pair[1];
$count++;
}
}
if ($count === 0) {
return null;
}
return [
'lng' => $sumLng / $count,
'lat' => $sumLat / $count
];
}
function deterministicRoadDensity($populationDensity, $areaId) {
$seed = ($areaId * 1103515245 + 12345) & 0x7fffffff;
$variation = 0.8 + (($seed % 1000) / 1000.0) * 0.6;
$base = 0.9 + (log(max($populationDensity, 1.0) + 1.0) / log(10)) * 1.4;
return round($base * $variation, 6);
}
function deterministicDamagedDensity($roadDensity, $areaId) {
$seed = ($areaId * 214013 + 2531011) & 0x7fffffff;
$ratio = 0.14 + (($seed % 1000) / 1000.0) * 0.18;
return round($roadDensity * $ratio, 6);
}
try {
$db = getDB();
// Get layer type parameter
$layer_type = isset($_GET['layer_type']) ? $_GET['layer_type'] : 'population';
$admin_level = isset($_GET['admin_level']) ? strtolower($_GET['admin_level']) : 'kecamatan';
// Validate layer type
$valid_layers = ['population', 'road_density', 'damaged_roads_density', 'city_boundary'];
if (!in_array($layer_type, $valid_layers)) {
throw new Exception('Invalid layer type. Must be: ' . implode(', ', $valid_layers));
}
// Special handling for city boundary
if ($layer_type === 'city_boundary') {
$stmt = $db->prepare('SELECT id, name, geometry FROM pontianak_areas WHERE area_type = ?');
$stmt->execute(['city']);
$cities = $stmt->fetchAll(PDO::FETCH_ASSOC);
$data_items = [];
foreach ($cities as $city) {
$data_items[] = [
'area_id' => $city['id'],
'area_name' => $city['name'],
'value' => 0, // No numeric value for boundary
'geometry' => json_decode($city['geometry'], true)
];
}
http_response_code(200);
echo json_encode([
'success' => true,
'layer_type' => 'city_boundary',
'data' => $data_items,
'min_value' => 0.0,
'max_value' => 1.0,
'admin_level' => 'city',
'value_source' => 'boundary'
]);
exit;
}
$valid_admin_levels = ['kecamatan', 'kelurahan'];
if (!in_array($admin_level, $valid_admin_levels)) {
throw new Exception('Invalid admin_level. Must be: ' . implode(', ', $valid_admin_levels));
}
// Get Pontianak areas by selected admin level
$stmt = $db->prepare('SELECT id, name, geometry, population FROM pontianak_areas WHERE area_type = ? ORDER BY name');
$stmt->execute([$admin_level]);
$areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($areas)) {
// Fallback to kecamatan when kelurahan data does not exist.
if ($admin_level === 'kelurahan') {
$stmt->execute(['kecamatan']);
$areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
$admin_level = 'kecamatan';
}
}
if (empty($areas)) {
throw new Exception('No Pontianak areas found for selected admin level. Run seed_pontianak_data.php?force=true first.');
}
$areaMetrics = [];
foreach ($areas as $area) {
$geometry = decodeGeometry($area['geometry']);
if ($geometry === null) {
continue;
}
$areaKm2 = geometryAreaKm2($geometry);
if ($areaKm2 <= 0) {
$areaKm2 = 0.01;
}
$population = (float)$area['population'];
$populationDensity = $population / $areaKm2;
$areaMetrics[(int)$area['id']] = [
'id' => (int)$area['id'],
'name' => $area['name'],
'geometry' => $geometry,
'area_km2' => $areaKm2,
'centroid' => geometryCentroid($geometry),
'population' => $population,
'population_density' => $populationDensity
];
}
if (empty($areaMetrics)) {
throw new Exception('Area geometry data is invalid for selected admin level.');
}
$areaRoadCounts = [];
$areaDamagedRoadCounts = [];
foreach ($areaMetrics as $metric) {
$areaRoadCounts[$metric['id']] = 0;
$areaDamagedRoadCounts[$metric['id']] = 0;
}
$roadsExist = false;
$valueSource = 'computed';
if ($layer_type === 'road_density' || $layer_type === 'damaged_roads_density') {
$roadStmt = $db->query('SELECT id, coordinates, condition_status FROM roads');
$roads = $roadStmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($roads as $road) {
$coordinates = json_decode($road['coordinates'], true);
$centroid = roadCentroidLngLat($coordinates);
if ($centroid === null) {
continue;
}
$roadsExist = true;
$assignedAreaId = null;
foreach ($areaMetrics as $areaMetric) {
if (pointInGeometry($centroid['lng'], $centroid['lat'], $areaMetric['geometry'])) {
$assignedAreaId = $areaMetric['id'];
break;
}
}
if ($assignedAreaId === null) {
$nearestDistance = null;
foreach ($areaMetrics as $areaMetric) {
$distance = haversineMeters(
$centroid['lat'],
$centroid['lng'],
$areaMetric['centroid']['lat'],
$areaMetric['centroid']['lng']
);
if ($nearestDistance === null || $distance < $nearestDistance) {
$nearestDistance = $distance;
$assignedAreaId = $areaMetric['id'];
}
}
}
if ($assignedAreaId !== null) {
$areaRoadCounts[$assignedAreaId]++;
if (in_array($road['condition_status'], ['minor_damage', 'major_damage'])) {
$areaDamagedRoadCounts[$assignedAreaId]++;
}
}
}
if (!$roadsExist) {
$valueSource = 'dummy';
}
}
$data_items = [];
$values = [];
foreach ($areaMetrics as $areaMetric) {
$area_id = $areaMetric['id'];
$geometry = $areaMetric['geometry'];
$value = 0;
// Calculate value based on layer type
if ($layer_type === 'population') {
$value = $areaMetric['population_density'];
} else if ($layer_type === 'road_density') {
if ($roadsExist) {
$value = $areaRoadCounts[$area_id] / $areaMetric['area_km2'];
} else {
$value = deterministicRoadDensity($areaMetric['population_density'], $area_id);
}
} else if ($layer_type === 'damaged_roads_density') {
if ($roadsExist) {
$value = $areaDamagedRoadCounts[$area_id] / $areaMetric['area_km2'];
} else {
$fallbackRoadDensity = deterministicRoadDensity($areaMetric['population_density'], $area_id);
$value = deterministicDamagedDensity($fallbackRoadDensity, $area_id);
}
}
$value = (float)$value;
$values[] = $value;
$data_items[] = [
'area_id' => $area_id,
'area_name' => $areaMetric['name'],
'value' => $value,
'geometry' => $geometry,
'area_km2' => round($areaMetric['area_km2'], 6),
'population' => $areaMetric['population'],
'population_density' => round($areaMetric['population_density'], 6),
'value_source' => $valueSource
];
}
// Calculate min and max values
$min_value = count($values) > 0 ? min($values) : 0;
$max_value = count($values) > 0 ? max($values) : 0;
// If all values are same, set reasonable range
if ($min_value === $max_value && $min_value > 0) {
$min_value = $min_value * 0.8;
$max_value = $max_value * 1.2;
} else if ($min_value === $max_value) {
$min_value = 0;
$max_value = 1;
}
// Return success response
http_response_code(200);
echo json_encode([
'success' => true,
'layer_type' => $layer_type,
'data' => $data_items,
'min_value' => floatval($min_value),
'max_value' => floatval($max_value),
'admin_level' => $admin_level,
'value_source' => $valueSource
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>
@@ -0,0 +1,88 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!is_array($data)) {
$data = [];
}
$db = getDB();
$households = [];
if (isset($data['household_id']) && $data['household_id'] !== null && $data['household_id'] !== '') {
if (!is_numeric($data['household_id'])) {
http_response_code(400);
echo json_encode(['error' => 'household_id must be numeric']);
exit;
}
$stmt = $db->prepare('SELECT * FROM households WHERE id = ?');
$stmt->execute([(int)$data['household_id']]);
$household = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$household) {
http_response_code(404);
echo json_encode(['error' => 'Household not found']);
exit;
}
$households[] = $household;
} else {
$stmt = $db->query('SELECT * FROM households ORDER BY id ASC');
$households = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
if (empty($households)) {
echo json_encode([
'success' => true,
'message' => 'No households to score',
'computed_count' => 0,
'scores' => []
]);
exit;
}
$scored = [];
foreach ($households as $household) {
$scoreData = computeHouseholdScores($db, $household);
upsertHouseholdScore($db, $household['id'], $scoreData);
$scored[] = [
'household_id' => (int)$household['id'],
'household_code' => $household['household_code'],
'head_name' => $household['head_name'],
'skr' => $scoreData['skr'],
'sag' => $scoreData['sag'],
'spi' => $scoreData['spi'],
'priority_level' => $scoreData['priority_level']
];
}
usort($scored, function ($a, $b) {
if ($a['spi'] === $b['spi']) {
return 0;
}
return ($a['spi'] < $b['spi']) ? 1 : -1;
});
echo json_encode([
'success' => true,
'computed_count' => count($scored),
'scores' => $scored,
'score_version' => 'v1'
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Check authentication and authorization
requireRole(['Admin']);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
}
$data = json_decode(file_get_contents('php://input'), true);
// Validate input
if (!isset($data['username']) || !isset($data['email']) || !isset($data['password']) || !isset($data['role'])) {
http_response_code(400);
die(json_encode(['error' => 'Missing required fields: username, email, password, role']));
}
$result = createUser($data['username'], $data['email'], $data['password'], $data['role']);
if ($result['success']) {
http_response_code(201);
} else {
http_response_code(400);
}
die(json_encode($result));
@@ -0,0 +1,50 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!is_array($data) || !isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing household id']);
exit;
}
if (!is_numeric($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'id must be numeric']);
exit;
}
$id = (int)$data['id'];
$db = getDB();
$checkStmt = $db->prepare('SELECT id FROM households WHERE id = ?');
$checkStmt->execute([$id]);
if (!$checkStmt->fetch(PDO::FETCH_ASSOC)) {
http_response_code(404);
echo json_encode(['error' => 'Household not found']);
exit;
}
$deleteStmt = $db->prepare('DELETE FROM households WHERE id = ?');
$deleteStmt->execute([$id]);
echo json_encode([
'success' => true,
'deleted_id' => $id
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,51 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing masjid id']);
exit;
}
$id = (int)$data['id'];
$db = getDB();
$checkStmt = $db->prepare('SELECT id FROM masjids WHERE id = ?');
$checkStmt->execute([$id]);
if (!$checkStmt->fetch(PDO::FETCH_ASSOC)) {
http_response_code(404);
echo json_encode(['error' => 'Masjid not found']);
exit;
}
$childStmt = $db->prepare('SELECT id FROM need_points WHERE masjid_id = ?');
$childStmt->execute([$id]);
$childIds = array_map(function($row) {
return (int)$row['id'];
}, $childStmt->fetchAll(PDO::FETCH_ASSOC));
$deleteStmt = $db->prepare('DELETE FROM masjids WHERE id = ?');
$deleteStmt->execute([$id]);
echo json_encode([
'success' => true,
'deleted_need_points' => count($childIds),
'deleted_need_point_ids' => $childIds
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,39 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing point id']);
exit;
}
$id = (int)$data['id'];
$db = getDB();
$deleteStmt = $db->prepare('DELETE FROM need_points WHERE id = ?');
$deleteStmt->execute([$id]);
if ($deleteStmt->rowCount() === 0) {
http_response_code(404);
echo json_encode(['error' => 'Point berkebutuhan not found']);
exit;
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,58 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Check authentication and authorization
requireRole(['Admin']);
if ($_SERVER['REQUEST_METHOD'] !== 'DELETE') {
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
}
$data = json_decode(file_get_contents('php://input'), true);
// Validate input
if (!isset($data['id'])) {
http_response_code(400);
die(json_encode(['error' => 'Missing required field: id']));
}
try {
$db = getDB();
// Prevent deactivating the only admin
$adminCount = $db->query('SELECT COUNT(*) as cnt FROM users WHERE role = \'Admin\' AND is_active = 1')->fetch(PDO::FETCH_ASSOC)['cnt'];
$stmt = $db->prepare('SELECT role FROM users WHERE id = ?');
$stmt->execute([$data['id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
http_response_code(404);
die(json_encode(['error' => 'User not found']));
}
if ($user['role'] === 'Admin' && $adminCount <= 1) {
http_response_code(400);
die(json_encode(['error' => 'Cannot deactivate the only admin user']));
}
// Deactivate user (soft delete)
$updateStmt = $db->prepare('
UPDATE users
SET is_active = 0, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
');
$updateStmt->execute([$data['id']]);
http_response_code(200);
die(json_encode([
'success' => true,
'message' => 'User deactivated successfully'
]));
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['error' => 'Database error: ' . $e->getMessage()]));
}
@@ -0,0 +1,133 @@
<?php
header('Content-Type: application/geo+json');
header('Content-Disposition: attachment; filename="export.geojson"');
require_once 'config.php';
try {
$db = getDB();
// Get type parameter
$type = isset($_GET['type']) ? $_GET['type'] : 'roads';
// Validate type
$valid_types = ['roads', 'parcels', 'pontianak_areas'];
if (!in_array($type, $valid_types)) {
throw new Exception('Invalid type. Must be: ' . implode(', ', $valid_types));
}
$features = [];
if ($type === 'roads') {
// Export roads as LineString features
$stmt = $db->prepare('SELECT id, name, road_type, length_meters, condition_status, coordinates, created_at FROM roads');
$stmt->execute();
$roads = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($roads as $road) {
$coordinates = json_decode($road['coordinates'], true);
// Convert [lat, lng] to [lng, lat] for GeoJSON
$geojson_coords = [];
foreach ($coordinates as $coord) {
$geojson_coords[] = [$coord[1], $coord[0]];
}
$features[] = [
'type' => 'Feature',
'geometry' => [
'type' => 'LineString',
'coordinates' => $geojson_coords
],
'properties' => [
'id' => $road['id'],
'name' => $road['name'],
'road_type' => $road['road_type'],
'length_meters' => floatval($road['length_meters']),
'length_km' => floatval($road['length_meters']) / 1000,
'condition_status' => $road['condition_status'],
'created_at' => $road['created_at']
]
];
}
} else if ($type === 'parcels') {
// Export parcels as Polygon features
$stmt = $db->prepare('SELECT id, name, certificate_type, area_sqm, coordinates, created_at FROM land_parcels');
$stmt->execute();
$parcels = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($parcels as $parcel) {
$coordinates = json_decode($parcel['coordinates'], true);
// Convert [lat, lng] to [lng, lat] for GeoJSON
$geojson_coords = [];
foreach ($coordinates as $coord) {
$geojson_coords[] = [$coord[1], $coord[0]];
}
// Close the polygon
if ($geojson_coords[0] !== end($geojson_coords)) {
$geojson_coords[] = $geojson_coords[0];
}
$features[] = [
'type' => 'Feature',
'geometry' => [
'type' => 'Polygon',
'coordinates' => [$geojson_coords]
],
'properties' => [
'id' => $parcel['id'],
'name' => $parcel['name'],
'certificate_type' => $parcel['certificate_type'],
'area_sqm' => floatval($parcel['area_sqm']),
'area_hectares' => floatval($parcel['area_sqm']) / 10000,
'created_at' => $parcel['created_at']
]
];
}
} else if ($type === 'pontianak_areas') {
// Export Pontianak areas as features
$stmt = $db->prepare('SELECT id, name, area_type, geometry, population FROM pontianak_areas ORDER BY area_type, name');
$stmt->execute();
$areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($areas as $area) {
$geometry = json_decode($area['geometry'], true);
$features[] = [
'type' => 'Feature',
'geometry' => $geometry,
'properties' => [
'id' => $area['id'],
'name' => $area['name'],
'area_type' => $area['area_type'],
'population' => intval($area['population'])
]
];
}
}
// Build FeatureCollection
$geojson = [
'type' => 'FeatureCollection',
'features' => $features
];
// Output GeoJSON
echo json_encode($geojson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>
@@ -0,0 +1,51 @@
<?php
// Backfill missing household fields for existing need_points
header('Content-Type: application/json');
require_once 'config.php';
try {
$db = getDB();
// Fetch points missing any of the new fields
$stmt = $db->query("SELECT id, name FROM need_points WHERE household_name IS NULL OR TRIM(household_name) = '' OR head_name IS NULL OR TRIM(head_name) = '' OR economic_status IS NULL OR TRIM(economic_status) = ''");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) {
echo json_encode(['success' => true, 'updated' => 0, 'message' => 'No need_points required backfill.']);
exit;
}
$updateStmt = $db->prepare('UPDATE need_points SET household_name = ?, head_name = ?, economic_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?');
$firstNames = ['Ahmad','Budi','Siti','Aulia','Rizal','Dewi','Agus','Fitri','Hendra','Yulia'];
$lastNames = ['Santoso','Wijaya','Pratama','Hidayat','Saputra','Nur','Ibrahim','Setiawan','Rahman','Putri'];
$statuses = ['sangat_miskin','miskin','rentan','cukup','baik'];
$db->beginTransaction();
$updated = 0;
$examples = [];
foreach ($rows as $r) {
$id = (int)$r['id'];
$hhName = 'Keluarga ' . substr(md5($r['name'] . $id . microtime(true)), 0, 8);
$headName = $firstNames[array_rand($firstNames)] . ' ' . $lastNames[array_rand($lastNames)];
$econ = $statuses[array_rand($statuses)];
$updateStmt->execute([$hhName, $headName, $econ, $id]);
$updated++;
if (count($examples) < 8) {
$examples[] = ['id' => $id, 'household_name' => $hhName, 'head_name' => $headName, 'economic_status' => $econ];
}
}
$db->commit();
echo json_encode(['success' => true, 'updated' => $updated, 'examples' => $examples]);
} catch (Exception $e) {
if (isset($db) && $db->inTransaction()) $db->rollBack();
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
// If called from CLI, also print a newline
if (php_sapi_name() === 'cli') echo "\n";
?>
@@ -0,0 +1,37 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
try {
$db = getDB();
// Get statistics by area (kelurahan/kecamatan)
$areaStats = $db->query('
SELECT
pa.id,
pa.name as area_name,
pa.area_type,
COUNT(h.id) as household_count,
AVG(hs.skr) as avg_poverty_score,
SUM(CASE WHEN h.verification_status = \'admin_approved\' THEN 1 ELSE 0 END) as verified_count,
SUM(CASE WHEN ad.delivery_status = \'delivered\' THEN 1 ELSE 0 END) as assisted_count
FROM pontianak_areas pa
LEFT JOIN households h ON h.pontianak_area_id = pa.id
LEFT JOIN household_scores hs ON hs.household_id = h.id
LEFT JOIN assistance_distributions ad ON ad.household_id = h.id
GROUP BY pa.id, pa.name, pa.area_type
ORDER BY household_count DESC
')->fetchAll(PDO::FETCH_ASSOC);
$response = [
'success' => true,
'areas' => $areaStats
];
http_response_code(200);
die(json_encode($response));
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['error' => 'Database error: ' . $e->getMessage()]));
}
@@ -0,0 +1,128 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$sql = '
SELECT
ad.*,
h.household_code,
h.head_name,
h.verification_status AS household_verification_status
FROM assistance_distributions ad
JOIN households h ON h.id = ad.household_id
WHERE 1 = 1
';
$params = [];
if (isset($_GET['household_id']) && $_GET['household_id'] !== '') {
if (!is_numeric($_GET['household_id'])) {
http_response_code(400);
echo json_encode(['error' => 'household_id must be numeric']);
exit;
}
$sql .= ' AND ad.household_id = ?';
$params[] = (int)$_GET['household_id'];
}
if (isset($_GET['assistance_type']) && $_GET['assistance_type'] !== '') {
$error = null;
$assistanceType = normalizeAssistanceType($_GET['assistance_type'], $error);
if ($assistanceType === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$sql .= ' AND ad.assistance_type = ?';
$params[] = $assistanceType;
}
if (isset($_GET['delivery_status']) && $_GET['delivery_status'] !== '') {
$error = null;
$deliveryStatus = normalizeDeliveryStatus($_GET['delivery_status'], $error);
if ($deliveryStatus === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$sql .= ' AND ad.delivery_status = ?';
$params[] = $deliveryStatus;
}
if (isset($_GET['date_from']) && $_GET['date_from'] !== '') {
$dateFrom = trim((string)$_GET['date_from']);
$dateObj = DateTime::createFromFormat('Y-m-d', $dateFrom);
if (!$dateObj || $dateObj->format('Y-m-d') !== $dateFrom) {
http_response_code(400);
echo json_encode(['error' => 'date_from must be in YYYY-MM-DD format']);
exit;
}
$sql .= ' AND ad.distribution_date >= ?';
$params[] = $dateFrom;
}
if (isset($_GET['date_to']) && $_GET['date_to'] !== '') {
$dateTo = trim((string)$_GET['date_to']);
$dateObj = DateTime::createFromFormat('Y-m-d', $dateTo);
if (!$dateObj || $dateObj->format('Y-m-d') !== $dateTo) {
http_response_code(400);
echo json_encode(['error' => 'date_to must be in YYYY-MM-DD format']);
exit;
}
$sql .= ' AND ad.distribution_date <= ?';
$params[] = $dateTo;
}
$limit = 200;
if (isset($_GET['limit']) && $_GET['limit'] !== '') {
if (!is_numeric($_GET['limit'])) {
http_response_code(400);
echo json_encode(['error' => 'limit must be numeric']);
exit;
}
$limit = max(1, min((int)$_GET['limit'], 1000));
}
$sql .= ' ORDER BY ad.distribution_date DESC, ad.created_at DESC LIMIT ' . $limit;
$stmt = $db->prepare($sql);
$stmt->execute($params);
$distributions = $stmt->fetchAll(PDO::FETCH_ASSOC);
$summarySql = '
SELECT
COUNT(*) AS total_records,
COALESCE(SUM(assistance_value), 0) AS total_assistance_value,
COALESCE(SUM(CASE WHEN delivery_status = \'delivered\' THEN assistance_value ELSE 0 END), 0) AS delivered_assistance_value
FROM assistance_distributions
';
$summaryStmt = $db->query($summarySql);
$summary = $summaryStmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'count' => count($distributions),
'distributions' => $distributions,
'summary' => $summary
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,49 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
}
try {
$db = getDB();
$stmt = $db->prepare('SELECT key_value FROM score_metadata WHERE key_name = ? LIMIT 1');
$stmt->execute(['target_nasional']);
$targetNasional = $stmt->fetchColumn();
$stmt->execute(['population_reference']);
$populationReference = $stmt->fetchColumn();
if ($targetNasional === false || $targetNasional === null || $targetNasional === '') {
$targetNasional = '7.50';
}
if ($populationReference === false || $populationReference === null || $populationReference === '') {
$populationReference = '288315899';
}
echo json_encode([
'success' => true,
'configuration' => [
'target_nasional' => (float)$targetNasional,
'population_reference' => (int)$populationReference
]
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
?>
@@ -0,0 +1,28 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Handle CORS
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$user = getCurrentUser();
if ($user) {
http_response_code(200);
die(json_encode(['success' => true, 'user' => $user]));
} else {
http_response_code(401);
die(json_encode(['success' => false, 'user' => null]));
}
}
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
@@ -0,0 +1,83 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Get current user to restrict access (optional for MVP)
$user = getCurrentUser();
try {
$db = getDB();
// Get total households
$householdTotal = $db->query('SELECT COUNT(*) as count FROM households')->fetch(PDO::FETCH_ASSOC)['count'];
// Get households by verification status
$statusBreakdown = $db->query('
SELECT verification_status, COUNT(*) as count
FROM households
GROUP BY verification_status
')->fetchAll(PDO::FETCH_ASSOC);
$statusMap = [];
foreach ($statusBreakdown as $row) {
$statusMap[$row['verification_status']] = $row['count'];
}
// Get average SKR (poverty score)
$skrAvg = $db->query('
SELECT
AVG(skr) as avg_skr,
COUNT(*) as scored_count
FROM household_scores
')->fetch(PDO::FETCH_ASSOC);
// Get households assisted
$assistedCount = $db->query('
SELECT COUNT(DISTINCT household_id) as count
FROM assistance_distributions
WHERE delivery_status = \'delivered\'
')->fetch(PDO::FETCH_ASSOC)['count'];
// Get priority level breakdown
$priorityBreakdown = $db->query('
SELECT priority_level, COUNT(*) as count
FROM household_scores
GROUP BY priority_level
')->fetchAll(PDO::FETCH_ASSOC);
$priorityMap = [];
foreach ($priorityBreakdown as $row) {
$priorityMap[$row['priority_level']] = $row['count'];
}
// Get assistance type breakdown
$assistanceTypes = $db->query('
SELECT
assistance_type,
COUNT(*) as count,
SUM(assistance_value) as total_value
FROM assistance_distributions
GROUP BY assistance_type
')->fetchAll(PDO::FETCH_ASSOC);
$response = [
'success' => true,
'summary' => [
'total_households' => $householdTotal,
'avg_poverty_score' => round($skrAvg['avg_skr'] ?? 0, 2),
'households_scored' => $skrAvg['scored_count'] ?? 0,
'households_assisted' => $assistedCount,
'percentage_assisted' => $householdTotal > 0 ? round(($assistedCount / $householdTotal) * 100, 1) : 0
],
'status_breakdown' => $statusMap,
'priority_breakdown' => $priorityMap,
'assistance_by_type' => $assistanceTypes
];
http_response_code(200);
die(json_encode($response));
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['error' => 'Database error: ' . $e->getMessage()]));
}
@@ -0,0 +1,66 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Get current user to restrict access
$user = getCurrentUser();
try {
$db = getDB();
// Get current statistics
$currentStats = $db->query('
SELECT
COUNT(*) as total,
AVG(skr) as avg_score
FROM household_scores
')->fetch(PDO::FETCH_ASSOC);
// Generate realistic placeholder trend data (last 12 months declining trend)
// This simulates poverty reduction over time
$months = [];
$scores = [];
$householdCounts = [];
for ($i = 11; $i >= 0; $i--) {
$date = new DateTime("-$i months");
$months[] = $date->format('M Y');
// Simulate declining poverty trend (improvement over time)
$baseScore = 7.8;
$trend = ($i / 12) * 0.3; // 0-30% improvement
$score = $baseScore * (0.8 + $trend) + (rand(-5, 5) / 100);
$scores[] = round(max(5.0, min(9.0, $score)), 2);
$householdCounts[] = rand(450, 550);
}
$response = [
'success' => true,
'trend' => [
'months' => $months,
'scores' => $scores,
'household_counts' => $householdCounts,
'current_avg' => round($currentStats['avg_score'] ?? 0, 2),
'current_total' => $currentStats['total'] ?? 0,
'note' => 'Trend data is simulated based on current poverty scores'
]
];
http_response_code(200);
die(json_encode($response));
} catch (PDOException $e) {
http_response_code(500);
die(json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]));
} catch (Exception $e) {
http_response_code(400);
die(json_encode([
'success' => false,
'error' => 'Error: ' . $e->getMessage()
]));
}
?>
@@ -0,0 +1,110 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
syncNeedPointsToHouseholds($db);
ensureHouseholdScoresForAllHouseholds($db);
$scoreCountStmt = $db->query('SELECT COUNT(*) AS count FROM household_scores');
$scoreCount = (int)$scoreCountStmt->fetch(PDO::FETCH_ASSOC)['count'];
$sql = '
SELECT
hs.household_id,
hs.skr,
hs.sag,
hs.spi,
hs.priority_level,
hs.score_version,
hs.computed_at,
h.household_code,
h.head_name,
h.economic_status,
h.monthly_income,
h.dependents_count,
h.verification_status,
CASE WHEN EXISTS (
SELECT 1
FROM assistance_distributions ad
WHERE ad.household_id = hs.household_id
AND ad.delivery_status = \'delivered\'
) THEN 1 ELSE 0 END AS assisted,
m.name AS masjid_name,
pa.name AS area_name
FROM household_scores hs
JOIN households h ON h.id = hs.household_id
LEFT JOIN masjids m ON m.id = h.masjid_id
LEFT JOIN pontianak_areas pa ON pa.id = h.pontianak_area_id
WHERE 1 = 1
';
$params = [];
if (isset($_GET['priority_level']) && $_GET['priority_level'] !== '') {
$allowed = ['very_high', 'high', 'medium', 'low'];
$priority = strtolower(trim((string)$_GET['priority_level']));
if (!in_array($priority, $allowed, true)) {
http_response_code(400);
echo json_encode(['error' => 'priority_level is invalid']);
exit;
}
$sql .= ' AND hs.priority_level = ?';
$params[] = $priority;
}
if (isset($_GET['min_spi']) && $_GET['min_spi'] !== '') {
if (!is_numeric($_GET['min_spi'])) {
http_response_code(400);
echo json_encode(['error' => 'min_spi must be numeric']);
exit;
}
$minSpi = (float)$_GET['min_spi'];
$sql .= ' AND hs.spi >= ?';
$params[] = $minSpi;
}
$includeAssisted = isset($_GET['include_assisted']) && $_GET['include_assisted'] !== '' && $_GET['include_assisted'] !== '0';
if (!$includeAssisted) {
$sql .= ' AND NOT EXISTS (SELECT 1 FROM assistance_distributions ad WHERE ad.household_id = hs.household_id AND ad.delivery_status = \'delivered\')';
}
$limit = 100;
if (isset($_GET['limit']) && $_GET['limit'] !== '') {
if (!is_numeric($_GET['limit'])) {
http_response_code(400);
echo json_encode(['error' => 'limit must be numeric']);
exit;
}
$limit = max(1, min((int)$_GET['limit'], 1000));
}
$sql .= ' ORDER BY hs.spi DESC, hs.skr DESC, hs.computed_at DESC LIMIT ' . $limit;
$stmt = $db->prepare($sql);
$stmt->execute($params);
$ranking = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'count' => count($ranking),
'assisted_count' => (int)$db->query("SELECT COUNT(DISTINCT household_id) AS count FROM assistance_distributions WHERE delivery_status = 'delivered'")->fetch(PDO::FETCH_ASSOC)['count'],
'ranking' => $ranking,
'score_version' => 'v1'
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,104 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$syncResult = syncNeedPointsToHouseholds($db);
$computedMissing = ensureHouseholdScoresForAllHouseholds($db);
$totalStmt = $db->query('SELECT COUNT(*) AS total_households FROM households');
$totalHouseholds = (int)$totalStmt->fetch(PDO::FETCH_ASSOC)['total_households'];
$scoreTotalStmt = $db->query('SELECT COUNT(*) AS scored_households FROM household_scores');
$scoredHouseholds = (int)$scoreTotalStmt->fetch(PDO::FETCH_ASSOC)['scored_households'];
$assistedStmt = $db->query("\n SELECT COUNT(DISTINCT household_id) AS assisted_households\n FROM assistance_distributions\n WHERE delivery_status = 'delivered'\n ");
$assistedHouseholds = (int)$assistedStmt->fetch(PDO::FETCH_ASSOC)['assisted_households'];
$priorityStmt = $db->query('
SELECT
priority_level,
COUNT(*) AS count
FROM household_scores hs
JOIN households h ON h.id = hs.household_id
WHERE NOT EXISTS (
SELECT 1
FROM assistance_distributions ad
WHERE ad.household_id = hs.household_id
AND ad.delivery_status = \'delivered\'
)
GROUP BY priority_level
');
$priorityRows = $priorityStmt->fetchAll(PDO::FETCH_ASSOC);
$priorityCounts = [
'very_high' => 0,
'high' => 0,
'medium' => 0,
'low' => 0
];
foreach ($priorityRows as $row) {
$priority = $row['priority_level'];
$priorityCounts[$priority] = (int)$row['count'];
}
$avgStmt = $db->query('
SELECT
COALESCE(AVG(skr), 0) AS avg_skr,
COALESCE(AVG(sag), 0) AS avg_sag,
COALESCE(AVG(spi), 0) AS avg_spi,
MAX(computed_at) AS latest_computed_at
FROM household_scores
');
$averages = $avgStmt->fetch(PDO::FETCH_ASSOC);
$topStmt = $db->query('
SELECT
hs.household_id,
h.household_code,
h.head_name,
hs.spi,
hs.priority_level
FROM household_scores hs
JOIN households h ON h.id = hs.household_id
WHERE NOT EXISTS (
SELECT 1
FROM assistance_distributions ad
WHERE ad.household_id = hs.household_id
AND ad.delivery_status = \'delivered\'
)
ORDER BY hs.spi DESC, hs.skr DESC
LIMIT 5
');
$topPriorities = $topStmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'summary' => [
'total_households' => $totalHouseholds,
'scored_households' => $scoredHouseholds,
'assisted_households' => $assistedHouseholds,
'priority_counts' => $priorityCounts,
'averages' => $averages,
'top_priorities' => $topPriorities,
'score_version' => 'v1',
'sync_result' => $syncResult,
'computed_missing_scores' => $computedMissing
]
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,74 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$totalsStmt = $db->query('
SELECT
COUNT(*) AS total_households,
SUM(CASE WHEN verification_status = \'unverified\' THEN 1 ELSE 0 END) AS unverified_count,
SUM(CASE WHEN verification_status = \'field_verified\' THEN 1 ELSE 0 END) AS field_verified_count,
SUM(CASE WHEN verification_status = \'admin_approved\' THEN 1 ELSE 0 END) AS admin_approved_count,
SUM(CASE WHEN verification_status = \'rejected\' THEN 1 ELSE 0 END) AS rejected_count,
SUM(CASE WHEN verification_status = \'needs_review\' THEN 1 ELSE 0 END) AS needs_review_count,
COALESCE(AVG(monthly_income), 0) AS avg_monthly_income
FROM households
');
$totals = $totalsStmt->fetch(PDO::FETCH_ASSOC);
$assistanceStmt = $db->query('
SELECT
COUNT(*) AS total_distributions,
COUNT(DISTINCT household_id) AS assisted_households,
COALESCE(SUM(assistance_value), 0) AS total_assistance_value,
COALESCE(SUM(CASE WHEN delivery_status = \'delivered\' THEN assistance_value ELSE 0 END), 0) AS delivered_assistance_value
FROM assistance_distributions
');
$assistance = $assistanceStmt->fetch(PDO::FETCH_ASSOC);
$verificationStmt = $db->query('
SELECT
COUNT(*) AS total_verifications,
SUM(CASE WHEN DATE(verified_at) >= DATE(\'now\', \'-7 day\') THEN 1 ELSE 0 END) AS verifications_last_7_days,
COALESCE(AVG(data_confidence_score), 0) AS avg_confidence_score
FROM verification_logs
');
$verification = $verificationStmt->fetch(PDO::FETCH_ASSOC);
$topMasjidStmt = $db->query('
SELECT
m.id,
m.name,
COUNT(h.id) AS household_count
FROM masjids m
LEFT JOIN households h ON h.masjid_id = m.id
GROUP BY m.id, m.name
ORDER BY household_count DESC, m.name ASC
LIMIT 5
');
$topMasjidCoverage = $topMasjidStmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'summary' => [
'totals' => $totals,
'assistance' => $assistance,
'verification' => $verification,
'top_masjid_coverage' => $topMasjidCoverage
]
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,111 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
syncNeedPointsToHouseholds($db);
ensureHouseholdScoresForAllHouseholds($db);
$sql = '
SELECT
h.*,
np.name AS need_point_name,
m.name AS masjid_name,
pa.name AS area_name,
(
SELECT COUNT(*)
FROM assistance_distributions ad
WHERE ad.household_id = h.id
) AS assistance_count,
(
SELECT MAX(vl.verified_at)
FROM verification_logs vl
WHERE vl.household_id = h.id
) AS last_verified_at
FROM households h
LEFT JOIN need_points np ON np.id = h.need_point_id
LEFT JOIN masjids m ON m.id = h.masjid_id
LEFT JOIN pontianak_areas pa ON pa.id = h.pontianak_area_id
WHERE 1 = 1
';
$params = [];
if (isset($_GET['verification_status']) && $_GET['verification_status'] !== '') {
$error = null;
$status = normalizeVerificationStatus($_GET['verification_status'], $error, true);
if ($status === null) {
http_response_code(400);
echo json_encode(['error' => $error]);
exit;
}
$sql .= ' AND h.verification_status = ?';
$params[] = $status;
}
if (isset($_GET['masjid_id']) && $_GET['masjid_id'] !== '') {
if (!is_numeric($_GET['masjid_id'])) {
http_response_code(400);
echo json_encode(['error' => 'masjid_id must be numeric']);
exit;
}
$sql .= ' AND h.masjid_id = ?';
$params[] = (int)$_GET['masjid_id'];
}
if (isset($_GET['pontianak_area_id']) && $_GET['pontianak_area_id'] !== '') {
if (!is_numeric($_GET['pontianak_area_id'])) {
http_response_code(400);
echo json_encode(['error' => 'pontianak_area_id must be numeric']);
exit;
}
$sql .= ' AND h.pontianak_area_id = ?';
$params[] = (int)$_GET['pontianak_area_id'];
}
if (isset($_GET['q']) && trim((string)$_GET['q']) !== '') {
$q = '%' . trim((string)$_GET['q']) . '%';
$sql .= ' AND (h.household_code LIKE ? OR h.head_name LIKE ?)';
$params[] = $q;
$params[] = $q;
}
$limit = 200;
if (isset($_GET['limit']) && $_GET['limit'] !== '') {
if (!is_numeric($_GET['limit'])) {
http_response_code(400);
echo json_encode(['error' => 'limit must be numeric']);
exit;
}
$limit = max(1, min((int)$_GET['limit'], 1000));
}
$sql .= ' ORDER BY h.created_at DESC LIMIT ' . $limit;
$stmt = $db->prepare($sql);
$stmt->execute($params);
$households = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'count' => count($households),
'households' => $households
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,41 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query('
SELECT
m.id,
m.name,
m.latitude,
m.longitude,
m.radius_meters,
m.created_at,
m.updated_at,
COUNT(np.id) AS need_point_count
FROM masjids m
LEFT JOIN need_points np ON np.masjid_id = m.id
GROUP BY m.id
ORDER BY m.created_at DESC
');
$masjids = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'masjids' => $masjids
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,36 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query("\n SELECT\n np.id,\n np.name,\n np.latitude,\n np.longitude,\n np.masjid_id,\n np.household_name,\n np.head_name,\n np.economic_status,\n np.is_verified,\n CASE WHEN EXISTS (\n SELECT 1\n FROM households h\n JOIN assistance_distributions ad ON ad.household_id = h.id\n WHERE h.need_point_id = np.id\n AND ad.delivery_status = 'delivered'\n ) THEN 1 ELSE 0 END AS assisted,\n np.created_at,\n np.updated_at,\n m.name AS masjid_name,\n m.latitude AS masjid_latitude,\n m.longitude AS masjid_longitude,\n m.radius_meters\n FROM need_points np\n JOIN masjids m ON m.id = np.masjid_id\n ORDER BY np.created_at DESC\n ");
$points = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($points as &$point) {
$point['distance_meters'] = haversineDistanceMeters(
(float)$point['latitude'],
(float)$point['longitude'],
(float)$point['masjid_latitude'],
(float)$point['masjid_longitude']
);
}
unset($point);
echo json_encode([
'success' => true,
'points' => $points
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
@@ -0,0 +1,50 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
try {
$db = getDB();
// Get optional filter parameter
$area_type = isset($_GET['area_type']) ? $_GET['area_type'] : '';
// Build SQL query
$sql = 'SELECT id, name, area_type, geometry, population, created_at, updated_at FROM pontianak_areas';
if ($area_type) {
$sql .= ' WHERE area_type = ?';
$stmt = $db->prepare($sql);
$stmt->execute([$area_type]);
} else {
$stmt = $db->prepare($sql);
$stmt->execute();
}
$areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decode geometry JSON for each area
foreach ($areas as &$area) {
$area['geometry'] = json_decode($area['geometry'], true);
}
// Return success response
http_response_code(200);
echo json_encode([
'success' => true,
'areas' => $areas
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Database error: ' . $e->getMessage()
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>
@@ -0,0 +1,33 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
// Check authentication and authorization
requireRole(['Admin']);
try {
$db = getDB();
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Get all users
$stmt = $db->query('
SELECT id, username, email, role, is_active, created_at
FROM users
ORDER BY created_at DESC
');
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
http_response_code(200);
die(json_encode([
'success' => true,
'users' => $users
]));
}
http_response_code(405);
die(json_encode(['error' => 'Method not allowed']));
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['error' => 'Database error: ' . $e->getMessage()]));
}

Some files were not shown because too many files have changed in this diff Show More