1294 lines
44 KiB
PHP
1294 lines
44 KiB
PHP
<?php
|
|
// Session configuration
|
|
session_start();
|
|
|
|
// Database configuration
|
|
define('DB_PATH', __DIR__ . '/../db/markers.db');
|
|
|
|
// Create db directory if it doesn't exist
|
|
if (!is_dir(__DIR__ . '/../db')) {
|
|
mkdir(__DIR__ . '/../db', 0755, true);
|
|
}
|
|
|
|
// Connect to SQLite database
|
|
function getDB() {
|
|
try {
|
|
$db = new PDO('sqlite:' . DB_PATH);
|
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
$db->exec('PRAGMA foreign_keys = ON');
|
|
|
|
// Initialize database schema
|
|
initializeDB($db);
|
|
|
|
return $db;
|
|
} catch (PDOException $e) {
|
|
die(json_encode(['error' => 'Database connection failed: ' . $e->getMessage()]));
|
|
}
|
|
}
|
|
|
|
// Initialize database schema
|
|
function initializeDB($db) {
|
|
// Create markers table for points
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS markers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(255) NOT NULL,
|
|
nomor_spbu VARCHAR(100),
|
|
latitude REAL NOT NULL,
|
|
longitude REAL NOT NULL,
|
|
open_24_hours BOOLEAN DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
');
|
|
|
|
// Add nomor_spbu column to existing databases if it doesn't exist
|
|
try {
|
|
$db->exec('PRAGMA table_info(markers)');
|
|
$columns = $db->query('PRAGMA table_info(markers)')->fetchAll(PDO::FETCH_ASSOC);
|
|
$hasNomorSpbu = false;
|
|
|
|
foreach ($columns as $col) {
|
|
if ($col['name'] === 'nomor_spbu') {
|
|
$hasNomorSpbu = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$hasNomorSpbu) {
|
|
$db->exec('ALTER TABLE markers ADD COLUMN nomor_spbu VARCHAR(100)');
|
|
}
|
|
} catch (PDOException $e) {
|
|
// Table already has the column or other non-critical error
|
|
}
|
|
|
|
// Create roads table for polylines
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS roads (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(255) NOT NULL,
|
|
road_type VARCHAR(50) NOT NULL CHECK(road_type IN (\'Nasional\', \'Provinsi\', \'Kabupaten\')),
|
|
coordinates TEXT NOT NULL,
|
|
length_meters REAL NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
');
|
|
|
|
// Create indexes for roads table
|
|
try {
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_roads_road_type ON roads(road_type)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_roads_created_at ON roads(created_at)');
|
|
} catch (PDOException $e) {
|
|
// Indexes may already exist
|
|
}
|
|
|
|
// Create land_parcels table for polygons
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS land_parcels (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(255) NOT NULL,
|
|
certificate_type VARCHAR(50) NOT NULL CHECK(certificate_type IN (\'SHM\', \'HGB\', \'HGU\', \'HP\')),
|
|
coordinates TEXT NOT NULL,
|
|
area_sqm REAL NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
');
|
|
|
|
// Create indexes for land_parcels table
|
|
try {
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_parcels_cert_type ON land_parcels(certificate_type)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_parcels_created_at ON land_parcels(created_at)');
|
|
} catch (PDOException $e) {
|
|
// Indexes may already exist
|
|
}
|
|
|
|
// Add condition_status column to roads table if it doesn't exist
|
|
try {
|
|
$columns = $db->query('PRAGMA table_info(roads)')->fetchAll(PDO::FETCH_ASSOC);
|
|
$hasConditionStatus = false;
|
|
|
|
foreach ($columns as $col) {
|
|
if ($col['name'] === 'condition_status') {
|
|
$hasConditionStatus = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$hasConditionStatus) {
|
|
$db->exec('ALTER TABLE roads ADD COLUMN condition_status VARCHAR(20) DEFAULT \'normal\' CHECK(condition_status IN (\'normal\', \'minor_damage\', \'major_damage\'))');
|
|
}
|
|
} catch (PDOException $e) {
|
|
// Column may already exist
|
|
}
|
|
|
|
// Create pontianak_areas table for choropleth boundaries
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS pontianak_areas (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(255) NOT NULL,
|
|
area_type VARCHAR(50) NOT NULL CHECK(area_type IN (\'kelurahan\', \'kecamatan\', \'city\')),
|
|
geometry TEXT NOT NULL,
|
|
population INT DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(name, area_type)
|
|
)
|
|
');
|
|
|
|
// Create indexes for pontianak_areas table
|
|
try {
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_pontianak_areas_type ON pontianak_areas(area_type)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_pontianak_areas_name ON pontianak_areas(name)');
|
|
} catch (PDOException $e) {
|
|
// Indexes may already exist
|
|
}
|
|
|
|
// Create area_statistics table for computed densities
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS area_statistics (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
pontianak_area_id INTEGER NOT NULL,
|
|
statistic_type VARCHAR(50) NOT NULL CHECK(statistic_type IN (\'population_density\', \'road_density\', \'damaged_roads_density\')),
|
|
value REAL NOT NULL,
|
|
data_date DATE NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY(pontianak_area_id) REFERENCES pontianak_areas(id) ON DELETE CASCADE,
|
|
UNIQUE(pontianak_area_id, statistic_type, data_date)
|
|
)
|
|
');
|
|
|
|
// Create indexes for area_statistics table
|
|
try {
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_stats_area_type ON area_statistics(pontianak_area_id, statistic_type)');
|
|
} catch (PDOException $e) {
|
|
// Indexes may already exist
|
|
}
|
|
|
|
// Create masjids table
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS masjids (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(255) NOT NULL,
|
|
latitude REAL NOT NULL,
|
|
longitude REAL NOT NULL,
|
|
radius_meters REAL NOT NULL DEFAULT 500,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
');
|
|
|
|
// Create indexes for masjids table
|
|
try {
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_masjids_created_at ON masjids(created_at)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_masjids_name ON masjids(name)');
|
|
} catch (PDOException $e) {
|
|
// Indexes may already exist
|
|
}
|
|
|
|
// Create special needs points table
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS need_points (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name VARCHAR(255) NOT NULL,
|
|
latitude REAL NOT NULL,
|
|
longitude REAL NOT NULL,
|
|
masjid_id INTEGER NOT NULL,
|
|
household_name VARCHAR(255),
|
|
head_name VARCHAR(255),
|
|
economic_status VARCHAR(50),
|
|
is_verified INTEGER NOT NULL DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY(masjid_id) REFERENCES masjids(id) ON DELETE CASCADE
|
|
)
|
|
');
|
|
|
|
// Create indexes for need_points table
|
|
try {
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_need_points_masjid_id ON need_points(masjid_id)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_need_points_created_at ON need_points(created_at)');
|
|
} catch (PDOException $e) {
|
|
// Indexes may already exist
|
|
}
|
|
|
|
// Add new columns to existing need_points table if they don't exist
|
|
try {
|
|
$columns = $db->query('PRAGMA table_info(need_points)')->fetchAll(PDO::FETCH_ASSOC);
|
|
$hasHouseholdName = false;
|
|
$hasHeadName = false;
|
|
$hasEconomic = false;
|
|
$hasVerified = false;
|
|
foreach ($columns as $col) {
|
|
if ($col['name'] === 'household_name') $hasHouseholdName = true;
|
|
if ($col['name'] === 'head_name') $hasHeadName = true;
|
|
if ($col['name'] === 'economic_status') $hasEconomic = true;
|
|
if ($col['name'] === 'is_verified') $hasVerified = true;
|
|
}
|
|
if (!$hasHouseholdName) {
|
|
$db->exec('ALTER TABLE need_points ADD COLUMN household_name VARCHAR(255)');
|
|
}
|
|
if (!$hasHeadName) {
|
|
$db->exec('ALTER TABLE need_points ADD COLUMN head_name VARCHAR(255)');
|
|
}
|
|
if (!$hasEconomic) {
|
|
$db->exec('ALTER TABLE need_points ADD COLUMN economic_status VARCHAR(50)');
|
|
}
|
|
if (!$hasVerified) {
|
|
$db->exec('ALTER TABLE need_points ADD COLUMN is_verified INTEGER NOT NULL DEFAULT 0');
|
|
}
|
|
} catch (PDOException $e) {
|
|
// non-critical
|
|
}
|
|
|
|
// Create households table for poverty alleviation base data
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS households (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
household_code VARCHAR(100) NOT NULL UNIQUE,
|
|
head_name VARCHAR(255) NOT NULL,
|
|
nik_hash VARCHAR(255),
|
|
latitude REAL NOT NULL,
|
|
longitude REAL NOT NULL,
|
|
need_point_id INTEGER UNIQUE,
|
|
masjid_id INTEGER,
|
|
pontianak_area_id INTEGER,
|
|
monthly_income REAL NOT NULL DEFAULT 0,
|
|
dependents_count INTEGER NOT NULL DEFAULT 0,
|
|
housing_condition_score INTEGER NOT NULL DEFAULT 3 CHECK(housing_condition_score BETWEEN 1 AND 5),
|
|
employment_status VARCHAR(50) NOT NULL DEFAULT \'unknown\' CHECK(employment_status IN (\'formal\', \'informal\', \'unemployed\', \'daily_worker\', \'micro_business\', \'retired\', \'unknown\')),
|
|
economic_status VARCHAR(50) NOT NULL DEFAULT \'unknown\' CHECK(economic_status IN (\'sangat_miskin\', \'miskin\', \'rentan\', \'cukup\', \'baik\', \'unknown\')),
|
|
verification_status VARCHAR(30) NOT NULL DEFAULT \'unverified\' CHECK(verification_status IN (\'unverified\', \'field_verified\', \'admin_approved\', \'rejected\', \'needs_review\')),
|
|
vulnerability_notes TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY(need_point_id) REFERENCES need_points(id) ON DELETE SET NULL,
|
|
FOREIGN KEY(masjid_id) REFERENCES masjids(id) ON DELETE SET NULL,
|
|
FOREIGN KEY(pontianak_area_id) REFERENCES pontianak_areas(id) ON DELETE SET NULL
|
|
)
|
|
');
|
|
|
|
// Create indexes for households table
|
|
try {
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_households_code ON households(household_code)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_households_verification_status ON households(verification_status)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_households_masjid_id ON households(masjid_id)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_households_area_id ON households(pontianak_area_id)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_households_created_at ON households(created_at)');
|
|
} catch (PDOException $e) {
|
|
// Indexes may already exist
|
|
}
|
|
|
|
// Add economic_status column to existing households table if it doesn't exist
|
|
try {
|
|
$columns = $db->query('PRAGMA table_info(households)')->fetchAll(PDO::FETCH_ASSOC);
|
|
$hasEconomicStatus = false;
|
|
foreach ($columns as $col) {
|
|
if ($col['name'] === 'economic_status') {
|
|
$hasEconomicStatus = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$hasEconomicStatus) {
|
|
$db->exec("ALTER TABLE households ADD COLUMN economic_status VARCHAR(50) NOT NULL DEFAULT 'unknown'");
|
|
}
|
|
} catch (PDOException $e) {
|
|
// non-critical
|
|
}
|
|
|
|
// Create assistance distributions table
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS assistance_distributions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
household_id INTEGER NOT NULL,
|
|
assistance_type VARCHAR(50) NOT NULL CHECK(assistance_type IN (\'pangan\', \'tunai\', \'kesehatan\', \'pendidikan\', \'usaha_mikro\', \'lainnya\')),
|
|
assistance_value REAL NOT NULL DEFAULT 0,
|
|
assistance_frequency VARCHAR(30) NOT NULL DEFAULT \'sekali\' CHECK(assistance_frequency IN (\'sekali\', \'mingguan\', \'bulanan\', \'triwulanan\', \'tahunan\', \'insidental\')),
|
|
distribution_date DATE NOT NULL,
|
|
delivery_status VARCHAR(30) NOT NULL DEFAULT \'pending\' CHECK(delivery_status IN (\'pending\', \'delivered\', \'failed\')),
|
|
delivered_by VARCHAR(255),
|
|
notes TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY(household_id) REFERENCES households(id) ON DELETE CASCADE
|
|
)
|
|
');
|
|
|
|
// Create indexes for assistance distributions table
|
|
try {
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_assistance_household_id ON assistance_distributions(household_id)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_assistance_date ON assistance_distributions(distribution_date)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_assistance_status ON assistance_distributions(delivery_status)');
|
|
} catch (PDOException $e) {
|
|
// Indexes may already exist
|
|
}
|
|
|
|
// Create verification logs table
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS verification_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
household_id INTEGER NOT NULL,
|
|
verification_status VARCHAR(30) NOT NULL CHECK(verification_status IN (\'field_verified\', \'admin_approved\', \'rejected\', \'needs_review\')),
|
|
verifier_name VARCHAR(255) NOT NULL,
|
|
verifier_role VARCHAR(100),
|
|
field_note TEXT,
|
|
evidence_photo_count INTEGER NOT NULL DEFAULT 0,
|
|
data_confidence_score REAL,
|
|
verified_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY(household_id) REFERENCES households(id) ON DELETE CASCADE
|
|
)
|
|
');
|
|
|
|
// Create indexes for verification logs table
|
|
try {
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_verification_household_id ON verification_logs(household_id)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_verification_status ON verification_logs(verification_status)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_verification_verified_at ON verification_logs(verified_at)');
|
|
} catch (PDOException $e) {
|
|
// Indexes may already exist
|
|
}
|
|
|
|
// Create household scores table (Option B)
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS household_scores (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
household_id INTEGER NOT NULL UNIQUE,
|
|
skr REAL NOT NULL DEFAULT 0,
|
|
sag REAL NOT NULL DEFAULT 0,
|
|
spi REAL NOT NULL DEFAULT 0,
|
|
priority_level VARCHAR(30) NOT NULL DEFAULT \'low\' CHECK(priority_level IN (\'very_high\', \'high\', \'medium\', \'low\')),
|
|
score_version VARCHAR(30) NOT NULL DEFAULT \'v1\',
|
|
components_json TEXT,
|
|
computed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY(household_id) REFERENCES households(id) ON DELETE CASCADE
|
|
)
|
|
');
|
|
|
|
// Create indexes for household_scores table
|
|
try {
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_household_scores_spi ON household_scores(spi DESC)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_household_scores_priority ON household_scores(priority_level)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_household_scores_computed_at ON household_scores(computed_at)');
|
|
} catch (PDOException $e) {
|
|
// Indexes may already exist
|
|
}
|
|
|
|
// Create score metadata table for versioning/tuning
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS score_metadata (
|
|
key_name VARCHAR(100) PRIMARY KEY,
|
|
key_value TEXT NOT NULL,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
');
|
|
|
|
// Create users table for authentication
|
|
$db->exec('
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username VARCHAR(100) NOT NULL UNIQUE,
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
role VARCHAR(50) NOT NULL DEFAULT \'Petugas\' CHECK(role IN (\'Admin\', \'Petugas\', \'Pimpinan\')),
|
|
is_active BOOLEAN NOT NULL DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
');
|
|
|
|
// Create indexes for users table
|
|
try {
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)');
|
|
} catch (PDOException $e) {
|
|
// Indexes may already exist
|
|
}
|
|
|
|
// Initialize default admin user
|
|
try {
|
|
$adminCount = $db->query('SELECT COUNT(*) as cnt FROM users WHERE role = \'Admin\'')->fetch(PDO::FETCH_ASSOC);
|
|
if ($adminCount['cnt'] == 0) {
|
|
$adminPassword = password_hash('admin123', PASSWORD_BCRYPT, ['cost' => 12]);
|
|
$stmt = $db->prepare('INSERT INTO users (username, email, password_hash, role, is_active) VALUES (?, ?, ?, ?, 1)');
|
|
$stmt->execute(['admin', 'admin@system.local', $adminPassword, 'Admin']);
|
|
}
|
|
} catch (PDOException $e) {
|
|
// User may already exist
|
|
}
|
|
|
|
// Initialize default score metadata values
|
|
try {
|
|
$defaultMeta = [
|
|
'score_version' => 'v1',
|
|
'skr_weights' => '{"I":0.25,"E":0.20,"D":0.15,"H":0.15,"A":0.15,"S":0.10}',
|
|
'sag_weights' => '{"J":0.40,"R":0.35,"T":0.25}',
|
|
'spi_weights' => '{"SKR":0.60,"SAG_INV":0.30,"K":0.10}',
|
|
'income_max_reference' => '5000000',
|
|
'distance_max_reference' => '2000',
|
|
'target_nasional' => '7.50',
|
|
'population_reference' => '288315899'
|
|
];
|
|
|
|
$metaStmt = $db->prepare('INSERT OR IGNORE INTO score_metadata (key_name, key_value) VALUES (?, ?)');
|
|
foreach ($defaultMeta as $key => $value) {
|
|
$metaStmt->execute([$key, $value]);
|
|
}
|
|
} catch (PDOException $e) {
|
|
// Metadata init may already exist
|
|
}
|
|
}
|
|
|
|
function haversineDistanceMeters($lat1, $lng1, $lat2, $lng2) {
|
|
$earthRadius = 6371000; // meters
|
|
|
|
$lat1Rad = deg2rad((float)$lat1);
|
|
$lat2Rad = deg2rad((float)$lat2);
|
|
$dLat = deg2rad((float)$lat2 - (float)$lat1);
|
|
$dLng = deg2rad((float)$lng2 - (float)$lng1);
|
|
|
|
$a = sin($dLat / 2) * sin($dLat / 2) +
|
|
cos($lat1Rad) * cos($lat2Rad) * sin($dLng / 2) * sin($dLng / 2);
|
|
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
|
|
|
return $earthRadius * $c;
|
|
}
|
|
|
|
function normalizeMasjidRadius($radiusValue, &$error = null) {
|
|
$error = null;
|
|
|
|
if ($radiusValue === null || $radiusValue === '') {
|
|
return 500.0;
|
|
}
|
|
|
|
if (!is_numeric($radiusValue)) {
|
|
$error = 'Radius must be a numeric value';
|
|
return null;
|
|
}
|
|
|
|
$radius = (float)$radiusValue;
|
|
if ($radius < 50 || $radius > 5000) {
|
|
$error = 'Radius must be between 50 and 5000 meters';
|
|
return null;
|
|
}
|
|
|
|
return $radius;
|
|
}
|
|
|
|
function findNearestMasjidCoveringPoint($db, $latitude, $longitude) {
|
|
$stmt = $db->query('SELECT id, name, latitude, longitude, radius_meters FROM masjids');
|
|
$masjids = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$nearest = null;
|
|
$nearestDistance = null;
|
|
|
|
foreach ($masjids as $masjid) {
|
|
$distance = haversineDistanceMeters(
|
|
(float)$latitude,
|
|
(float)$longitude,
|
|
(float)$masjid['latitude'],
|
|
(float)$masjid['longitude']
|
|
);
|
|
|
|
if ($distance <= (float)$masjid['radius_meters']) {
|
|
if ($nearestDistance === null || $distance < $nearestDistance) {
|
|
$nearest = $masjid;
|
|
$nearestDistance = $distance;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($nearest === null) {
|
|
return null;
|
|
}
|
|
|
|
$nearest['distance_meters'] = $nearestDistance;
|
|
return $nearest;
|
|
}
|
|
|
|
function getNeedPointWithMasjid($db, $id) {
|
|
$stmt = $db->prepare('
|
|
SELECT
|
|
np.id,
|
|
np.name,
|
|
np.latitude,
|
|
np.longitude,
|
|
np.masjid_id,
|
|
np.household_name,
|
|
np.head_name,
|
|
np.economic_status,
|
|
np.is_verified,
|
|
np.created_at,
|
|
m.name AS masjid_name,
|
|
m.latitude AS masjid_latitude,
|
|
m.longitude AS masjid_longitude,
|
|
m.radius_meters
|
|
FROM need_points np
|
|
JOIN masjids m ON m.id = np.masjid_id
|
|
WHERE np.id = ?
|
|
');
|
|
$stmt->execute([$id]);
|
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
function cleanupNeedPointsOutsideMasjidCoverage($db, $masjidId) {
|
|
$masjidStmt = $db->prepare('SELECT id, latitude, longitude, radius_meters FROM masjids WHERE id = ?');
|
|
$masjidStmt->execute([$masjidId]);
|
|
$masjid = $masjidStmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$masjid) {
|
|
return [
|
|
'deleted_count' => 0,
|
|
'deleted_ids' => []
|
|
];
|
|
}
|
|
|
|
$pointsStmt = $db->prepare('SELECT id, latitude, longitude FROM need_points WHERE masjid_id = ?');
|
|
$pointsStmt->execute([$masjidId]);
|
|
$points = $pointsStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$idsToDelete = [];
|
|
foreach ($points as $point) {
|
|
$distance = haversineDistanceMeters(
|
|
(float)$point['latitude'],
|
|
(float)$point['longitude'],
|
|
(float)$masjid['latitude'],
|
|
(float)$masjid['longitude']
|
|
);
|
|
|
|
if ($distance > (float)$masjid['radius_meters']) {
|
|
$idsToDelete[] = (int)$point['id'];
|
|
}
|
|
}
|
|
|
|
if (!empty($idsToDelete)) {
|
|
$placeholders = implode(',', array_fill(0, count($idsToDelete), '?'));
|
|
$deleteStmt = $db->prepare("DELETE FROM need_points WHERE id IN ($placeholders)");
|
|
$deleteStmt->execute($idsToDelete);
|
|
}
|
|
|
|
return [
|
|
'deleted_count' => count($idsToDelete),
|
|
'deleted_ids' => $idsToDelete
|
|
];
|
|
}
|
|
|
|
function validateCoordinates($latitude, $longitude, &$error = null) {
|
|
$error = null;
|
|
|
|
if (!is_numeric($latitude) || !is_numeric($longitude)) {
|
|
$error = 'Latitude and longitude must be numeric values';
|
|
return false;
|
|
}
|
|
|
|
$lat = (float)$latitude;
|
|
$lng = (float)$longitude;
|
|
|
|
if ($lat < -90 || $lat > 90) {
|
|
$error = 'Latitude must be between -90 and 90';
|
|
return false;
|
|
}
|
|
|
|
if ($lng < -180 || $lng > 180) {
|
|
$error = 'Longitude must be between -180 and 180';
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function normalizeHouseholdCode($db, $inputCode = null) {
|
|
$code = trim((string)($inputCode ?? ''));
|
|
if ($code !== '') {
|
|
return $code;
|
|
}
|
|
|
|
do {
|
|
try {
|
|
$random = strtoupper(substr(bin2hex(random_bytes(3)), 0, 6));
|
|
} catch (Exception $e) {
|
|
$random = strtoupper(substr(md5((string)mt_rand()), 0, 6));
|
|
}
|
|
|
|
$code = 'HH-' . date('Ymd') . '-' . $random;
|
|
$checkStmt = $db->prepare('SELECT id FROM households WHERE household_code = ? LIMIT 1');
|
|
$checkStmt->execute([$code]);
|
|
$exists = $checkStmt->fetch(PDO::FETCH_ASSOC);
|
|
} while ($exists);
|
|
|
|
return $code;
|
|
}
|
|
|
|
function normalizeMonthlyIncome($value, &$error = null) {
|
|
$error = null;
|
|
if ($value === null || $value === '') {
|
|
return 0.0;
|
|
}
|
|
|
|
if (!is_numeric($value)) {
|
|
$error = 'Monthly income must be numeric';
|
|
return null;
|
|
}
|
|
|
|
$income = (float)$value;
|
|
if ($income < 0 || $income > 1000000000) {
|
|
$error = 'Monthly income must be between 0 and 1000000000';
|
|
return null;
|
|
}
|
|
|
|
return $income;
|
|
}
|
|
|
|
function normalizeDependentsCount($value, &$error = null) {
|
|
$error = null;
|
|
if ($value === null || $value === '') {
|
|
return 0;
|
|
}
|
|
|
|
if (!is_numeric($value)) {
|
|
$error = 'Dependents count must be numeric';
|
|
return null;
|
|
}
|
|
|
|
$count = (int)$value;
|
|
if ($count < 0 || $count > 50) {
|
|
$error = 'Dependents count must be between 0 and 50';
|
|
return null;
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
function normalizeHousingConditionScore($value, &$error = null) {
|
|
$error = null;
|
|
if ($value === null || $value === '') {
|
|
return 3;
|
|
}
|
|
|
|
if (!is_numeric($value)) {
|
|
$error = 'Housing condition score must be numeric';
|
|
return null;
|
|
}
|
|
|
|
$score = (int)$value;
|
|
if ($score < 1 || $score > 5) {
|
|
$error = 'Housing condition score must be between 1 and 5';
|
|
return null;
|
|
}
|
|
|
|
return $score;
|
|
}
|
|
|
|
function normalizeEmploymentStatus($value, &$error = null) {
|
|
$error = null;
|
|
$allowed = ['formal', 'informal', 'unemployed', 'daily_worker', 'micro_business', 'retired', 'unknown'];
|
|
|
|
if ($value === null || $value === '') {
|
|
return 'unknown';
|
|
}
|
|
|
|
$status = strtolower(trim((string)$value));
|
|
if (!in_array($status, $allowed, true)) {
|
|
$error = 'Employment status is invalid';
|
|
return null;
|
|
}
|
|
|
|
return $status;
|
|
}
|
|
|
|
function normalizeVerificationStatus($value, &$error = null, $allowUnverified = true) {
|
|
$error = null;
|
|
$allowed = ['field_verified', 'admin_approved', 'rejected', 'needs_review'];
|
|
if ($allowUnverified) {
|
|
$allowed[] = 'unverified';
|
|
}
|
|
|
|
if ($value === null || $value === '') {
|
|
return $allowUnverified ? 'unverified' : 'needs_review';
|
|
}
|
|
|
|
$status = strtolower(trim((string)$value));
|
|
if (!in_array($status, $allowed, true)) {
|
|
$error = 'Verification status is invalid';
|
|
return null;
|
|
}
|
|
|
|
return $status;
|
|
}
|
|
|
|
function normalizeAssistanceType($value, &$error = null) {
|
|
$error = null;
|
|
$allowed = ['pangan', 'tunai', 'kesehatan', 'pendidikan', 'usaha_mikro', 'lainnya'];
|
|
|
|
if ($value === null || $value === '') {
|
|
$error = 'Assistance type is required';
|
|
return null;
|
|
}
|
|
|
|
$type = strtolower(trim((string)$value));
|
|
if (!in_array($type, $allowed, true)) {
|
|
$error = 'Assistance type is invalid';
|
|
return null;
|
|
}
|
|
|
|
return $type;
|
|
}
|
|
|
|
function normalizeAssistanceFrequency($value, &$error = null) {
|
|
$error = null;
|
|
$allowed = ['sekali', 'mingguan', 'bulanan', 'triwulanan', 'tahunan', 'insidental'];
|
|
|
|
if ($value === null || $value === '') {
|
|
return 'sekali';
|
|
}
|
|
|
|
$frequency = strtolower(trim((string)$value));
|
|
if (!in_array($frequency, $allowed, true)) {
|
|
$error = 'Assistance frequency is invalid';
|
|
return null;
|
|
}
|
|
|
|
return $frequency;
|
|
}
|
|
|
|
function normalizeDeliveryStatus($value, &$error = null) {
|
|
$error = null;
|
|
$allowed = ['pending', 'delivered', 'failed'];
|
|
|
|
if ($value === null || $value === '') {
|
|
return 'pending';
|
|
}
|
|
|
|
$status = strtolower(trim((string)$value));
|
|
if (!in_array($status, $allowed, true)) {
|
|
$error = 'Delivery status is invalid';
|
|
return null;
|
|
}
|
|
|
|
return $status;
|
|
}
|
|
|
|
function normalizeAssistanceValue($value, &$error = null) {
|
|
$error = null;
|
|
if ($value === null || $value === '') {
|
|
return 0.0;
|
|
}
|
|
|
|
if (!is_numeric($value)) {
|
|
$error = 'Assistance value must be numeric';
|
|
return null;
|
|
}
|
|
|
|
$assistanceValue = (float)$value;
|
|
if ($assistanceValue < 0 || $assistanceValue > 1000000000) {
|
|
$error = 'Assistance value must be between 0 and 1000000000';
|
|
return null;
|
|
}
|
|
|
|
return $assistanceValue;
|
|
}
|
|
|
|
function normalizeEvidencePhotoCount($value, &$error = null) {
|
|
$error = null;
|
|
if ($value === null || $value === '') {
|
|
return 0;
|
|
}
|
|
|
|
if (!is_numeric($value)) {
|
|
$error = 'Evidence photo count must be numeric';
|
|
return null;
|
|
}
|
|
|
|
$count = (int)$value;
|
|
if ($count < 0 || $count > 100) {
|
|
$error = 'Evidence photo count must be between 0 and 100';
|
|
return null;
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
function normalizeDataConfidenceScore($value, &$error = null) {
|
|
$error = null;
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
if (!is_numeric($value)) {
|
|
$error = 'Data confidence score must be numeric';
|
|
return null;
|
|
}
|
|
|
|
$score = (float)$value;
|
|
if ($score < 0 || $score > 1) {
|
|
$error = 'Data confidence score must be between 0 and 1';
|
|
return null;
|
|
}
|
|
|
|
return $score;
|
|
}
|
|
|
|
function normalizeNikHashFromInput($nikInput = null) {
|
|
if ($nikInput === null) {
|
|
return null;
|
|
}
|
|
|
|
$nik = trim((string)$nikInput);
|
|
if ($nik === '') {
|
|
return null;
|
|
}
|
|
|
|
return hash('sha256', $nik);
|
|
}
|
|
|
|
function householdExists($db, $householdId) {
|
|
$stmt = $db->prepare('SELECT id FROM households WHERE id = ? LIMIT 1');
|
|
$stmt->execute([(int)$householdId]);
|
|
return (bool)$stmt->fetch(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
function getHouseholdById($db, $householdId) {
|
|
$stmt = $db->prepare('
|
|
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 h.id = ?
|
|
');
|
|
$stmt->execute([(int)$householdId]);
|
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
function mapEconomicVulnerability($economicStatus) {
|
|
$status = strtolower(trim((string)$economicStatus));
|
|
$scores = [
|
|
'sangat_miskin' => 1.0,
|
|
'miskin' => 0.85,
|
|
'rentan' => 0.65,
|
|
'cukup' => 0.35,
|
|
'baik' => 0.15,
|
|
'unknown' => 0.5
|
|
];
|
|
|
|
return isset($scores[$status]) ? $scores[$status] : 0.5;
|
|
}
|
|
|
|
function syncNeedPointsToHouseholds($db) {
|
|
$updatedCount = 0;
|
|
$insertedCount = 0;
|
|
|
|
$updateStmt = $db->prepare("\n UPDATE households\n SET economic_status = (\n SELECT COALESCE(NULLIF(TRIM(np.economic_status), ''), 'unknown')\n FROM need_points np\n WHERE np.id = households.need_point_id\n ),\n updated_at = CURRENT_TIMESTAMP\n WHERE need_point_id IS NOT NULL\n AND (economic_status IS NULL OR TRIM(economic_status) = '')\n ");
|
|
$updateStmt->execute();
|
|
$updatedCount += $updateStmt->rowCount();
|
|
|
|
$missingStmt = $db->query("\n SELECT\n np.id,\n np.name,\n np.latitude,\n np.longitude,\n np.masjid_id,\n np.head_name,\n np.economic_status,\n np.is_verified\n FROM need_points np\n LEFT JOIN households h ON h.need_point_id = np.id\n WHERE h.id IS NULL\n ORDER BY np.id ASC\n ");
|
|
$missingRows = $missingStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
if (!empty($missingRows)) {
|
|
$insertStmt = $db->prepare("\n INSERT INTO households (\n household_code,\n head_name,\n latitude,\n longitude,\n need_point_id,\n masjid_id,\n monthly_income,\n dependents_count,\n housing_condition_score,\n employment_status,\n economic_status,\n verification_status,\n vulnerability_notes\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ");
|
|
|
|
foreach ($missingRows as $row) {
|
|
$pointId = (int)$row['id'];
|
|
$householdCode = 'NP-' . str_pad((string)$pointId, 6, '0', STR_PAD_LEFT);
|
|
$economicStatus = strtolower(trim((string)($row['economic_status'] ?? 'unknown')));
|
|
if ($economicStatus === '') {
|
|
$economicStatus = 'unknown';
|
|
}
|
|
$verificationStatus = ((int)($row['is_verified'] ?? 0) === 1) ? 'field_verified' : 'unverified';
|
|
$headName = trim((string)($row['head_name'] ?? ''));
|
|
if ($headName === '') {
|
|
$headName = 'Kepala ' . $householdCode;
|
|
}
|
|
|
|
$insertStmt->execute([
|
|
$householdCode,
|
|
$headName,
|
|
(float)$row['latitude'],
|
|
(float)$row['longitude'],
|
|
$pointId,
|
|
!empty($row['masjid_id']) ? (int)$row['masjid_id'] : null,
|
|
0,
|
|
0,
|
|
3,
|
|
'unknown',
|
|
$economicStatus,
|
|
$verificationStatus,
|
|
'Auto-synced from need_point #' . $pointId
|
|
]);
|
|
$insertedCount++;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'updated_count' => $updatedCount,
|
|
'inserted_count' => $insertedCount,
|
|
'total_changed' => $updatedCount + $insertedCount
|
|
];
|
|
}
|
|
|
|
function ensureHouseholdScoresForAllHouseholds($db) {
|
|
$missingStmt = $db->query("\n SELECT h.*\n FROM households h\n LEFT JOIN household_scores hs ON hs.household_id = h.id\n WHERE hs.household_id IS NULL\n ORDER BY h.id ASC\n ");
|
|
$missingHouseholds = $missingStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$computed = 0;
|
|
foreach ($missingHouseholds as $household) {
|
|
$scoreData = computeHouseholdScores($db, $household);
|
|
upsertHouseholdScore($db, $household['id'], $scoreData);
|
|
$computed++;
|
|
}
|
|
|
|
return $computed;
|
|
}
|
|
|
|
function clamp01($value) {
|
|
$v = (float)$value;
|
|
if ($v < 0) return 0.0;
|
|
if ($v > 1) return 1.0;
|
|
return $v;
|
|
}
|
|
|
|
function normalizeToUnit($value, $minValue, $maxValue, $invert = false) {
|
|
$minV = (float)$minValue;
|
|
$maxV = (float)$maxValue;
|
|
|
|
if ($maxV <= $minV) {
|
|
return 0.0;
|
|
}
|
|
|
|
$normalized = ((float)$value - $minV) / ($maxV - $minV);
|
|
$normalized = clamp01($normalized);
|
|
|
|
if ($invert) {
|
|
return 1.0 - $normalized;
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
function getLatestAreaStatisticValue($db, $areaId, $statType) {
|
|
if (!$areaId || !$statType) {
|
|
return null;
|
|
}
|
|
|
|
$stmt = $db->prepare('
|
|
SELECT value
|
|
FROM area_statistics
|
|
WHERE pontianak_area_id = ? AND statistic_type = ?
|
|
ORDER BY data_date DESC, created_at DESC
|
|
LIMIT 1
|
|
');
|
|
$stmt->execute([(int)$areaId, (string)$statType]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row) {
|
|
return null;
|
|
}
|
|
|
|
return (float)$row['value'];
|
|
}
|
|
|
|
function getHouseholdDistanceToMasjidMeters($householdRow, $masjidRow) {
|
|
if (!$householdRow || !$masjidRow) {
|
|
return null;
|
|
}
|
|
|
|
return haversineDistanceMeters(
|
|
(float)$householdRow['latitude'],
|
|
(float)$householdRow['longitude'],
|
|
(float)$masjidRow['latitude'],
|
|
(float)$masjidRow['longitude']
|
|
);
|
|
}
|
|
|
|
function mapEmploymentVulnerability($employmentStatus) {
|
|
$status = strtolower(trim((string)$employmentStatus));
|
|
$scores = [
|
|
'unemployed' => 1.0,
|
|
'daily_worker' => 0.85,
|
|
'informal' => 0.7,
|
|
'micro_business' => 0.55,
|
|
'retired' => 0.4,
|
|
'formal' => 0.25,
|
|
'unknown' => 0.5
|
|
];
|
|
|
|
return isset($scores[$status]) ? $scores[$status] : 0.5;
|
|
}
|
|
|
|
function mapShockFactor($verificationStatus) {
|
|
$status = strtolower(trim((string)$verificationStatus));
|
|
$map = [
|
|
'needs_review' => 1.0,
|
|
'unverified' => 0.7,
|
|
'rejected' => 0.6,
|
|
'field_verified' => 0.4,
|
|
'admin_approved' => 0.2
|
|
];
|
|
|
|
return isset($map[$status]) ? $map[$status] : 0.5;
|
|
}
|
|
|
|
function classifyPriorityLevel($spi) {
|
|
$value = (float)$spi;
|
|
if ($value >= 0.75) {
|
|
return 'very_high';
|
|
}
|
|
if ($value >= 0.60) {
|
|
return 'high';
|
|
}
|
|
if ($value >= 0.45) {
|
|
return 'medium';
|
|
}
|
|
|
|
return 'low';
|
|
}
|
|
|
|
function computeHouseholdScores($db, $householdRow) {
|
|
$income = (float)($householdRow['monthly_income'] ?? 0);
|
|
$dependents = (int)($householdRow['dependents_count'] ?? 0);
|
|
$housingScore = (int)($householdRow['housing_condition_score'] ?? 3);
|
|
$employmentStatus = $householdRow['employment_status'] ?? 'unknown';
|
|
$economicStatus = $householdRow['economic_status'] ?? 'unknown';
|
|
$verificationStatus = $householdRow['verification_status'] ?? 'unverified';
|
|
|
|
// SKR components (higher = more vulnerable)
|
|
$economicVulnerability = mapEconomicVulnerability($economicStatus); // primary poverty condition
|
|
$incomeDeprivation = normalizeToUnit($income, 0, 5000000, true); // supporting signal
|
|
$employmentVulnerability = mapEmploymentVulnerability($employmentStatus); // E
|
|
$dependencyBurden = normalizeToUnit($dependents, 0, 6, false); // D
|
|
$housingVulnerability = normalizeToUnit($housingScore, 1, 5, true); // H
|
|
|
|
// A: accessibility deprivation (inverse from distance to masjid)
|
|
$masjidDistanceMeters = null;
|
|
$accessibilityService = 0.5;
|
|
if (!empty($householdRow['masjid_id'])) {
|
|
$masjidStmt = $db->prepare('SELECT latitude, longitude FROM masjids WHERE id = ?');
|
|
$masjidStmt->execute([(int)$householdRow['masjid_id']]);
|
|
$masjid = $masjidStmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if ($masjid) {
|
|
$masjidDistanceMeters = getHouseholdDistanceToMasjidMeters($householdRow, $masjid);
|
|
$distanceAccessScore = normalizeToUnit($masjidDistanceMeters, 0, 2000, true); // J as accessibility
|
|
$accessibilityService = 1.0 - $distanceAccessScore; // convert to deprivation
|
|
}
|
|
}
|
|
|
|
// S: shock factor from current verification confidence
|
|
$shockFactor = mapShockFactor($verificationStatus);
|
|
|
|
$skr =
|
|
(0.40 * $economicVulnerability) +
|
|
(0.18 * $incomeDeprivation) +
|
|
(0.12 * $employmentVulnerability) +
|
|
(0.10 * $dependencyBurden) +
|
|
(0.10 * $housingVulnerability) +
|
|
(0.06 * $accessibilityService) +
|
|
(0.04 * $shockFactor);
|
|
$skr = round(clamp01($skr), 6);
|
|
|
|
// SAG components (higher = better accessibility)
|
|
$J = 0.5;
|
|
if ($masjidDistanceMeters !== null) {
|
|
$J = normalizeToUnit($masjidDistanceMeters, 0, 2000, true);
|
|
}
|
|
|
|
$roadDensity = getLatestAreaStatisticValue($db, $householdRow['pontianak_area_id'] ?? null, 'road_density');
|
|
$R = $roadDensity !== null ? normalizeToUnit($roadDensity, 0, 5, false) : 0.5;
|
|
|
|
// T fallback because travel-time data is not available yet.
|
|
$T = ($J + $R) / 2;
|
|
|
|
$sag = (0.40 * $J) + (0.35 * $R) + (0.25 * $T);
|
|
$sag = round(clamp01($sag), 6);
|
|
|
|
$K = $shockFactor;
|
|
$spi = (0.60 * $skr) + (0.30 * (1 - $sag)) + (0.10 * $K);
|
|
$spi = round(clamp01($spi), 6);
|
|
|
|
$priorityLevel = classifyPriorityLevel($spi);
|
|
|
|
return [
|
|
'skr' => $skr,
|
|
'sag' => $sag,
|
|
'spi' => $spi,
|
|
'priority_level' => $priorityLevel,
|
|
'score_version' => 'v1',
|
|
'components' => [
|
|
'ECO_economic_vulnerability' => round($economicVulnerability, 6),
|
|
'I_income_deprivation' => round($incomeDeprivation, 6),
|
|
'E_employment_vulnerability' => round($employmentVulnerability, 6),
|
|
'D_dependency_burden' => round($dependencyBurden, 6),
|
|
'H_housing_vulnerability' => round($housingVulnerability, 6),
|
|
'A_accessibility_deprivation' => round($accessibilityService, 6),
|
|
'S_shock_factor' => round($shockFactor, 6),
|
|
'J_distance_access' => round($J, 6),
|
|
'R_road_access' => round($R, 6),
|
|
'T_time_access_proxy' => round($T, 6)
|
|
]
|
|
];
|
|
}
|
|
|
|
function upsertHouseholdScore($db, $householdId, $scoreData) {
|
|
$stmt = $db->prepare('
|
|
INSERT INTO household_scores (
|
|
household_id,
|
|
skr,
|
|
sag,
|
|
spi,
|
|
priority_level,
|
|
score_version,
|
|
components_json,
|
|
computed_at,
|
|
updated_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
ON CONFLICT(household_id)
|
|
DO UPDATE SET
|
|
skr = excluded.skr,
|
|
sag = excluded.sag,
|
|
spi = excluded.spi,
|
|
priority_level = excluded.priority_level,
|
|
score_version = excluded.score_version,
|
|
components_json = excluded.components_json,
|
|
computed_at = CURRENT_TIMESTAMP,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
');
|
|
|
|
$stmt->execute([
|
|
(int)$householdId,
|
|
(float)$scoreData['skr'],
|
|
(float)$scoreData['sag'],
|
|
(float)$scoreData['spi'],
|
|
(string)$scoreData['priority_level'],
|
|
(string)$scoreData['score_version'],
|
|
json_encode($scoreData['components'])
|
|
]);
|
|
}
|
|
|
|
// Authentication helper functions
|
|
function loginUser($email, $password) {
|
|
try {
|
|
$db = getDB();
|
|
$stmt = $db->prepare('SELECT id, username, email, password_hash, role, is_active FROM users WHERE email = ? AND is_active = 1');
|
|
$stmt->execute([$email]);
|
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$user) {
|
|
return ['success' => false, 'message' => 'User not found or inactive'];
|
|
}
|
|
|
|
if (!password_verify($password, $user['password_hash'])) {
|
|
return ['success' => false, 'message' => 'Invalid password'];
|
|
}
|
|
|
|
// Set session
|
|
$_SESSION['user_id'] = $user['id'];
|
|
$_SESSION['username'] = $user['username'];
|
|
$_SESSION['email'] = $user['email'];
|
|
$_SESSION['role'] = $user['role'];
|
|
$_SESSION['login_time'] = time();
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Login successful',
|
|
'user' => [
|
|
'id' => $user['id'],
|
|
'username' => $user['username'],
|
|
'email' => $user['email'],
|
|
'role' => $user['role']
|
|
]
|
|
];
|
|
} catch (PDOException $e) {
|
|
return ['success' => false, 'message' => 'Database error: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
function logoutUser() {
|
|
session_destroy();
|
|
return ['success' => true, 'message' => 'Logout successful'];
|
|
}
|
|
|
|
function getCurrentUser() {
|
|
if (!isset($_SESSION['user_id'])) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'id' => $_SESSION['user_id'],
|
|
'username' => $_SESSION['username'],
|
|
'email' => $_SESSION['email'],
|
|
'role' => $_SESSION['role']
|
|
];
|
|
}
|
|
|
|
function isUserLoggedIn() {
|
|
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
|
|
}
|
|
|
|
function requireLogin() {
|
|
if (!isUserLoggedIn()) {
|
|
http_response_code(401);
|
|
die(json_encode(['error' => 'Unauthorized - Please login']));
|
|
}
|
|
}
|
|
|
|
function requireRole($allowedRoles) {
|
|
requireLogin();
|
|
|
|
if (!is_array($allowedRoles)) {
|
|
$allowedRoles = [$allowedRoles];
|
|
}
|
|
|
|
if (!in_array($_SESSION['role'], $allowedRoles)) {
|
|
http_response_code(403);
|
|
die(json_encode(['error' => 'Forbidden - Insufficient permissions']));
|
|
}
|
|
}
|
|
|
|
function createUser($username, $email, $password, $role = 'Petugas') {
|
|
try {
|
|
$db = getDB();
|
|
|
|
// Check if user already exists
|
|
$stmt = $db->prepare('SELECT id FROM users WHERE email = ? OR username = ?');
|
|
$stmt->execute([$email, $username]);
|
|
if ($stmt->fetch()) {
|
|
return ['success' => false, 'message' => 'User already exists'];
|
|
}
|
|
|
|
$passwordHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
|
|
|
|
$stmt = $db->prepare('
|
|
INSERT INTO users (username, email, password_hash, role, is_active)
|
|
VALUES (?, ?, ?, ?, 1)
|
|
');
|
|
$stmt->execute([$username, $email, $passwordHash, $role]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'User created successfully',
|
|
'user_id' => $db->lastInsertId()
|
|
];
|
|
} catch (PDOException $e) {
|
|
return ['success' => false, 'message' => 'Database error: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
?>
|