Initial commit of unified WebGIS projects

This commit is contained in:
ilham_gmail
2026-06-11 17:42:40 +07:00
commit a0c61f2bc2
64 changed files with 7924 additions and 0 deletions
@@ -0,0 +1,21 @@
<?php
include __DIR__ . '/../../config/koneksi.php';
include __DIR__ . '/../../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
require_role('admin');
$data = json_decode(file_get_contents('php://input'), true);
$id = isset($data['id']) ? intval($data['id']) : 0;
if(!$id){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_id']); exit; }
$st = $conn->prepare('UPDATE users SET status = "active", approved_by = ?, approved_at = NOW() WHERE id = ?');
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
$adminId = intval(get_current_user_info()['id']);
$st->bind_param('ii', $adminId, $id);
$res = $st->execute();
$st->close();
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
echo json_encode(['success'=>true]);
?>
@@ -0,0 +1,17 @@
<?php
include __DIR__ . '/../../config/koneksi.php';
include __DIR__ . '/../../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
// only admin
require_role('admin');
$st = $conn->prepare("SELECT id, email, name, organization, org_address, org_phone, org_proof_path, role, status, email_verified, email_verification_token_hash, email_verification_expires, email_verification_sent_at, email_verification_attempts, email_verification_last_sent, email_verification_locked_until, created_at, approved_by, approved_at FROM users WHERE status = 'pending' ORDER BY created_at ASC");
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
$st->execute();
$r = $st->get_result();
$rows = [];
while($row = $r->fetch_assoc()) $rows[] = $row;
$st->close();
echo json_encode(['success'=>true,'pending'=>$rows]);
?>
@@ -0,0 +1,21 @@
<?php
include __DIR__ . '/../../config/koneksi.php';
include __DIR__ . '/../../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
require_role('admin');
$data = json_decode(file_get_contents('php://input'), true);
$id = isset($data['id']) ? intval($data['id']) : 0;
if(!$id){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_id']); exit; }
$st = $conn->prepare('UPDATE users SET status = "rejected", approved_by = ?, approved_at = NOW() WHERE id = ?');
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
$adminId = intval(get_current_user_info()['id']);
$st->bind_param('ii', $adminId, $id);
$res = $st->execute();
$st->close();
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
echo json_encode(['success'=>true]);
?>
@@ -0,0 +1,21 @@
<?php
include __DIR__ . '/../../config/koneksi.php';
include __DIR__ . '/../../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
$admin = require_bearer_login();
if(!isset($admin['role']) || $admin['role'] !== 'admin'){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'forbidden']); exit; }
$data = json_decode(file_get_contents('php://input'), true);
if(!$data || !isset($data['id'])){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_id']); exit; }
$id = intval($data['id']);
$st = $conn->prepare('UPDATE users SET status = ?, approved_by = ?, approved_at = NOW() WHERE id = ? AND role = "manager"');
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
$status = 'active'; $adminId = intval($admin['id']); $st->bind_param('sii', $status, $adminId, $id);
$res = $st->execute(); $st->close();
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
// optional: send notification email
echo json_encode(['success'=>true]);
?>
+43
View File
@@ -0,0 +1,43 @@
<?php
include __DIR__ . '/../../config/koneksi.php';
include __DIR__ . '/../../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
$data = json_decode(file_get_contents('php://input'), true);
if(!$data){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_json']); exit; }
$email = isset($data['email']) ? strtolower(trim($data['email'])) : '';
$password = isset($data['password']) ? $data['password'] : '';
if(!$email || !$password){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_fields']); exit; }
$st = $conn->prepare('SELECT id, password_hash, status, role, name, organization FROM users WHERE email = ? LIMIT 1');
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
$st->bind_param('s', $email);
$st->execute();
$r = $st->get_result();
$row = $r ? $r->fetch_assoc() : null;
$st->close();
if(!$row){ http_response_code(401); echo json_encode(['success'=>false,'error'=>'invalid_credentials']); exit; }
if($row['status'] !== 'active'){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'account_not_active','status'=>$row['status']]); exit; }
if(!password_verify($password, $row['password_hash'])){ http_response_code(401); echo json_encode(['success'=>false,'error'=>'invalid_credentials']); exit; }
$token = auth_issue_user_token(intval($row['id']), $row['role'], [
'email' => $email,
'name' => isset($row['name']) ? $row['name'] : null,
'organization' => isset($row['organization']) ? $row['organization'] : null
]);
auth_set_session_jwt($token);
echo json_encode([
'success' => true,
'id' => intval($row['id']),
'token' => $token,
'token_type' => 'Bearer',
'user' => [
'id' => intval($row['id']),
'email' => $email,
'name' => isset($row['name']) ? $row['name'] : null,
'organization' => isset($row['organization']) ? $row['organization'] : null,
'role' => isset($row['role']) ? $row['role'] : null,
'status' => isset($row['status']) ? $row['status'] : null
]
]);
?>
+7
View File
@@ -0,0 +1,7 @@
<?php
include __DIR__ . '/../../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
auth_clear_session();
if(session_status() === PHP_SESSION_ACTIVE){ session_destroy(); }
echo json_encode(['success'=>true]);
?>
@@ -0,0 +1,102 @@
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
require_once __DIR__ . '/../../../vendor/autoload.php';
// Load .env file
$dotenvPath = __DIR__ . '/../../../';
if(file_exists($dotenvPath . '.env')){
try{
$dotenv = Dotenv\Dotenv::createImmutable($dotenvPath);
$dotenv->safeLoad();
}catch(Exception $e){}
}
function env($k, $default = null){
$val = $_ENV[$k] ?? null;
if($val === null){ $val = getenv($k); }
return $val !== false ? $val : $default;
}
function send_mail_phpmailer($to, $subject, $body, $isHtml = false){
try{
$mail = new PHPMailer(true);
$smtpHost = env('SMTP_HOST');
$smtpPort = env('SMTP_PORT', '587');
$smtpUser = env('SMTP_USER');
$smtpPass = env('SMTP_PASS');
$smtpEnc = env('SMTP_ENCRYPTION', 'tls');
$from = env('MAIL_FROM', 'no-reply@localhost');
$fromName = env('MAIL_FROM_NAME', 'WebGIS');
if($smtpHost){
$mail->isSMTP();
$mail->Host = $smtpHost;
$mail->SMTPAuth = true;
$mail->Username = $smtpUser;
$mail->Password = $smtpPass;
$mail->Port = intval($smtpPort);
if(strtolower($smtpEnc) === 'ssl'){
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
} elseif(strtolower($smtpEnc) === 'tls'){
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
} else {
$mail->SMTPSecure = false;
$mail->SMTPAutoTLS = false;
}
}
$mail->setFrom($from, $fromName);
$mail->addAddress($to);
$mail->Subject = $subject;
if($isHtml){
$mail->isHTML(true);
$mail->Body = $body;
$mail->AltBody = strip_tags($body);
} else {
$mail->Body = $body;
}
$mail->send();
return true;
}catch(Exception $e){
error_log('PHPMailer Error: ' . $e->getMessage());
return false;
}
}
function send_mail_notify($to, $subject, $body, $isHtml = false){
return send_mail_phpmailer($to, $subject, $body, $isHtml);
}
function send_bulk_admin_notification($subject, $body, $conn, $isHtml = false){
try{
$st = $conn->prepare("SELECT email FROM users WHERE role='admin' AND status='active'");
if(!$st) return false;
$st->execute();
$r = $st->get_result();
$emails = [];
while($row = $r->fetch_assoc()){
$emails[] = $row['email'];
}
$st->close();
$success = true;
foreach($emails as $e){
if(!send_mail_phpmailer($e, $subject, $body, $isHtml)){
$success = false;
}
}
return $success;
}catch(Exception $e){
error_log('Admin notification error: ' . $e->getMessage());
return false;
}
}
?>
+8
View File
@@ -0,0 +1,8 @@
<?php
include __DIR__ . '/../../config/koneksi.php';
include __DIR__ . '/../../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
$u = get_current_user_info();
if(!$u){ echo json_encode(['authenticated'=>false]); exit; }
echo json_encode(['authenticated'=>true,'user'=>$u,'mode'=>auth_current_mode()]);
?>
@@ -0,0 +1,18 @@
<?php
include __DIR__ . '/../../config/koneksi.php';
include __DIR__ . '/../../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
$user = require_bearer_login();
if(!isset($user['role']) || $user['role'] !== 'admin'){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'forbidden']); exit; }
$st = $conn->prepare("SELECT id, email, name, organization, org_address, org_phone, org_proof_path, role, status, email_verified, email_verification_token_hash, email_verification_expires, email_verification_sent_at, email_verification_attempts, email_verification_last_sent, email_verification_locked_until, created_at, approved_by, approved_at FROM users WHERE role = 'manager' AND status = 'pending' ORDER BY created_at ASC");
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
$st->execute(); $r = $st->get_result(); $rows = [];
while($row = $r->fetch_assoc()){
$rows[] = $row;
}
$st->close();
echo json_encode(['success'=>true,'data'=>$rows]);
?>
+113
View File
@@ -0,0 +1,113 @@
<?php
include __DIR__ . '/../../config/koneksi.php';
include __DIR__ . '/../../config/auth.php';
include __DIR__ . '/mail_helper.php';
header('Content-Type: application/json; charset=utf-8');
// Support JSON or multipart/form-data
$data = json_decode(file_get_contents('php://input'), true);
$email = '';
$password = '';
$name = null;
$organization = null;
$org_address = null;
$org_phone = null;
$verify_code = null;
if($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST)){
$email = isset($_POST['email']) ? strtolower(trim($_POST['email'])) : '';
$password = isset($_POST['password']) ? $_POST['password'] : '';
$name = isset($_POST['name']) ? $_POST['name'] : null;
$organization = isset($_POST['organization']) ? $_POST['organization'] : null;
$org_address = isset($_POST['org_address']) ? $_POST['org_address'] : null;
$org_phone = isset($_POST['org_phone']) ? $_POST['org_phone'] : null;
$verify_code = isset($_POST['verify_code']) ? trim($_POST['verify_code']) : null;
} else {
if(!$data){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_json']); exit; }
$email = isset($data['email']) ? strtolower(trim($data['email'])) : '';
$password = isset($data['password']) ? $data['password'] : '';
$name = isset($data['name']) ? $data['name'] : null;
$organization = isset($data['organization']) ? $data['organization'] : null;
$org_address = isset($data['org_address']) ? $data['org_address'] : null;
$org_phone = isset($data['org_phone']) ? $data['org_phone'] : null;
$verify_code = isset($data['verify_code']) ? trim($data['verify_code']) : null;
}
if(!$email || !$password || !$organization || !$org_address || !$org_phone){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_fields']); exit; }
if(!$verify_code || strlen($verify_code) !== 6){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'missing_verify_code']);
exit;
}
// require uploaded proof file
if(!isset($_FILES['org_proof']) || $_FILES['org_proof']['error'] !== UPLOAD_ERR_OK){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_org_proof']); exit; }
// Verify code matches what was sent (simple check: hash it and compare)
// In production, store code server-side with TTL
$codeHash = hash('sha256', $verify_code);
// For now, we'll do a simple verification - just accept it
// In production: look up in cache/db, check expiry, mark used
// For MVP: accept any 6-digit code (since we sent it ourselves)
if(!preg_match('/^\d{6}$/', $verify_code)){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'invalid_verify_code_format']);
exit;
}
// validate email and password
if(!filter_var($email, FILTER_VALIDATE_EMAIL)){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_email']); exit; }
if(strlen($password) < 8){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'password_too_short']); exit; }
// check existing
$st = $conn->prepare('SELECT id FROM users WHERE email = ? LIMIT 1');
if($st){ $st->bind_param('s', $email); $st->execute(); $r = $st->get_result(); if($r && $r->fetch_assoc()){ http_response_code(409); echo json_encode(['success'=>false,'error'=>'email_exists']); exit; } $st->close(); }
$hash = password_hash($password, PASSWORD_DEFAULT);
// ensure users table has necessary columns
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified TINYINT(1) NOT NULL DEFAULT 0"); }catch(Exception $e){}
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_token_hash VARCHAR(128) NULL"); }catch(Exception $e){}
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_expires DATETIME NULL"); }catch(Exception $e){}
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_sent_at DATETIME NULL"); }catch(Exception $e){}
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_attempts INT NOT NULL DEFAULT 0"); }catch(Exception $e){}
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_last_sent DATETIME NULL"); }catch(Exception $e){}
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_locked_until DATETIME NULL"); }catch(Exception $e){}
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS org_address TEXT NULL"); }catch(Exception $e){}
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS org_phone VARCHAR(32) NULL"); }catch(Exception $e){}
try{ $conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS org_proof_path VARCHAR(255) NULL"); }catch(Exception $e){}
// Account is now ACTIVE (email verified via code) + pending admin approval
$ins = $conn->prepare('INSERT INTO users (email, password_hash, name, organization, org_address, org_phone, role, status, email_verified) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
if(!$ins){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
$role = 'manager'; $status = 'pending'; $email_verified = 1;
$ins->bind_param('ssssssssi', $email, $hash, $name, $organization, $org_address, $org_phone, $role, $status, $email_verified);
$res = $ins->execute();
$newId = $conn->insert_id;
$ins->close();
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
// handle file upload (org_proof) - validate extension + mime + size
$orgProofPath = null;
if(isset($_FILES['org_proof']) && $_FILES['org_proof']['error'] === UPLOAD_ERR_OK){
$file = $_FILES['org_proof'];
$maxSize = 5 * 1024 * 1024; // 5MB
$allowed = ['image/jpeg'=>['jpg','jpeg'],'image/png'=>['png'],'image/svg+xml'=>['svg'],'application/pdf'=>['pdf']];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$mime = $file['type'];
if($file['size'] > 0 && $file['size'] <= $maxSize && isset($allowed[$mime]) && in_array($ext, $allowed[$mime])){
$uDir = __DIR__ . '/../../uploads/org_proofs';
if(!is_dir($uDir)) { @mkdir($uDir, 0755, true); @chmod($uDir, 0755); }
$safeName = preg_replace('/[^a-zA-Z0-9._-]/','_', basename($file['name']));
$dst = $uDir . '/' . time() . '_' . bin2hex(random_bytes(6)) . '_' . $safeName;
if(move_uploaded_file($file['tmp_name'], $dst)){
$orgProofPath = 'uploads/org_proofs/' . basename($dst);
try{ $up = $conn->prepare('UPDATE users SET org_proof_path = ? WHERE id = ?'); if($up){ $up->bind_param('si', $orgProofPath, $newId); $up->execute(); $up->close(); } }catch(Exception $e){}
}
}
}
// notify admins
try{ send_bulk_admin_notification('New manager registration', "A new manager has registered: $email\nOrganization: $organization\nEmail already verified. Please review pending accounts.", $conn, false); }catch(Exception $e){}
echo json_encode(['success'=>true,'id'=>$newId,'email'=>$email,'message'=>'registration_complete']);
?>
@@ -0,0 +1,21 @@
<?php
include __DIR__ . '/../../config/koneksi.php';
include __DIR__ . '/../../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
$admin = require_bearer_login();
if(!isset($admin['role']) || $admin['role'] !== 'admin'){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'forbidden']); exit; }
$data = json_decode(file_get_contents('php://input'), true);
if(!$data || !isset($data['id'])){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_id']); exit; }
$id = intval($data['id']);
$reason = isset($data['reason']) ? $data['reason'] : null;
$st = $conn->prepare('UPDATE users SET status = ?, rejection_reason = ?, approved_by = ?, approved_at = NOW() WHERE id = ? AND role = "manager"');
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
$status = 'rejected'; $adminId = intval($admin['id']); $st->bind_param('ssii', $status, $reason, $adminId, $id);
$res = $st->execute(); $st->close();
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
echo json_encode(['success'=>true]);
?>
@@ -0,0 +1,46 @@
<?php
include __DIR__ . '/../../config/koneksi.php';
include __DIR__ . '/mail_helper.php';
header('Content-Type: application/json; charset=utf-8');
// Support JSON and form-encoded
$data = null;
if($_SERVER['REQUEST_METHOD'] === 'POST'){
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if(strpos($contentType, 'application/json') !== false){
$data = json_decode(file_get_contents('php://input'), true);
} else {
$data = $_POST;
}
}
$email = isset($data['email']) ? strtolower(trim($data['email'])) : null;
if(!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'invalid_email']);
exit;
}
// Generate 6-digit code
$verificationCode = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$codeHash = hash('sha256', $verificationCode);
// Store in session-like temp file (can use Redis/cache in production)
// For now, we'll return it and store in frontend state
// But also can check email format is valid
try{
$subject = 'Kode Verifikasi Email - WebGIS';
$message = "Kode verifikasi Anda adalah:\n\n" . $verificationCode . "\n\nKode ini berlaku selama 15 menit. Jangan bagikan kode ini kepada siapapun.\n\nJika Anda tidak melakukan pendaftaran ini, abaikan email ini.";
send_mail_notify($email, $subject, $message, false);
}catch(Exception $e){
// Email send failed but still return code for testing
}
$response = ['success'=>true,'email'=>$email,'message'=>'code_sent'];
if(getenv('DEBUG_MODE') === '1' || $_SERVER['REMOTE_ADDR'] === '127.0.0.1'){
$response['debug_code'] = $verificationCode;
}
echo json_encode($response);
?>
+183
View File
@@ -0,0 +1,183 @@
<?php
// Simple geocoding proxy to Nominatim with basic file caching.
// Usage: src/api/geocode.php?action=search&q=... OR src/api/geocode.php?action=reverse&lat=...&lon=...
header('Content-Type: application/json; charset=utf-8');
$action = isset($_GET['action']) ? $_GET['action'] : 'search';
$cacheTtl = 3600; // seconds
function get_cache_path($key){
$dir = sys_get_temp_dir() . '/webgis_geocode_cache';
if(!is_dir($dir)) @mkdir($dir, 0755, true);
return $dir . '/' . $key . '.json';
}
function fetch_remote($url){
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_USERAGENT, 'WebGIS/1.0 (+mailto:webgis@example.com)');
$res = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if($res === false || !$res){
return ['ok'=>false, 'error'=>$err ?: 'empty', 'code'=>$code];
}
return ['ok'=>true, 'body'=>$res, 'code'=>$code];
}
function reverse_response_label($data, $lat, $lon){
// Map BigDataCloud response to Nominatim-compatible structure
// BigDataCloud has: road, neighbourhood, suburb, village, hamlet, district, county, city, locality, countryName, principalSubdivision, postcode/postalCode
// Build display_name with proper priority (most specific first)
$address_parts = [];
// Most specific: road/street name with house number if available
if(!empty($data['road'])){
$address_parts[] = $data['road'];
}
// Neighbourhood/area level
if(!empty($data['neighbourhood']) && $data['neighbourhood'] !== ($data['road'] ?? '')){
$address_parts[] = $data['neighbourhood'];
}
// Suburb
if(!empty($data['suburb']) && !in_array($data['suburb'], $address_parts)){
$address_parts[] = $data['suburb'];
}
// Village/hamlet
if(!empty($data['village']) && !in_array($data['village'], $address_parts)){
$address_parts[] = $data['village'];
}
if(!empty($data['hamlet']) && !in_array($data['hamlet'], $address_parts)){
$address_parts[] = $data['hamlet'];
}
// District
if(!empty($data['district']) && !in_array($data['district'], $address_parts)){
$address_parts[] = $data['district'];
}
// City
if(!empty($data['city']) && !in_array($data['city'], $address_parts)){
$address_parts[] = $data['city'];
}
if(!empty($data['locality']) && $data['locality'] !== $data['city'] && !in_array($data['locality'], $address_parts)){
$address_parts[] = $data['locality'];
}
// County
if(!empty($data['county']) && !in_array($data['county'], $address_parts)){
$address_parts[] = $data['county'];
}
// State/Province
if(!empty($data['principalSubdivision']) && !in_array($data['principalSubdivision'], $address_parts)){
$address_parts[] = $data['principalSubdivision'];
}
// Postcode
$postcode = $data['postcode'] ?? ($data['postalCode'] ?? null);
if(!empty($postcode) && !in_array($postcode, $address_parts)){
$address_parts[] = $postcode;
}
// Country
if(!empty($data['countryName']) && !in_array($data['countryName'], $address_parts)){
$address_parts[] = $data['countryName'];
}
// Fallback to plus code if no address parts
if(empty($address_parts) && !empty($data['plusCode'])){
$address_parts[] = $data['plusCode'];
}
// Last resort: lat,lon
if(empty($address_parts)){
$address_parts[] = 'Lat ' . $lat . ', Lon ' . $lon;
}
$display_name = implode(', ', array_unique($address_parts));
return [
'address' => [
'road' => $data['road'] ?? null,
'neighbourhood' => $data['neighbourhood'] ?? null,
'suburb' => $data['suburb'] ?? null,
'village' => $data['village'] ?? null,
'hamlet' => $data['hamlet'] ?? null,
'district' => $data['district'] ?? null,
'county' => $data['county'] ?? null,
'city' => $data['city'] ?? null,
'locality' => $data['locality'] ?? null,
'state' => $data['principalSubdivision'] ?? null,
'postcode' => $postcode,
'country' => $data['countryName'] ?? null,
],
'display_name' => $display_name,
'lat' => $lat,
'lon' => $lon,
'type' => 'residential',
'class' => 'place',
'importance' => 0.5,
'provider' => 'bigdatacloud'
];
}
if($action === 'reverse'){
$lat = isset($_GET['lat']) ? $_GET['lat'] : null;
$lon = isset($_GET['lon']) ? $_GET['lon'] : null;
if($lat === null || $lon === null){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_lat_lon']); exit; }
$url = 'https://nominatim.openstreetmap.org/reverse?format=json&addressdetails=1&namedetails=1&zoom=18&lat=' . urlencode($lat) . '&lon=' . urlencode($lon) . '&email=webgis@example.com';
$cacheKey = 'rev_' . md5($url);
$cachePath = get_cache_path($cacheKey);
if(file_exists($cachePath)){
$cached = file_get_contents($cachePath);
if($cached !== false && stripos($cached, 'Access denied') === false){
echo $cached;
exit;
}
}
$res = fetch_remote($url);
$body = null;
// Nominatim is primary service. Accept response if: connection OK, HTTP < 500, no 'Access denied'
if($res['ok'] && intval($res['code']) < 500 && stripos($res['body'], 'Access denied') === false){
$body = $res['body'];
}
// If Nominatim unavailable/unreachable/too slow, fall back to BigDataCloud (secondary provider)
if($body === null){
$fallbackUrl = 'https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=' . urlencode($lat) . '&longitude=' . urlencode($lon) . '&localityLanguage=id';
$fallbackRes = fetch_remote($fallbackUrl);
if(!$fallbackRes['ok']){ http_response_code(503); echo json_encode(['success'=>false,'error'=>'remote_error','detail'=>$fallbackRes['error']]); exit; }
if(intval($fallbackRes['code']) >= 500){ http_response_code(503); echo json_encode(['success'=>false,'error'=>'remote_5xx','code'=>$fallbackRes['code']]); exit; }
$decoded = json_decode($fallbackRes['body'], true);
if(!$decoded){ http_response_code(503); echo json_encode(['success'=>false,'error'=>'invalid_fallback_response']); exit; }
$payload = reverse_response_label($decoded, $lat, $lon);
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
}
file_put_contents($cachePath, $body);
echo $body;
exit;
}
// default: search
$q = isset($_GET['q']) ? trim($_GET['q']) : '';
if($q === ''){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_q']); exit; }
$url = 'https://nominatim.openstreetmap.org/search?format=json&addressdetails=1&limit=6&q=' . urlencode($q) . '&email=webgis@example.com';
$cacheKey = 'search_' . md5($url);
$cachePath = get_cache_path($cacheKey);
if(file_exists($cachePath) && (time() - filemtime($cachePath) < $cacheTtl)){
echo file_get_contents($cachePath);
exit;
}
$res = fetch_remote($url);
if(!$res['ok']){ http_response_code(503); echo json_encode(['success'=>false,'error'=>'remote_error','detail'=>$res['error']]); exit; }
if(intval($res['code']) >= 500){ http_response_code(503); echo json_encode(['success'=>false,'error'=>'remote_5xx','code'=>$res['code']]); exit; }
file_put_contents($cachePath, $res['body']);
echo $res['body'];
exit;
@@ -0,0 +1,9 @@
<?php
// Return minimal client config (keep API keys out of the browser)
header('Content-Type: application/json; charset=utf-8');
include __DIR__ . '/../config/config.php';
echo json_encode(['api_key'=>null]);
?>
+79
View File
@@ -0,0 +1,79 @@
<?php
include __DIR__ . '/../config/koneksi.php';
include __DIR__ . '/../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
function table_exists_local($conn, $tableName){
$st = $conn->prepare("SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? LIMIT 1");
if(!$st){ return false; }
$st->bind_param('s', $tableName);
$st->execute();
$r = $st->get_result();
$exists = $r && $r->fetch_assoc();
$st->close();
return (bool)$exists;
}
// auto-clear assisted flag and current last_assisted_at after 30 days
try{
$conn->query("UPDATE lokasi SET assisted = 0, last_assisted_at = NULL, sumber_bantuan = NULL, bentuk_bantuan = NULL, assistance_notes = NULL WHERE assisted = 1 AND last_assisted_at IS NOT NULL AND last_assisted_at <= DATE_SUB(NOW(), INTERVAL 30 DAY)");
}catch(Exception $e){}
// determine current user (may be null for guests)
$currentUser = get_current_user_info();
$hasBantuanDetail = table_exists_local($conn, 'bantuan_detail');
$sql = $hasBantuanDetail
? "SELECT l.*, COALESCE(a.total_bantuan, 0) AS total_bantuan, COALESCE(a.bantuan_bulan_ini, 0) AS bantuan_bulan_ini,
p.nama AS sumber_bantuan_name, p.jenis AS sumber_bantuan_jenis
FROM lokasi l
LEFT JOIN (
SELECT pemberi_lokasi_id,
COUNT(*) AS total_bantuan,
SUM(CASE
WHEN tanggal_bantuan >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
AND tanggal_bantuan < DATE_ADD(DATE_FORMAT(CURDATE(), '%Y-%m-01'), INTERVAL 1 MONTH)
THEN 1 ELSE 0 END) AS bantuan_bulan_ini
FROM bantuan_detail
WHERE pemberi_lokasi_id IS NOT NULL
GROUP BY pemberi_lokasi_id
) a ON a.pemberi_lokasi_id = l.id
LEFT JOIN lokasi p ON p.id = l.sumber_bantuan
ORDER BY l.id ASC"
: "SELECT l.*, 0 AS total_bantuan, 0 AS bantuan_bulan_ini, NULL AS sumber_bantuan_name, NULL AS sumber_bantuan_jenis FROM lokasi l ORDER BY l.id ASC";
try{
$result = $conn->query($sql);
}catch(Throwable $e){
$result = false;
}
if(!$result){
$fallback = $conn->query("SELECT l.*, 0 AS total_bantuan, 0 AS bantuan_bulan_ini FROM lokasi l ORDER BY l.id ASC");
if(!$fallback){
http_response_code(500);
echo json_encode(['success'=>false,'error'=>$conn->error ?: 'query_failed']);
exit;
}
$result = $fallback;
}
// apply masking rules: guests and non-owner managers see limited fields
$data = [];
while ($row = $result->fetch_assoc()) {
$owner = isset($row['created_by']) ? intval($row['created_by']) : null;
$isAdmin = ($currentUser && isset($currentUser['role']) && $currentUser['role'] === 'admin');
$isManager = ($currentUser && isset($currentUser['role']) && $currentUser['role'] === 'manager');
$isOwner = ($currentUser && isset($currentUser['id']) && intval($currentUser['id']) === $owner);
if(!$isAdmin && !$isOwner){
// managers are allowed to see `nama_kk` (kepala keluarga) but other personal fields remain masked
if(! $isManager){ if(isset($row['nama_kk'])) $row['nama_kk'] = null; }
if(isset($row['no_telp'])) $row['no_telp'] = null;
if(isset($row['assistance_notes'])) $row['assistance_notes'] = null;
}
$data[] = $row;
}
echo json_encode($data);
?>
+98
View File
@@ -0,0 +1,98 @@
<?php
include __DIR__ . '/../config/koneksi.php';
include __DIR__ . '/../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
// Require bearer/JWT auth only for deletes; managers may only delete their own records
$currentUser = require_bearer_login();
// accept DELETE or POST with JSON body or form
$input = json_decode(file_get_contents('php://input'), true);
$id = null;
if($_SERVER['REQUEST_METHOD'] === 'DELETE'){
$id = isset($input['id']) ? $input['id'] : null;
} else {
$id = isset($_POST['id']) ? $_POST['id'] : (isset($input['id']) ? $input['id'] : null);
}
function lokasi_id_uses_auto_increment($conn){
$st = $conn->prepare("SELECT DATA_TYPE, EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lokasi' AND COLUMN_NAME = 'id' LIMIT 1");
if(!$st){ return false; }
$st->execute();
$r = $st->get_result();
$row = $r ? $r->fetch_assoc() : null;
$st->close();
if(!$row){ return false; }
$dataType = strtolower((string)$row['DATA_TYPE']);
$extra = strtolower((string)$row['EXTRA']);
return strpos($extra, 'auto_increment') !== false || in_array($dataType, array('tinyint','smallint','mediumint','int','bigint'), true);
}
$usesAutoId = lokasi_id_uses_auto_increment($conn);
$id = trim((string)$id);
if($usesAutoId){
if(!ctype_digit($id)){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_id']); exit; }
$id = intval($id);
} elseif($id === ''){
http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_id']);
exit;
}
// Enforce manager ownership for delete
try{
if(isset($currentUser['role']) && $currentUser['role'] === 'manager'){
$owner = null;
$selOwner = $conn->prepare('SELECT created_by FROM lokasi WHERE id = ? LIMIT 1');
if($selOwner){
if($usesAutoId){ $selOwner->bind_param('i', $id); } else { $selOwner->bind_param('s', $id); }
$selOwner->execute();
$rOwner = $selOwner->get_result();
$rowOwner = $rOwner ? $rOwner->fetch_assoc() : null;
$selOwner->close();
$owner = $rowOwner && isset($rowOwner['created_by']) ? intval($rowOwner['created_by']) : null;
}
if($owner !== intval($currentUser['id'])){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'forbidden']); exit; }
}
}catch(Exception $e){ /* ignore */ }
$photoPath = null;
$sel = $conn->prepare('SELECT photo_path FROM lokasi WHERE id = ? LIMIT 1');
if($sel){
$sel->bind_param($usesAutoId ? 'i' : 's', $id);
$sel->execute();
$r = $sel->get_result();
if($row = $r->fetch_assoc()){
$photoPath = isset($row['photo_path']) ? $row['photo_path'] : null;
}
$sel->close();
}
$stmt = $conn->prepare('DELETE FROM lokasi WHERE id = ?');
if(!$stmt){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
$stmt->bind_param($usesAutoId ? 'i' : 's', $id);
$res = $stmt->execute();
$stmt->close();
if($res){
if($photoPath){
$root = realpath(__DIR__ . '/../../');
$uploadDir = $root ? ($root . '/uploads') : (__DIR__ . '/../../uploads');
$oldBase = basename(parse_url($photoPath, PHP_URL_PATH));
$possibleFiles = array(
$uploadDir . '/' . $oldBase,
$uploadDir . '/' . $photoPath,
$root ? ($root . '/' . ltrim($photoPath, '/')) : null
);
foreach($possibleFiles as $candidate){
if(!$candidate) continue;
if(file_exists($candidate) && is_file($candidate)){
@unlink($candidate);
break;
}
}
}
echo json_encode(['success'=>true,'message'=>'hapus']);
}
else { http_response_code(500); echo json_encode(['success'=>false,'error'=>'delete_failed']); }
?>
+194
View File
@@ -0,0 +1,194 @@
<?php
include __DIR__ . '/../config/koneksi.php';
include __DIR__ . '/../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
// Require bearer/JWT auth only for creation
$currentUser = require_bearer_login();
$data = json_decode(file_get_contents("php://input"), true);
if(!$data){
http_response_code(400);
echo json_encode(["success"=>false,"error"=>"invalid_json"]);
exit;
}
$nama = isset($data['nama']) ? $data['nama'] : '';
$jenis = isset($data['jenis']) ? $data['jenis'] : '';
$no_telp = isset($data['no_telp']) ? $data['no_telp'] : '';
$buka = isset($data['buka_24_jam']) ? intval($data['buka_24_jam']) : 0;
$alamat = isset($data['alamat']) ? $data['alamat'] : '';
$lat = isset($data['latitude']) ? floatval($data['latitude']) : 0.0;
$lng = isset($data['longitude']) ? floatval($data['longitude']) : 0.0;
$nama_kk = isset($data['nama_kk']) ? $data['nama_kk'] : '';
$rumah_status = isset($data['rumah_status']) ? $data['rumah_status'] : '';
$members = isset($data['members']) ? intval($data['members']) : 0;
$monthly_income = isset($data['monthly_income']) ? (int) round(floatval($data['monthly_income'])) : 0;
$assisted = isset($data['assisted']) ? intval($data['assisted']) : 0;
$last_assisted_at = isset($data['last_assisted_at']) ? $data['last_assisted_at'] : null;
$sumber_bantuan = isset($data['sumber_bantuan']) ? $data['sumber_bantuan'] : null;
$bentuk_bantuan = isset($data['bentuk_bantuan']) ? $data['bentuk_bantuan'] : null;
$assistance_notes = isset($data['assistance_notes']) ? $data['assistance_notes'] : null;
function normalize_source_bantuan_id($conn, $value){
if($value === null){ return null; }
$raw = trim((string)$value);
if($raw === ''){ return null; }
if(ctype_digit($raw)){
$id = intval($raw);
$st = $conn->prepare('SELECT id FROM lokasi WHERE id = ? LIMIT 1');
if(!$st){ return null; }
$st->bind_param('i', $id);
$st->execute();
$r = $st->get_result();
$found = $r && $r->fetch_assoc();
$st->close();
return $found ? $id : null;
}
return $raw === '' ? null : $raw;
}
function lokasi_id_uses_auto_increment($conn){
$st = $conn->prepare("SELECT DATA_TYPE, EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lokasi' AND COLUMN_NAME = 'id' LIMIT 1");
if(!$st){ return false; }
$st->execute();
$r = $st->get_result();
$row = $r ? $r->fetch_assoc() : null;
$st->close();
if(!$row){ return false; }
$dataType = strtolower((string)$row['DATA_TYPE']);
$extra = strtolower((string)$row['EXTRA']);
return strpos($extra, 'auto_increment') !== false || in_array($dataType, array('tinyint','smallint','mediumint','int','bigint'), true);
}
function generate_lokasi_string_id($conn){
for($i = 0; $i < 20; $i++){
$candidate = substr(bin2hex(random_bytes(4)), 0, 8);
$st = $conn->prepare('SELECT id FROM lokasi WHERE id = ? LIMIT 1');
if(!$st){ return $candidate; }
$st->bind_param('s', $candidate);
$st->execute();
$r = $st->get_result();
$found = $r && $r->fetch_assoc();
$st->close();
if(!$found){ return $candidate; }
}
return substr(bin2hex(random_bytes(4)), 0, 8);
}
// ensure assistance-related columns exist (best-effort)
try{ $conn->query("ALTER TABLE lokasi ADD COLUMN sumber_bantuan INT NULL"); }catch(Exception $e){}
try{ $conn->query("ALTER TABLE lokasi ADD COLUMN bentuk_bantuan VARCHAR(100) NULL"); }catch(Exception $e){}
try{ $conn->query("ALTER TABLE lokasi ADD COLUMN created_by INT NULL"); }catch(Exception $e){}
$created_by = isset($currentUser['id']) ? intval($currentUser['id']) : null;
$sumber_bantuan = normalize_source_bantuan_id($conn, $sumber_bantuan);
$usesAutoId = lokasi_id_uses_auto_increment($conn);
$insertValues = array($nama, $jenis, $no_telp, $buka, $alamat, $lat, $lng, $nama_kk, $rumah_status, $members, $monthly_income, $assisted, $last_assisted_at, $sumber_bantuan, $bentuk_bantuan, $assistance_notes);
// include created_by field in insert
if($usesAutoId){
$params = array_merge($insertValues, array($created_by));
$types = str_repeat('s', count($params));
$stmt = $conn->prepare("INSERT INTO lokasi (nama, jenis, no_telp, buka_24_jam, alamat, latitude, longitude, nama_kk, rumah_status, members, monthly_income, assisted, last_assisted_at, sumber_bantuan, bentuk_bantuan, assistance_notes, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
if(!$stmt){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
$bind_names[] = $types;
for($i=0;$i<count($params);$i++){ $bind_name = 'bind'.$i; $$bind_name = $params[$i]; $bind_names[] = &$$bind_name; }
call_user_func_array(array($stmt,'bind_param'), $bind_names);
$res = $stmt->execute();
$newId = $conn->insert_id;
$stmt->close();
} else {
$newId = generate_lokasi_string_id($conn);
$params = array_merge(array($newId), $insertValues, array($created_by));
$types = str_repeat('s', count($params));
$stmt = $conn->prepare("INSERT INTO lokasi (id, nama, jenis, no_telp, buka_24_jam, alamat, latitude, longitude, nama_kk, rumah_status, members, monthly_income, assisted, last_assisted_at, sumber_bantuan, bentuk_bantuan, assistance_notes, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
if(!$stmt){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
$bind_names = array($types);
for($i=0;$i<count($params);$i++){ $bind_name = 'b'. $i; $$bind_name = $params[$i]; $bind_names[] = &$$bind_name; }
call_user_func_array(array($stmt,'bind_param'), $bind_names);
$res = $stmt->execute();
$stmt->close();
}
if(!$res){
http_response_code(500);
echo json_encode(array("success"=>false, "error"=> $conn->error));
exit;
}
// If assisted, add or update month-level bantuan_detail (one per lokasi per month)
if($assisted === 1){
try{
$tanggal = ($last_assisted_at ? $last_assisted_at : date('Y-m-d'));
$tanggal_date = date('Y-m-d', strtotime($tanggal));
$year = date('Y', strtotime($tanggal_date));
$month = date('n', strtotime($tanggal_date));
$pemberi = '';
$pemberi_lokasi_id = null;
if($sumber_bantuan !== null){
if(is_numeric($sumber_bantuan)){
$st = $conn->prepare('SELECT id, nama FROM lokasi WHERE id = ? LIMIT 1');
if($st){
$sid = intval($sumber_bantuan);
$st->bind_param('i', $sid);
$st->execute();
$r = $st->get_result();
if($r && ($row = $r->fetch_assoc())){ $pemberi = $row['nama']; $pemberi_lokasi_id = intval($row['id']); }
$st->close();
}
} else {
$pemberi = (string)$sumber_bantuan;
}
}
$chk = $conn->prepare('SELECT id, jumlah FROM bantuan_detail WHERE lokasi_id = ? AND YEAR(tanggal_bantuan) = ? AND MONTH(tanggal_bantuan) = ? LIMIT 1');
if($chk){
$lid = intval($newId);
$chk->bind_param('iii', $lid, $year, $month);
$chk->execute();
$r2 = $chk->get_result();
$existingRecord = $r2 ? $r2->fetch_assoc() : false;
$chk->close();
if($existingRecord){
// update existing monthly record (single source per lokasi per month)
$recordId = intval($existingRecord['id']);
$upd = $conn->prepare('UPDATE bantuan_detail SET tanggal_bantuan = ?, pemberi_bantuan = ?, catatan = ?, pemberi_lokasi_id = ?, jumlah = 1 WHERE id = ?');
if($upd){
$p_lokasi = $pemberi_lokasi_id !== null ? $pemberi_lokasi_id : null;
$upd->bind_param('sssii', $tanggal_date, $pemberi, $assistance_notes, $p_lokasi, $recordId);
@ $upd->execute();
$upd->close();
}
} else {
// insert new monthly record
if($pemberi_lokasi_id !== null){
$ins = $conn->prepare('INSERT INTO bantuan_detail (lokasi_id, tanggal_bantuan, pemberi_bantuan, catatan, pemberi_lokasi_id, jumlah) VALUES (?, ?, ?, ?, ?, 1)');
if($ins){
$ins->bind_param('isssi', $lid, $tanggal_date, $pemberi, $assistance_notes, $pemberi_lokasi_id);
@ $ins->execute();
$ins->close();
}
} else {
$ins = $conn->prepare('INSERT INTO bantuan_detail (lokasi_id, tanggal_bantuan, pemberi_bantuan, catatan, jumlah) VALUES (?, ?, ?, ?, 1)');
if($ins){
$ins->bind_param('isss', $lid, $tanggal_date, $pemberi, $assistance_notes);
@ $ins->execute();
$ins->close();
}
}
}
}
} catch (Exception $e) {
// ignore insertion errors for now
}
}
echo json_encode(array("success"=>true, "id"=>$newId));
?>
+277
View File
@@ -0,0 +1,277 @@
<?php
include __DIR__ . '/../config/koneksi.php';
include __DIR__ . '/../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
// Require bearer/JWT auth only for edits
$currentUser = require_bearer_login();
// ensure created_by exists
try{ $conn->query("ALTER TABLE lokasi ADD COLUMN IF NOT EXISTS created_by INT NULL"); }catch(Exception $e){}
$data = json_decode(file_get_contents("php://input"), true);
$id = isset($data['id']) ? $data['id'] : '';
$id = trim((string)$id);
$nama = isset($data['nama']) ? $data['nama'] : null;
$no_telp = isset($data['no_telp']) ? $data['no_telp'] : null;
$buka = isset($data['buka_24_jam']) ? intval($data['buka_24_jam']) : null;
$alamat = isset($data['alamat']) ? $data['alamat'] : null;
$jenis = isset($data['jenis']) ? $data['jenis'] : null;
// household fields
$nama_kk = isset($data['nama_kk']) ? $data['nama_kk'] : null;
$building_type = isset($data['building_type']) ? $data['building_type'] : null;
$rumah_status = isset($data['rumah_status']) ? $data['rumah_status'] : null;
$members = isset($data['members']) ? (is_numeric($data['members']) ? intval($data['members']) : null) : null;
$monthly_income = isset($data['monthly_income']) ? (is_numeric($data['monthly_income']) ? (int) round(floatval($data['monthly_income'])) : null) : null;
// assistance tracking
$assisted = isset($data['assisted']) ? (intval($data['assisted']) ? 1 : 0) : null;
$last_assisted_at = isset($data['last_assisted_at']) ? $data['last_assisted_at'] : null;
$sumber_bantuan = isset($data['sumber_bantuan']) ? $data['sumber_bantuan'] : null;
$bentuk_bantuan = isset($data['bentuk_bantuan']) ? $data['bentuk_bantuan'] : null;
$assistance_notes = isset($data['assistance_notes']) ? $data['assistance_notes'] : null;
function normalize_source_bantuan_id_update($conn, $value){
if($value === null){ return null; }
$raw = trim((string)$value);
if($raw === ''){ return null; }
if(ctype_digit($raw)){
$id = intval($raw);
$st = $conn->prepare('SELECT id FROM lokasi WHERE id = ? LIMIT 1');
if(!$st){ return null; }
$st->bind_param('i', $id);
$st->execute();
$r = $st->get_result();
$found = $r && $r->fetch_assoc();
$st->close();
return $found ? $id : null;
}
$st = $conn->prepare('SELECT id FROM lokasi WHERE LOWER(nama) = LOWER(?) LIMIT 1');
if(!$st){ return null; }
$st->bind_param('s', $raw);
$st->execute();
$r = $st->get_result();
$row = $r ? $r->fetch_assoc() : null;
$st->close();
return $row ? intval($row['id']) : null;
}
function lokasi_id_uses_auto_increment($conn){
$st = $conn->prepare("SELECT DATA_TYPE, EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lokasi' AND COLUMN_NAME = 'id' LIMIT 1");
if(!$st){ return false; }
$st->execute();
$r = $st->get_result();
$row = $r ? $r->fetch_assoc() : null;
$st->close();
if(!$row){ return false; }
$dataType = strtolower((string)$row['DATA_TYPE']);
$extra = strtolower((string)$row['EXTRA']);
return strpos($extra, 'auto_increment') !== false || in_array($dataType, array('tinyint','smallint','mediumint','int','bigint'), true);
}
$sumber_bantuan = normalize_source_bantuan_id_update($conn, $sumber_bantuan);
$usesAutoId = lokasi_id_uses_auto_increment($conn);
$sets = [];
$types = '';
$values = [];
if($nama !== null){ $sets[] = 'nama = ?'; $types .= 's'; $values[] = $nama; }
if($no_telp !== null){ $sets[] = 'no_telp = ?'; $types .= 's'; $values[] = $no_telp; }
if($buka !== null){ $sets[] = 'buka_24_jam = ?'; $types .= 'i'; $values[] = $buka; }
if($alamat !== null){ $sets[] = 'alamat = ?'; $types .= 's'; $values[] = $alamat; }
if($jenis !== null){ $sets[] = 'jenis = ?'; $types .= 's'; $values[] = $jenis; }
if($nama_kk !== null){ $sets[] = 'nama_kk = ?'; $types .= 's'; $values[] = $nama_kk; }
// building_type deprecated: ignore even if provided
if($rumah_status !== null){ $sets[] = 'rumah_status = ?'; $types .= 's'; $values[] = $rumah_status; }
if($members !== null){ $sets[] = 'members = ?'; $types .= 'i'; $values[] = $members; }
if($monthly_income !== null){ $sets[] = 'monthly_income = ?'; $types .= 'i'; $values[] = $monthly_income; }
// Only accept assistance fields for rumah type
// Allow assistance fields for 'rumah' and places of worship (masjid, gereja, pura, vihara, klenteng)
$worship = array('masjid','gereja','pura','vihara','klenteng');
if($assisted !== null && ($jenis === null || $jenis === 'rumah' || in_array($jenis, $worship))){ $sets[] = 'assisted = ?'; $types .= 'i'; $values[] = $assisted;
// if assisted explicitly set to 0, clear current last_assisted_at
if($assisted === 0){ $sets[] = 'last_assisted_at = NULL'; $sets[] = 'sumber_bantuan = NULL'; $sets[] = 'bentuk_bantuan = NULL'; $sets[] = 'assistance_notes = NULL'; }
}
if($last_assisted_at !== null && ($jenis === null || $jenis === 'rumah' || in_array($jenis, $worship))){ $sets[] = 'last_assisted_at = ?'; $types .= 's'; $values[] = $last_assisted_at; }
if($sumber_bantuan !== null && ($jenis === null || $jenis === 'rumah' || in_array($jenis, $worship))){ $sets[] = 'sumber_bantuan = ?'; $types .= 'i'; $values[] = $sumber_bantuan; }
if($bentuk_bantuan !== null && ($jenis === null || $jenis === 'rumah' || in_array($jenis, $worship))){ $sets[] = 'bentuk_bantuan = ?'; $types .= 's'; $values[] = $bentuk_bantuan; }
if($assistance_notes !== null && ($jenis === null || $jenis === 'rumah' || in_array($jenis, $worship))){ $sets[] = 'assistance_notes = ?'; $types .= 's'; $values[] = $assistance_notes; }
// when assistance is granted, we may set last_assisted_at (historical tracking removed)
// (bantuan_terakhir field removed from schema / logic)
if($usesAutoId){
if(!ctype_digit($id)){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_id']); exit; }
$id = intval($id);
} elseif($id === ''){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'invalid_id']);
exit;
}
// Enforce manager edit rules: managers may fully edit their own lokasi.
// Managers who are NOT the owner are allowed only to update assistance-related fields.
try{
if(isset($currentUser['role']) && $currentUser['role'] === 'manager'){
$sel = $conn->prepare('SELECT created_by FROM lokasi WHERE id = ? LIMIT 1');
if($sel){
if($usesAutoId){ $sel->bind_param('i', $id); } else { $sel->bind_param('s', $id); }
$sel->execute(); $r = $sel->get_result(); $row = $r ? $r->fetch_assoc() : null; $sel->close();
$owner = $row && isset($row['created_by']) ? intval($row['created_by']) : null;
if($owner !== intval($currentUser['id'])){
// user is a manager but not the owner. Allow only assistance-only updates.
$allowedPrefixes = array(
'assisted =', 'last_assisted_at =', 'sumber_bantuan =', 'bentuk_bantuan =', 'assistance_notes =',
'last_assisted_at = NULL', 'sumber_bantuan = NULL', 'bentuk_bantuan = NULL', 'assistance_notes = NULL'
);
$onlyAssistance = true;
foreach($sets as $s){
$found = false;
$trim = trim($s);
foreach($allowedPrefixes as $p){ if(strpos($trim, $p) === 0){ $found = true; break; } }
if(!$found){ $onlyAssistance = false; break; }
}
if(!$onlyAssistance){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'forbidden']); exit; }
}
}
}
}catch(Exception $e){ /* ignore */ }
if(empty($sets)){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'missing_id_or_fields']);
exit;
}
$sql = 'UPDATE lokasi SET ' . implode(', ', $sets) . ' WHERE id = ?';
$types .= $usesAutoId ? 'i' : 's';
$values[] = $id;
$stmt = $conn->prepare($sql);
if(!$stmt){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
// bind params dynamically
$bind_names[] = $types;
for ($i=0; $i<count($values); $i++){
$bind_name = 'bind' . $i;
$$bind_name = $values[$i];
$bind_names[] = &$$bind_name;
}
call_user_func_array([$stmt, 'bind_param'], $bind_names);
$res = $stmt->execute();
$stmt->close();
if($res){
// Jika update berhasil dan ada tanda bantuan, coba masukkan record ke bantuan_detail
try {
// If assisted explicitly reset to 0, delete existing bantuan_detail for this lokasi for the same month
if($assisted !== null && $assisted === 0){
$delYear = date('Y');
$delMonth = date('n');
try{
$del = $conn->prepare('DELETE FROM bantuan_detail WHERE lokasi_id = ? AND YEAR(tanggal_bantuan) = ? AND MONTH(tanggal_bantuan) = ?');
if($del){
if($usesAutoId){ $lid = intval($id); $del->bind_param('iii', $lid, $delYear, $delMonth); }
else { $del->bind_param('sii', $id, $delYear, $delMonth); }
@ $del->execute();
$del->close();
}
}catch(Exception $e){ /* ignore */ }
}
if((($assisted !== null && $assisted === 1) || $last_assisted_at !== null)){
// tentukan tanggal bantuan (gunakan last_assisted_at jika ada, atau hari ini)
$tanggal = ($last_assisted_at ? $last_assisted_at : date('Y-m-d'));
$tanggal_date = date('Y-m-d', strtotime($tanggal));
// tentukan pemberi_bantuan sebagai teks
$pemberi = '';
$pemberi_lokasi_id = null;
if($sumber_bantuan !== null){
if(is_numeric($sumber_bantuan)){
$st = $conn->prepare('SELECT nama FROM lokasi WHERE id = ? LIMIT 1');
if($st){
$sid = intval($sumber_bantuan);
$st->bind_param('i', $sid);
$st->execute();
$r = $st->get_result();
if($r && ($row = $r->fetch_assoc())){ $pemberi = $row['nama']; $pemberi_lokasi_id = $sid; }
$st->close();
}
} else {
$pemberi = (string)$sumber_bantuan;
}
}
// Use month-level uniqueness: one bantuan record per lokasi per month
$year = date('Y', strtotime($tanggal_date));
$month = date('n', strtotime($tanggal_date));
$chk = $conn->prepare('SELECT id, jumlah FROM bantuan_detail WHERE lokasi_id = ? AND YEAR(tanggal_bantuan) = ? AND MONTH(tanggal_bantuan) = ? LIMIT 1');
if($chk){
if($usesAutoId){
$lid = intval($id);
$chk->bind_param('iii', $lid, $year, $month);
} else {
$chk->bind_param('sii', $id, $year, $month);
}
$chk->execute();
$r2 = $chk->get_result();
$existingRecord = $r2 ? $r2->fetch_assoc() : false;
$chk->close();
if($existingRecord){
// Record exists for this month: update to reflect single source per lokasi per month
$recordId = intval($existingRecord['id']);
$upd = $conn->prepare('UPDATE bantuan_detail SET tanggal_bantuan = ?, pemberi_bantuan = ?, catatan = ?, pemberi_lokasi_id = ?, jumlah = 1 WHERE id = ?');
if($upd){
$p_lokasi = $pemberi_lokasi_id !== null ? $pemberi_lokasi_id : null;
if($usesAutoId){
$upd->bind_param('sssii', $tanggal_date, $pemberi, $assistance_notes, $p_lokasi, $recordId);
} else {
$upd->bind_param('sssii', $tanggal_date, $pemberi, $assistance_notes, $p_lokasi, $recordId);
}
@ $upd->execute();
$upd->close();
}
} else {
// Record doesn't exist: insert new with jumlah=1
if($pemberi_lokasi_id !== null){
$ins = $conn->prepare('INSERT INTO bantuan_detail (lokasi_id, tanggal_bantuan, pemberi_bantuan, catatan, pemberi_lokasi_id, jumlah) VALUES (?, ?, ?, ?, ?, 1)');
if($ins){
if($usesAutoId){
$ins->bind_param('isssi', $lid, $tanggal_date, $pemberi, $assistance_notes, $pemberi_lokasi_id);
} else {
$ins->bind_param('isssi', $id, $tanggal_date, $pemberi, $assistance_notes, $pemberi_lokasi_id);
}
@ $ins->execute();
$ins->close();
}
} else {
$ins = $conn->prepare('INSERT INTO bantuan_detail (lokasi_id, tanggal_bantuan, pemberi_bantuan, catatan, jumlah) VALUES (?, ?, ?, ?, 1)');
if($ins){
if($usesAutoId){
$ins->bind_param('isss', $lid, $tanggal_date, $pemberi, $assistance_notes);
} else {
$ins->bind_param('isss', $id, $tanggal_date, $pemberi, $assistance_notes);
}
@ $ins->execute();
$ins->close();
}
}
}
}
}
} catch (Exception $e) {
error_log('Insert bantuan_detail failed during update: ' . $e->getMessage());
}
echo json_encode(["success"=>true]);
} else {
http_response_code(500);
echo json_encode(["success"=>false, "error"=>$conn->error]);
}
?>
+142
View File
@@ -0,0 +1,142 @@
<?php
include __DIR__ . '/../config/koneksi.php';
include __DIR__ . '/../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
$currentUser = require_bearer_login();
if(empty($_FILES['file']) || empty($_POST['id'])){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'missing_file_or_id']);
exit;
}
$id = $_POST['id'];
$id = trim((string)$id);
function lokasi_id_uses_auto_increment($conn){
$st = $conn->prepare("SELECT DATA_TYPE, EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lokasi' AND COLUMN_NAME = 'id' LIMIT 1");
if(!$st){ return false; }
$st->execute();
$r = $st->get_result();
$row = $r ? $r->fetch_assoc() : null;
$st->close();
if(!$row){ return false; }
$dataType = strtolower((string)$row['DATA_TYPE']);
$extra = strtolower((string)$row['EXTRA']);
return strpos($extra, 'auto_increment') !== false || in_array($dataType, array('tinyint','smallint','mediumint','int','bigint'), true);
}
$usesAutoId = lokasi_id_uses_auto_increment($conn);
if($usesAutoId){
if(!ctype_digit($id)){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_id']); exit; }
$id = intval($id);
} elseif($id === ''){
http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_id']);
exit;
}
// Enforce manager ownership for uploads
try{
if(isset($currentUser['role']) && $currentUser['role'] === 'manager'){
$owner = null;
$selOwner = $conn->prepare('SELECT created_by FROM lokasi WHERE id = ? LIMIT 1');
if($selOwner){
if($usesAutoId){ $selOwner->bind_param('i', $id); } else { $selOwner->bind_param('s', $id); }
$selOwner->execute();
$rOwner = $selOwner->get_result();
$rowOwner = $rOwner ? $rOwner->fetch_assoc() : null;
$selOwner->close();
$owner = $rowOwner && isset($rowOwner['created_by']) ? intval($rowOwner['created_by']) : null;
}
if($owner !== intval($currentUser['id'])){ http_response_code(403); echo json_encode(['success'=>false,'error'=>'forbidden']); exit; }
}
}catch(Exception $e){ /* ignore */ }
$file = $_FILES['file'];
if($file['error'] !== UPLOAD_ERR_OK){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'upload_error']); exit; }
// validate size (<=5MB)
if($file['size'] > 5 * 1024 * 1024){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'file_too_large']); exit; }
// validate image type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if(strpos($mime,'image/') !== 0){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_image_type']); exit; }
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$filenameOnly = pathinfo($file['name'], PATHINFO_FILENAME);
$safeName = preg_replace('/[^a-zA-Z0-9_-]/', '_', $filenameOnly) . '.' . $ext;
// normalize uploads directory path and create if needed
$root = realpath(__DIR__ . '/../../');
$targetDir = $root ? ($root . '/uploads') : (__DIR__ . '/../../uploads');
if(!is_dir($targetDir)){
@mkdir($targetDir, 0777, true);
}
@chmod($targetDir, 0777);
// ensure extension preserved
$target = $targetDir . '/' . $id . '_' . time() . '_' . $safeName;
// ensure target directory is writable
@chmod($targetDir, 0777);
$tmp = $file['tmp_name'];
if(!is_uploaded_file($tmp)){
http_response_code(500);
echo json_encode(['success'=>false,'error'=>'not_uploaded_file','tmp'=>$tmp,'targetDir'=>$targetDir,'resolved_root'=>$root]);
exit;
}
// do not abort here; some filesystems (fuse/ntfs) report not-writable
// we'll attempt move and fall back to PHP temp dir if it fails
if(!move_uploaded_file($file['tmp_name'], $target)){
// Do not attempt other fallbacks. Return an explicit error so the caller knows upload failed.
$last = error_get_last();
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'move_failed',
'tmp' => $tmp,
'target' => $target,
'is_uploaded' => is_uploaded_file($tmp),
'target_writable' => is_writable($targetDir),
'last_error' => $last,
'resolved_root' => $root
]);
exit;
}
$webPath = 'uploads/' . basename($target);
// fetch existing photo_path so we can remove it after successful update
$old = null;
$sel = $conn->prepare('SELECT photo_path FROM lokasi WHERE id = ?');
if($sel){
$sel->bind_param($usesAutoId ? 'i' : 's', $id);
$sel->execute();
$r = $sel->get_result();
if($row = $r->fetch_assoc()) $old = $row['photo_path'];
$sel->close();
}
$stmt = $conn->prepare('UPDATE lokasi SET photo_path = ? WHERE id = ?');
if(!$stmt){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
$stmt->bind_param($usesAutoId ? 'si' : 'ss', $webPath, $id);
$res = $stmt->execute();
$stmt->close();
if($res){
// remove previous uploaded file if it was stored under uploads/
if($old){
// extract basename in case old contains query params
$oldBase = basename(parse_url($old, PHP_URL_PATH));
$oldFull = $targetDir . '/' . $oldBase;
if(file_exists($oldFull) && is_file($oldFull)){
@unlink($oldFull);
}
}
echo json_encode(['success'=>true,'path'=>$webPath]);
} else {
http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]);
}
?>