add files
This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
<?php
|
||||
|
||||
// Tangkap semua output PHP (termasuk error/warning) sebelum header dikirim
|
||||
ob_start();
|
||||
|
||||
// Tampilkan semua error ke buffer, bukan langsung ke browser
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
// Set header JSON di awal
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Tangkap fatal error dan kembalikan sebagai JSON
|
||||
register_shutdown_function(function () {
|
||||
$error = error_get_last();
|
||||
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
||||
ob_clean();
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'PHP Fatal Error: ' . $error['message'] . ' (line ' . $error['line'] . ')'
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// Tangkap exception yang tidak tertangkap
|
||||
set_exception_handler(function ($e) {
|
||||
ob_clean();
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Exception: ' . $e->getMessage()
|
||||
]);
|
||||
exit;
|
||||
});
|
||||
|
||||
// Cek apakah config.php ada
|
||||
if (!file_exists(__DIR__ . '/config.php')) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'File config.php tidak ditemukan di: ' . __DIR__
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
include 'config.php';
|
||||
|
||||
// Buang output apapun yang tidak sengaja tercetak sebelum JSON (misal warning PHP)
|
||||
ob_clean();
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
// ============================================================
|
||||
// Helper Upload Foto
|
||||
// ============================================================
|
||||
function uploadFoto($fileKey)
|
||||
{
|
||||
if (
|
||||
!isset($_FILES[$fileKey]) ||
|
||||
$_FILES[$fileKey]['error'] !== UPLOAD_ERR_OK
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$uploadDir = 'uploads/';
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($_FILES[$fileKey]['name'], PATHINFO_EXTENSION));
|
||||
|
||||
$allowed = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
|
||||
if (!in_array($ext, $allowed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$filename = uniqid('img_', true) . '.' . $ext;
|
||||
$destination = $uploadDir . $filename;
|
||||
|
||||
if (move_uploaded_file($_FILES[$fileKey]['tmp_name'], $destination)) {
|
||||
return $filename;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET DATA
|
||||
// ============================================================
|
||||
if ($action === 'get_data') {
|
||||
|
||||
$roadsResult = $conn->query("SELECT * FROM roads ORDER BY id DESC");
|
||||
$parcelsResult = $conn->query("SELECT * FROM parcels ORDER BY id DESC");
|
||||
$damagedResult = $conn->query("SELECT * FROM damaged_roads ORDER BY id DESC");
|
||||
|
||||
if (!$roadsResult || !$parcelsResult || !$damagedResult) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Query gagal: ' . $conn->error
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$roads = $roadsResult->fetch_all(MYSQLI_ASSOC);
|
||||
$parcels = $parcelsResult->fetch_all(MYSQLI_ASSOC);
|
||||
$damaged = $damagedResult->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
foreach ($roads as &$row) {
|
||||
$row['foto_url'] = !empty($row['foto'])
|
||||
? 'uploads/' . $row['foto']
|
||||
: null;
|
||||
}
|
||||
|
||||
foreach ($parcels as &$row) {
|
||||
$row['foto_url'] = !empty($row['foto'])
|
||||
? 'uploads/' . $row['foto']
|
||||
: null;
|
||||
}
|
||||
|
||||
foreach ($damaged as &$row) {
|
||||
$row['foto_url'] = !empty($row['foto'])
|
||||
? 'uploads/' . $row['foto']
|
||||
: null;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'roads' => $roads,
|
||||
'parcels' => $parcels,
|
||||
'damaged_roads' => $damaged
|
||||
]);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SEARCH
|
||||
// ============================================================
|
||||
if ($action === 'search') {
|
||||
|
||||
$keyword = $_GET['q'] ?? '';
|
||||
$keyword = $conn->real_escape_string($keyword);
|
||||
$like = "%{$keyword}%";
|
||||
|
||||
$roadsSearch = $conn->query("
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
status,
|
||||
length_m AS measure,
|
||||
'road' AS type
|
||||
FROM roads
|
||||
WHERE name LIKE '$like'
|
||||
OR status LIKE '$like'
|
||||
");
|
||||
|
||||
$parcelsSearch = $conn->query("
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
status,
|
||||
area_sqm AS measure,
|
||||
'parcel' AS type
|
||||
FROM parcels
|
||||
WHERE name LIKE '$like'
|
||||
OR status LIKE '$like'
|
||||
");
|
||||
|
||||
$damagedSearch = $conn->query("
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
status,
|
||||
description AS measure,
|
||||
'damaged' AS type
|
||||
FROM damaged_roads
|
||||
WHERE name LIKE '$like'
|
||||
OR status LIKE '$like'
|
||||
OR description LIKE '$like'
|
||||
");
|
||||
|
||||
if (!$roadsSearch || !$parcelsSearch || !$damagedSearch) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Query pencarian gagal: ' . $conn->error
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$roads = $roadsSearch->fetch_all(MYSQLI_ASSOC);
|
||||
$parcels = $parcelsSearch->fetch_all(MYSQLI_ASSOC);
|
||||
$damaged = $damagedSearch->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
$results = array_merge(
|
||||
$roads,
|
||||
$parcels,
|
||||
$damaged
|
||||
);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $results
|
||||
]);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET ONE
|
||||
// ============================================================
|
||||
if ($action === 'get_one') {
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$type = $_GET['type'] ?? '';
|
||||
|
||||
switch ($type) {
|
||||
|
||||
case 'road':
|
||||
$table = 'roads';
|
||||
break;
|
||||
|
||||
case 'parcel':
|
||||
$table = 'parcels';
|
||||
break;
|
||||
|
||||
case 'damaged':
|
||||
$table = 'damaged_roads';
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Tipe tidak valid'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = $conn->query(
|
||||
"SELECT * FROM $table WHERE id = $id"
|
||||
);
|
||||
|
||||
$row = $result->fetch_assoc();
|
||||
|
||||
if ($row) {
|
||||
$row['foto_url'] = !empty($row['foto'])
|
||||
? 'uploads/' . $row['foto']
|
||||
: null;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $row
|
||||
]);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SAVE DATA
|
||||
// ============================================================
|
||||
if ($action === 'save') {
|
||||
|
||||
$id = isset($_POST['id'])
|
||||
? (int)$_POST['id']
|
||||
: 0;
|
||||
|
||||
$type = $_POST['type'] ?? '';
|
||||
|
||||
$name = $conn->real_escape_string($_POST['name'] ?? '');
|
||||
$status = $conn->real_escape_string($_POST['status'] ?? '');
|
||||
$geom = $conn->real_escape_string($_POST['geom'] ?? '');
|
||||
$measure = $conn->real_escape_string($_POST['measure'] ?? '');
|
||||
$description = $conn->real_escape_string($_POST['description'] ?? '');
|
||||
|
||||
$newFoto = uploadFoto('foto');
|
||||
|
||||
if ($type === 'road') {
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$fotoSql = $newFoto
|
||||
? ", foto='$newFoto'"
|
||||
: "";
|
||||
|
||||
$sql = "
|
||||
UPDATE roads
|
||||
SET
|
||||
name='$name',
|
||||
status='$status',
|
||||
length_m='$measure',
|
||||
geom='$geom'
|
||||
$fotoSql
|
||||
WHERE id=$id
|
||||
";
|
||||
|
||||
} else {
|
||||
|
||||
$fotoVal = $newFoto
|
||||
? "'$newFoto'"
|
||||
: "NULL";
|
||||
|
||||
$sql = "
|
||||
INSERT INTO roads
|
||||
(
|
||||
name,
|
||||
status,
|
||||
length_m,
|
||||
geom,
|
||||
foto
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'$name',
|
||||
'$status',
|
||||
'$measure',
|
||||
'$geom',
|
||||
$fotoVal
|
||||
)
|
||||
";
|
||||
}
|
||||
|
||||
} elseif ($type === 'parcel') {
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$fotoSql = $newFoto
|
||||
? ", foto='$newFoto'"
|
||||
: "";
|
||||
|
||||
$sql = "
|
||||
UPDATE parcels
|
||||
SET
|
||||
name='$name',
|
||||
status='$status',
|
||||
area_sqm='$measure',
|
||||
geom='$geom'
|
||||
$fotoSql
|
||||
WHERE id=$id
|
||||
";
|
||||
|
||||
} else {
|
||||
|
||||
$fotoVal = $newFoto
|
||||
? "'$newFoto'"
|
||||
: "NULL";
|
||||
|
||||
$sql = "
|
||||
INSERT INTO parcels
|
||||
(
|
||||
name,
|
||||
status,
|
||||
area_sqm,
|
||||
geom,
|
||||
foto
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'$name',
|
||||
'$status',
|
||||
'$measure',
|
||||
'$geom',
|
||||
$fotoVal
|
||||
)
|
||||
";
|
||||
}
|
||||
|
||||
} elseif ($type === 'damaged') {
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$fotoSql = $newFoto
|
||||
? ", foto='$newFoto'"
|
||||
: "";
|
||||
|
||||
$sql = "
|
||||
UPDATE damaged_roads
|
||||
SET
|
||||
name='$name',
|
||||
status='$status',
|
||||
description='$description',
|
||||
geom='$geom'
|
||||
$fotoSql
|
||||
WHERE id=$id
|
||||
";
|
||||
|
||||
} else {
|
||||
|
||||
$fotoVal = $newFoto
|
||||
? "'$newFoto'"
|
||||
: "NULL";
|
||||
|
||||
$sql = "
|
||||
INSERT INTO damaged_roads
|
||||
(
|
||||
name,
|
||||
status,
|
||||
description,
|
||||
geom,
|
||||
foto
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'$name',
|
||||
'$status',
|
||||
'$description',
|
||||
'$geom',
|
||||
$fotoVal
|
||||
)
|
||||
";
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Tipe data tidak valid'
|
||||
]);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($conn->query($sql)) {
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Data berhasil disimpan',
|
||||
'id' => $id > 0 ? $id : $conn->insert_id
|
||||
]);
|
||||
|
||||
} else {
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $conn->error
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DELETE DATA
|
||||
// ============================================================
|
||||
if ($action === 'delete') {
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$type = $_POST['type'] ?? '';
|
||||
|
||||
switch ($type) {
|
||||
|
||||
case 'road':
|
||||
$table = 'roads';
|
||||
break;
|
||||
|
||||
case 'parcel':
|
||||
$table = 'parcels';
|
||||
break;
|
||||
|
||||
case 'damaged':
|
||||
$table = 'damaged_roads';
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Tipe tidak valid'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$check = $conn->query(
|
||||
"SELECT foto FROM $table WHERE id=$id"
|
||||
);
|
||||
|
||||
if ($check && $check->num_rows > 0) {
|
||||
|
||||
$row = $check->fetch_assoc();
|
||||
|
||||
if (
|
||||
!empty($row['foto']) &&
|
||||
file_exists('uploads/' . $row['foto'])
|
||||
) {
|
||||
unlink('uploads/' . $row['foto']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($conn->query(
|
||||
"DELETE FROM $table WHERE id=$id"
|
||||
)) {
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Data berhasil dihapus'
|
||||
]);
|
||||
|
||||
} else {
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $conn->error
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ACTION TIDAK DITEMUKAN
|
||||
// ============================================================
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Action tidak ditemukan'
|
||||
]);
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
$host = "localhost";
|
||||
$user = "root";
|
||||
$pass = "";
|
||||
$db = "webgis02";
|
||||
|
||||
$conn = new mysqli($host, $user, $pass, $db);
|
||||
|
||||
if ($conn->connect_error) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Koneksi database gagal: ' . $conn->connect_error
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set charset UTF-8 agar karakter khusus tidak rusak
|
||||
$conn->set_charset("utf8mb4");
|
||||
?>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,141 @@
|
||||
-- phpMyAdmin SQL Dump
|
||||
-- version 5.2.1
|
||||
-- https://www.phpmyadmin.net/
|
||||
--
|
||||
-- Host: 127.0.0.1
|
||||
-- Waktu pembuatan: 07 Jun 2026 pada 10.26
|
||||
-- Versi server: 10.4.32-MariaDB
|
||||
-- Versi PHP: 8.0.30
|
||||
|
||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
--
|
||||
-- Database: `webgis02`
|
||||
--
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Struktur dari tabel `damaged_roads`
|
||||
--
|
||||
|
||||
CREATE TABLE `damaged_roads` (
|
||||
`id` int(11) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`status` varchar(50) NOT NULL COMMENT 'Ringan / Sedang / Berat / Kritis',
|
||||
`description` text DEFAULT NULL COMMENT 'Keterangan kerusakan',
|
||||
`geom` longtext NOT NULL COMMENT 'GeoJSON geometry (Point)',
|
||||
`foto` varchar(255) DEFAULT NULL COMMENT 'Nama file foto',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Dumping data untuk tabel `damaged_roads`
|
||||
--
|
||||
|
||||
INSERT INTO `damaged_roads` (`id`, `name`, `status`, `description`, `geom`, `foto`, `created_at`) VALUES
|
||||
(1, 'Kerusakan Jalan Contoh', 'Sedang', 'Aspal berlubang diameter ±50cm, perlu penanganan segera', '{\"type\":\"Point\",\"coordinates\":[109.3420,-0.0210]}', NULL, '2026-06-07 08:20:12');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Struktur dari tabel `parcels`
|
||||
--
|
||||
|
||||
CREATE TABLE `parcels` (
|
||||
`id` int(11) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`status` varchar(50) NOT NULL COMMENT 'SHM / HGB / HGU / HP',
|
||||
`area_sqm` decimal(15,2) DEFAULT 0.00 COMMENT 'Luas dalam meter persegi',
|
||||
`geom` longtext NOT NULL COMMENT 'GeoJSON geometry (Polygon)',
|
||||
`foto` varchar(255) DEFAULT NULL COMMENT 'Nama file foto',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Dumping data untuk tabel `parcels`
|
||||
--
|
||||
|
||||
INSERT INTO `parcels` (`id`, `name`, `status`, `area_sqm`, `geom`, `foto`, `created_at`) VALUES
|
||||
(1, 'Persil Tanah SHM Contoh', 'SHM', 4500.00, '{\"type\":\"Polygon\",\"coordinates\":[[[109.3380,-0.0220],[109.3400,-0.0220],[109.3400,-0.0240],[109.3380,-0.0240],[109.3380,-0.0220]]]}', NULL, '2026-06-07 08:20:12');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Struktur dari tabel `roads`
|
||||
--
|
||||
|
||||
CREATE TABLE `roads` (
|
||||
`id` int(11) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`status` varchar(50) NOT NULL COMMENT 'Nasional / Provinsi / Kabupaten',
|
||||
`length_m` decimal(12,2) DEFAULT 0.00 COMMENT 'Panjang dalam meter',
|
||||
`geom` longtext NOT NULL COMMENT 'GeoJSON geometry (LineString)',
|
||||
`foto` varchar(255) DEFAULT NULL COMMENT 'Nama file foto',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Dumping data untuk tabel `roads`
|
||||
--
|
||||
|
||||
INSERT INTO `roads` (`id`, `name`, `status`, `length_m`, `geom`, `foto`, `created_at`) VALUES
|
||||
(1, 'Jalan Nasional Contoh', 'Nasional', 1250.00, '{\"type\":\"LineString\",\"coordinates\":[[109.3300,-0.0200],[109.3400,-0.0200],[109.3500,-0.0250]]}', NULL, '2026-06-07 08:20:12'),
|
||||
(2, 'Jalan Provinsi Contoh', 'Provinsi', 870.50, '{\"type\":\"LineString\",\"coordinates\":[[109.3350,-0.0300],[109.3420,-0.0310]]}', NULL, '2026-06-07 08:20:12');
|
||||
|
||||
--
|
||||
-- Indexes for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- Indeks untuk tabel `damaged_roads`
|
||||
--
|
||||
ALTER TABLE `damaged_roads`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indeks untuk tabel `parcels`
|
||||
--
|
||||
ALTER TABLE `parcels`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indeks untuk tabel `roads`
|
||||
--
|
||||
ALTER TABLE `roads`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT untuk tabel yang dibuang
|
||||
--
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT untuk tabel `damaged_roads`
|
||||
--
|
||||
ALTER TABLE `damaged_roads`
|
||||
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT untuk tabel `parcels`
|
||||
--
|
||||
ALTER TABLE `parcels`
|
||||
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT untuk tabel `roads`
|
||||
--
|
||||
ALTER TABLE `roads`
|
||||
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
+514
@@ -0,0 +1,514 @@
|
||||
<?php
|
||||
|
||||
// Tangkap semua output PHP (termasuk error/warning) sebelum header dikirim
|
||||
ob_start();
|
||||
|
||||
// Tampilkan semua error ke buffer, bukan langsung ke browser
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
// Set header JSON di awal
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Tangkap fatal error dan kembalikan sebagai JSON
|
||||
register_shutdown_function(function () {
|
||||
$error = error_get_last();
|
||||
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
||||
ob_clean();
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'PHP Fatal Error: ' . $error['message'] . ' (line ' . $error['line'] . ')'
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// Tangkap exception yang tidak tertangkap
|
||||
set_exception_handler(function ($e) {
|
||||
ob_clean();
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Exception: ' . $e->getMessage()
|
||||
]);
|
||||
exit;
|
||||
});
|
||||
|
||||
// Cek apakah config.php ada
|
||||
if (!file_exists(__DIR__ . '/config.php')) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'File config.php tidak ditemukan di: ' . __DIR__
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
include 'config.php';
|
||||
|
||||
// Buang output apapun yang tidak sengaja tercetak sebelum JSON (misal warning PHP)
|
||||
ob_clean();
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
// ============================================================
|
||||
// Helper Upload Foto
|
||||
// ============================================================
|
||||
function uploadFoto($fileKey)
|
||||
{
|
||||
if (
|
||||
!isset($_FILES[$fileKey]) ||
|
||||
$_FILES[$fileKey]['error'] !== UPLOAD_ERR_OK
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$uploadDir = 'uploads/';
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($_FILES[$fileKey]['name'], PATHINFO_EXTENSION));
|
||||
|
||||
$allowed = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
|
||||
if (!in_array($ext, $allowed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$filename = uniqid('img_', true) . '.' . $ext;
|
||||
$destination = $uploadDir . $filename;
|
||||
|
||||
if (move_uploaded_file($_FILES[$fileKey]['tmp_name'], $destination)) {
|
||||
return $filename;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET DATA
|
||||
// ============================================================
|
||||
if ($action === 'get_data') {
|
||||
|
||||
$roadsResult = $conn->query("SELECT * FROM roads ORDER BY id DESC");
|
||||
$parcelsResult = $conn->query("SELECT * FROM parcels ORDER BY id DESC");
|
||||
$damagedResult = $conn->query("SELECT * FROM damaged_roads ORDER BY id DESC");
|
||||
|
||||
if (!$roadsResult || !$parcelsResult || !$damagedResult) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Query gagal: ' . $conn->error
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$roads = $roadsResult->fetch_all(MYSQLI_ASSOC);
|
||||
$parcels = $parcelsResult->fetch_all(MYSQLI_ASSOC);
|
||||
$damaged = $damagedResult->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
foreach ($roads as &$row) {
|
||||
$row['foto_url'] = !empty($row['foto'])
|
||||
? 'uploads/' . $row['foto']
|
||||
: null;
|
||||
}
|
||||
|
||||
foreach ($parcels as &$row) {
|
||||
$row['foto_url'] = !empty($row['foto'])
|
||||
? 'uploads/' . $row['foto']
|
||||
: null;
|
||||
}
|
||||
|
||||
foreach ($damaged as &$row) {
|
||||
$row['foto_url'] = !empty($row['foto'])
|
||||
? 'uploads/' . $row['foto']
|
||||
: null;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'roads' => $roads,
|
||||
'parcels' => $parcels,
|
||||
'damaged_roads' => $damaged
|
||||
]);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SEARCH
|
||||
// ============================================================
|
||||
if ($action === 'search') {
|
||||
|
||||
$keyword = $_GET['q'] ?? '';
|
||||
$keyword = $conn->real_escape_string($keyword);
|
||||
$like = "%{$keyword}%";
|
||||
|
||||
$roadsSearch = $conn->query("
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
status,
|
||||
length_m AS measure,
|
||||
'road' AS type
|
||||
FROM roads
|
||||
WHERE name LIKE '$like'
|
||||
OR status LIKE '$like'
|
||||
");
|
||||
|
||||
$parcelsSearch = $conn->query("
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
status,
|
||||
area_sqm AS measure,
|
||||
'parcel' AS type
|
||||
FROM parcels
|
||||
WHERE name LIKE '$like'
|
||||
OR status LIKE '$like'
|
||||
");
|
||||
|
||||
$damagedSearch = $conn->query("
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
status,
|
||||
description AS measure,
|
||||
'damaged' AS type
|
||||
FROM damaged_roads
|
||||
WHERE name LIKE '$like'
|
||||
OR status LIKE '$like'
|
||||
OR description LIKE '$like'
|
||||
");
|
||||
|
||||
if (!$roadsSearch || !$parcelsSearch || !$damagedSearch) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Query pencarian gagal: ' . $conn->error
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$roads = $roadsSearch->fetch_all(MYSQLI_ASSOC);
|
||||
$parcels = $parcelsSearch->fetch_all(MYSQLI_ASSOC);
|
||||
$damaged = $damagedSearch->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
$results = array_merge(
|
||||
$roads,
|
||||
$parcels,
|
||||
$damaged
|
||||
);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $results
|
||||
]);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET ONE
|
||||
// ============================================================
|
||||
if ($action === 'get_one') {
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$type = $_GET['type'] ?? '';
|
||||
|
||||
switch ($type) {
|
||||
|
||||
case 'road':
|
||||
$table = 'roads';
|
||||
break;
|
||||
|
||||
case 'parcel':
|
||||
$table = 'parcels';
|
||||
break;
|
||||
|
||||
case 'damaged':
|
||||
$table = 'damaged_roads';
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Tipe tidak valid'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = $conn->query(
|
||||
"SELECT * FROM $table WHERE id = $id"
|
||||
);
|
||||
|
||||
$row = $result->fetch_assoc();
|
||||
|
||||
if ($row) {
|
||||
$row['foto_url'] = !empty($row['foto'])
|
||||
? 'uploads/' . $row['foto']
|
||||
: null;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $row
|
||||
]);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SAVE DATA
|
||||
// ============================================================
|
||||
if ($action === 'save') {
|
||||
|
||||
$id = isset($_POST['id'])
|
||||
? (int)$_POST['id']
|
||||
: 0;
|
||||
|
||||
$type = $_POST['type'] ?? '';
|
||||
|
||||
$name = $conn->real_escape_string($_POST['name'] ?? '');
|
||||
$status = $conn->real_escape_string($_POST['status'] ?? '');
|
||||
$geom = $conn->real_escape_string($_POST['geom'] ?? '');
|
||||
$measure = $conn->real_escape_string($_POST['measure'] ?? '');
|
||||
$description = $conn->real_escape_string($_POST['description'] ?? '');
|
||||
|
||||
$newFoto = uploadFoto('foto');
|
||||
|
||||
if ($type === 'road') {
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$fotoSql = $newFoto
|
||||
? ", foto='$newFoto'"
|
||||
: "";
|
||||
|
||||
$sql = "
|
||||
UPDATE roads
|
||||
SET
|
||||
name='$name',
|
||||
status='$status',
|
||||
length_m='$measure',
|
||||
geom='$geom'
|
||||
$fotoSql
|
||||
WHERE id=$id
|
||||
";
|
||||
|
||||
} else {
|
||||
|
||||
$fotoVal = $newFoto
|
||||
? "'$newFoto'"
|
||||
: "NULL";
|
||||
|
||||
$sql = "
|
||||
INSERT INTO roads
|
||||
(
|
||||
name,
|
||||
status,
|
||||
length_m,
|
||||
geom,
|
||||
foto
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'$name',
|
||||
'$status',
|
||||
'$measure',
|
||||
'$geom',
|
||||
$fotoVal
|
||||
)
|
||||
";
|
||||
}
|
||||
|
||||
} elseif ($type === 'parcel') {
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$fotoSql = $newFoto
|
||||
? ", foto='$newFoto'"
|
||||
: "";
|
||||
|
||||
$sql = "
|
||||
UPDATE parcels
|
||||
SET
|
||||
name='$name',
|
||||
status='$status',
|
||||
area_sqm='$measure',
|
||||
geom='$geom'
|
||||
$fotoSql
|
||||
WHERE id=$id
|
||||
";
|
||||
|
||||
} else {
|
||||
|
||||
$fotoVal = $newFoto
|
||||
? "'$newFoto'"
|
||||
: "NULL";
|
||||
|
||||
$sql = "
|
||||
INSERT INTO parcels
|
||||
(
|
||||
name,
|
||||
status,
|
||||
area_sqm,
|
||||
geom,
|
||||
foto
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'$name',
|
||||
'$status',
|
||||
'$measure',
|
||||
'$geom',
|
||||
$fotoVal
|
||||
)
|
||||
";
|
||||
}
|
||||
|
||||
} elseif ($type === 'damaged') {
|
||||
|
||||
if ($id > 0) {
|
||||
|
||||
$fotoSql = $newFoto
|
||||
? ", foto='$newFoto'"
|
||||
: "";
|
||||
|
||||
$sql = "
|
||||
UPDATE damaged_roads
|
||||
SET
|
||||
name='$name',
|
||||
status='$status',
|
||||
description='$description',
|
||||
geom='$geom'
|
||||
$fotoSql
|
||||
WHERE id=$id
|
||||
";
|
||||
|
||||
} else {
|
||||
|
||||
$fotoVal = $newFoto
|
||||
? "'$newFoto'"
|
||||
: "NULL";
|
||||
|
||||
$sql = "
|
||||
INSERT INTO damaged_roads
|
||||
(
|
||||
name,
|
||||
status,
|
||||
description,
|
||||
geom,
|
||||
foto
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'$name',
|
||||
'$status',
|
||||
'$description',
|
||||
'$geom',
|
||||
$fotoVal
|
||||
)
|
||||
";
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Tipe data tidak valid'
|
||||
]);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($conn->query($sql)) {
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Data berhasil disimpan',
|
||||
'id' => $id > 0 ? $id : $conn->insert_id
|
||||
]);
|
||||
|
||||
} else {
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $conn->error
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DELETE DATA
|
||||
// ============================================================
|
||||
if ($action === 'delete') {
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$type = $_POST['type'] ?? '';
|
||||
|
||||
switch ($type) {
|
||||
|
||||
case 'road':
|
||||
$table = 'roads';
|
||||
break;
|
||||
|
||||
case 'parcel':
|
||||
$table = 'parcels';
|
||||
break;
|
||||
|
||||
case 'damaged':
|
||||
$table = 'damaged_roads';
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Tipe tidak valid'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$check = $conn->query(
|
||||
"SELECT foto FROM $table WHERE id=$id"
|
||||
);
|
||||
|
||||
if ($check && $check->num_rows > 0) {
|
||||
|
||||
$row = $check->fetch_assoc();
|
||||
|
||||
if (
|
||||
!empty($row['foto']) &&
|
||||
file_exists('uploads/' . $row['foto'])
|
||||
) {
|
||||
unlink('uploads/' . $row['foto']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($conn->query(
|
||||
"DELETE FROM $table WHERE id=$id"
|
||||
)) {
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Data berhasil dihapus'
|
||||
]);
|
||||
|
||||
} else {
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $conn->error
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ACTION TIDAK DITEMUKAN
|
||||
// ============================================================
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Action tidak ditemukan'
|
||||
]);
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
$host = getenv('DB_HOST') ?: 'localhost';
|
||||
$user = getenv('DB_USER') ?: 'root';
|
||||
$pass = getenv('DB_PASSWORD') ?: '';
|
||||
$db = getenv('DB_NAME') ?: 'webgis02';
|
||||
|
||||
$conn = new mysqli($host, $user, $pass, $db);
|
||||
|
||||
if ($conn->connect_error) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Koneksi database gagal: ' . $conn->connect_error
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set charset UTF-8 agar karakter khusus tidak rusak
|
||||
$conn->set_charset("utf8mb4");
|
||||
?>
|
||||
+1021
File diff suppressed because it is too large
Load Diff
+141
@@ -0,0 +1,141 @@
|
||||
-- phpMyAdmin SQL Dump
|
||||
-- version 5.2.1
|
||||
-- https://www.phpmyadmin.net/
|
||||
--
|
||||
-- Host: 127.0.0.1
|
||||
-- Waktu pembuatan: 07 Jun 2026 pada 10.26
|
||||
-- Versi server: 10.4.32-MariaDB
|
||||
-- Versi PHP: 8.0.30
|
||||
|
||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
--
|
||||
-- Database: `webgis02`
|
||||
--
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Struktur dari tabel `damaged_roads`
|
||||
--
|
||||
|
||||
CREATE TABLE `damaged_roads` (
|
||||
`id` int(11) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`status` varchar(50) NOT NULL COMMENT 'Ringan / Sedang / Berat / Kritis',
|
||||
`description` text DEFAULT NULL COMMENT 'Keterangan kerusakan',
|
||||
`geom` longtext NOT NULL COMMENT 'GeoJSON geometry (Point)',
|
||||
`foto` varchar(255) DEFAULT NULL COMMENT 'Nama file foto',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Dumping data untuk tabel `damaged_roads`
|
||||
--
|
||||
|
||||
INSERT INTO `damaged_roads` (`id`, `name`, `status`, `description`, `geom`, `foto`, `created_at`) VALUES
|
||||
(1, 'Kerusakan Jalan Contoh', 'Sedang', 'Aspal berlubang diameter ±50cm, perlu penanganan segera', '{\"type\":\"Point\",\"coordinates\":[109.3420,-0.0210]}', NULL, '2026-06-07 08:20:12');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Struktur dari tabel `parcels`
|
||||
--
|
||||
|
||||
CREATE TABLE `parcels` (
|
||||
`id` int(11) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`status` varchar(50) NOT NULL COMMENT 'SHM / HGB / HGU / HP',
|
||||
`area_sqm` decimal(15,2) DEFAULT 0.00 COMMENT 'Luas dalam meter persegi',
|
||||
`geom` longtext NOT NULL COMMENT 'GeoJSON geometry (Polygon)',
|
||||
`foto` varchar(255) DEFAULT NULL COMMENT 'Nama file foto',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Dumping data untuk tabel `parcels`
|
||||
--
|
||||
|
||||
INSERT INTO `parcels` (`id`, `name`, `status`, `area_sqm`, `geom`, `foto`, `created_at`) VALUES
|
||||
(1, 'Persil Tanah SHM Contoh', 'SHM', 4500.00, '{\"type\":\"Polygon\",\"coordinates\":[[[109.3380,-0.0220],[109.3400,-0.0220],[109.3400,-0.0240],[109.3380,-0.0240],[109.3380,-0.0220]]]}', NULL, '2026-06-07 08:20:12');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Struktur dari tabel `roads`
|
||||
--
|
||||
|
||||
CREATE TABLE `roads` (
|
||||
`id` int(11) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`status` varchar(50) NOT NULL COMMENT 'Nasional / Provinsi / Kabupaten',
|
||||
`length_m` decimal(12,2) DEFAULT 0.00 COMMENT 'Panjang dalam meter',
|
||||
`geom` longtext NOT NULL COMMENT 'GeoJSON geometry (LineString)',
|
||||
`foto` varchar(255) DEFAULT NULL COMMENT 'Nama file foto',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Dumping data untuk tabel `roads`
|
||||
--
|
||||
|
||||
INSERT INTO `roads` (`id`, `name`, `status`, `length_m`, `geom`, `foto`, `created_at`) VALUES
|
||||
(1, 'Jalan Nasional Contoh', 'Nasional', 1250.00, '{\"type\":\"LineString\",\"coordinates\":[[109.3300,-0.0200],[109.3400,-0.0200],[109.3500,-0.0250]]}', NULL, '2026-06-07 08:20:12'),
|
||||
(2, 'Jalan Provinsi Contoh', 'Provinsi', 870.50, '{\"type\":\"LineString\",\"coordinates\":[[109.3350,-0.0300],[109.3420,-0.0310]]}', NULL, '2026-06-07 08:20:12');
|
||||
|
||||
--
|
||||
-- Indexes for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- Indeks untuk tabel `damaged_roads`
|
||||
--
|
||||
ALTER TABLE `damaged_roads`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indeks untuk tabel `parcels`
|
||||
--
|
||||
ALTER TABLE `parcels`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- Indeks untuk tabel `roads`
|
||||
--
|
||||
ALTER TABLE `roads`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT untuk tabel yang dibuang
|
||||
--
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT untuk tabel `damaged_roads`
|
||||
--
|
||||
ALTER TABLE `damaged_roads`
|
||||
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT untuk tabel `parcels`
|
||||
--
|
||||
ALTER TABLE `parcels`
|
||||
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT untuk tabel `roads`
|
||||
--
|
||||
ALTER TABLE `roads`
|
||||
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
Reference in New Issue
Block a user