feat: unified portal server, single database migration & railway configuration
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
**/node_modules/
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# Environment variables / Secrets
|
||||||
|
.env
|
||||||
|
**/.env
|
||||||
|
|
||||||
|
# OS Files
|
||||||
|
.DS_Store
|
||||||
|
**/.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Build and logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "webgis_chloropleth",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Static WebGIS Chloropleth app wrapped in an Express server",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^5.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(express.static(__dirname));
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`WebGIS Chloropleth static server running on port ${PORT}`);
|
||||||
|
});
|
||||||
Binary file not shown.
BIN
Binary file not shown.
@@ -10,18 +10,38 @@ app.use(bodyParser.json());
|
|||||||
app.use(express.static(__dirname)); // Serve static files from current directory
|
app.use(express.static(__dirname)); // Serve static files from current directory
|
||||||
|
|
||||||
// MySQL Connection
|
// MySQL Connection
|
||||||
const db = mysql.createConnection({
|
let db;
|
||||||
host: process.env.DB_HOST || 'localhost',
|
function handleDisconnect() {
|
||||||
user: process.env.DB_USER || 'root',
|
const connectionString = process.env.DATABASE_URL || process.env.MYSQL_URL || process.env.MYSQL_PRIVATE_URL;
|
||||||
password: process.env.DB_PASSWORD || '',
|
db = connectionString
|
||||||
database: process.env.DB_NAME || 'webgis_app',
|
? mysql.createConnection(connectionString)
|
||||||
port: process.env.DB_PORT || 3306
|
: mysql.createConnection({
|
||||||
});
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
user: process.env.DB_USER || 'root',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
database: process.env.DB_NAME || 'webgis_app',
|
||||||
|
port: process.env.DB_PORT || 3306
|
||||||
|
});
|
||||||
|
|
||||||
db.connect(err => {
|
db.connect(err => {
|
||||||
if (err) throw err;
|
if (err) {
|
||||||
console.log('MySQL Connected...');
|
console.error('MySQL connection failed: ' + err.stack);
|
||||||
});
|
setTimeout(handleDisconnect, 2000);
|
||||||
|
} else {
|
||||||
|
console.log('MySQL Connected...');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
db.on('error', err => {
|
||||||
|
console.error('Database error:', err);
|
||||||
|
if (err.code === 'PROTOCOL_CONNECTION_LOST') {
|
||||||
|
handleDisconnect();
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleDisconnect();
|
||||||
|
|
||||||
// Route to save a Road
|
// Route to save a Road
|
||||||
app.post('/api/roads', (req, res) => {
|
app.post('/api/roads', (req, res) => {
|
||||||
|
|||||||
@@ -14,51 +14,72 @@ app.use(express.json());
|
|||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
// Database connection
|
// Database connection
|
||||||
const db = mysql.createConnection({
|
let db;
|
||||||
host: process.env.DB_HOST || 'localhost',
|
let migrationsRun = false;
|
||||||
user: process.env.DB_USER || 'root',
|
function handleDisconnect() {
|
||||||
password: process.env.DB_PASSWORD || '',
|
const connectionString = process.env.DATABASE_URL || process.env.MYSQL_URL || process.env.MYSQL_PRIVATE_URL;
|
||||||
database: process.env.DB_NAME || 'webgis_donations',
|
db = connectionString
|
||||||
port: process.env.DB_PORT || 3306
|
? mysql.createConnection(connectionString)
|
||||||
});
|
: mysql.createConnection({
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
db.connect(err => {
|
user: process.env.DB_USER || 'root',
|
||||||
if (err) {
|
password: process.env.DB_PASSWORD || '',
|
||||||
console.error('Database connection failed:', err);
|
database: process.env.DB_NAME || 'webgis_donations',
|
||||||
return;
|
port: process.env.DB_PORT || 3306
|
||||||
}
|
|
||||||
console.log('Connected to MySQL database.');
|
|
||||||
|
|
||||||
db.query("SHOW COLUMNS FROM distribution_history LIKE 'verification_status'", (err, results) => {
|
|
||||||
if (err || results.length > 0) return;
|
|
||||||
console.log('Applying distribution_history schema migration...');
|
|
||||||
const alterSql = `
|
|
||||||
ALTER TABLE distribution_history
|
|
||||||
ADD COLUMN verification_status enum('pending','verified','rejected') NOT NULL DEFAULT 'pending',
|
|
||||||
ADD COLUMN verified_by int DEFAULT NULL,
|
|
||||||
ADD COLUMN verified_at datetime DEFAULT NULL,
|
|
||||||
ADD KEY verified_by (verified_by),
|
|
||||||
ADD CONSTRAINT distribution_history_ibfk_4 FOREIGN KEY (verified_by) REFERENCES users (id) ON DELETE SET NULL
|
|
||||||
`;
|
|
||||||
db.query(alterSql, (alterErr) => {
|
|
||||||
if (alterErr) console.error('Failed to migrate distribution_history schema:', alterErr);
|
|
||||||
else console.log('distribution_history schema migrated successfully.');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
db.connect(err => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Database connection failed:', err);
|
||||||
|
setTimeout(handleDisconnect, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to MySQL database.');
|
||||||
|
|
||||||
|
if (!migrationsRun) {
|
||||||
|
migrationsRun = true;
|
||||||
|
db.query("SHOW COLUMNS FROM distribution_history LIKE 'verification_status'", (err, results) => {
|
||||||
|
if (err || results.length > 0) return;
|
||||||
|
console.log('Applying distribution_history schema migration...');
|
||||||
|
const alterSql = `
|
||||||
|
ALTER TABLE distribution_history
|
||||||
|
ADD COLUMN verification_status enum('pending','verified','rejected') NOT NULL DEFAULT 'pending',
|
||||||
|
ADD COLUMN verified_by int DEFAULT NULL,
|
||||||
|
ADD COLUMN verified_at datetime DEFAULT NULL,
|
||||||
|
ADD KEY verified_by (verified_by),
|
||||||
|
ADD CONSTRAINT distribution_history_ibfk_4 FOREIGN KEY (verified_by) REFERENCES users (id) ON DELETE SET NULL
|
||||||
|
`;
|
||||||
|
db.query(alterSql, (alterErr) => {
|
||||||
|
if (alterErr) console.error('Failed to migrate distribution_history schema:', alterErr);
|
||||||
|
else console.log('distribution_history schema migrated successfully.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Ensure religious_places has place_type column
|
||||||
|
db.query("SHOW COLUMNS FROM religious_places LIKE 'place_type'", (err2, res2) => {
|
||||||
|
if (err2 || res2.length > 0) return;
|
||||||
|
console.log('Applying religious_places place_type migration...');
|
||||||
|
const alterPlaces = `
|
||||||
|
ALTER TABLE religious_places
|
||||||
|
ADD COLUMN place_type enum('mosque','church_catholic','church_protestant','pura','vihara','kelenteng') NOT NULL DEFAULT 'mosque'
|
||||||
|
`;
|
||||||
|
db.query(alterPlaces, (aErr) => {
|
||||||
|
if (aErr) console.error('Failed to migrate religious_places schema:', aErr);
|
||||||
|
else console.log('religious_places schema migrated successfully.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// Ensure religious_places has place_type column
|
|
||||||
db.query("SHOW COLUMNS FROM religious_places LIKE 'place_type'", (err2, res2) => {
|
db.on('error', err => {
|
||||||
if (err2 || res2.length > 0) return;
|
console.error('Database error:', err);
|
||||||
console.log('Applying religious_places place_type migration...');
|
if (err.code === 'PROTOCOL_CONNECTION_LOST') {
|
||||||
const alterPlaces = `
|
handleDisconnect();
|
||||||
ALTER TABLE religious_places
|
} else {
|
||||||
ADD COLUMN place_type enum('mosque','church_catholic','church_protestant','pura','vihara','kelenteng') NOT NULL DEFAULT 'mosque'
|
throw err;
|
||||||
`;
|
}
|
||||||
db.query(alterPlaces, (aErr) => {
|
|
||||||
if (aErr) console.error('Failed to migrate religious_places schema:', aErr);
|
|
||||||
else console.log('religious_places schema migrated successfully.');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
handleDisconnect();
|
||||||
|
|
||||||
// --- MIDDLEWARES ---
|
// --- MIDDLEWARES ---
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -224,7 +224,7 @@
|
|||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// MARKERS STORE { id: { marker, spbu, group } }
|
// MARKERS STORE { id: { marker, spbu, group } }
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
const API_URL = 'http://localhost:3000/api/spbus';
|
const API_URL = '/api/spbus';
|
||||||
let tempPopup = null;
|
let tempPopup = null;
|
||||||
let markers = {};
|
let markers = {};
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
"name": "webgis_gasstation",
|
"name": "webgis_gasstation",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node server.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@@ -11,22 +11,38 @@ app.use(bodyParser.json());
|
|||||||
app.use(express.static(__dirname));
|
app.use(express.static(__dirname));
|
||||||
|
|
||||||
// Create MySQL connection
|
// Create MySQL connection
|
||||||
const db = mysql.createConnection({
|
let db;
|
||||||
host: process.env.DB_HOST || 'localhost',
|
function handleDisconnect() {
|
||||||
user: process.env.DB_USER || 'root',
|
const connectionString = process.env.DATABASE_URL || process.env.MYSQL_URL || process.env.MYSQL_PRIVATE_URL;
|
||||||
password: process.env.DB_PASSWORD || '',
|
db = connectionString
|
||||||
database: process.env.DB_NAME || 'spbu_gis',
|
? mysql.createConnection(connectionString)
|
||||||
port: process.env.DB_PORT || 3306
|
: mysql.createConnection({
|
||||||
});
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
user: process.env.DB_USER || 'root',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
database: process.env.DB_NAME || 'spbu_gis',
|
||||||
|
port: process.env.DB_PORT || 3306
|
||||||
|
});
|
||||||
|
|
||||||
|
db.connect(err => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Database connection failed: ' + err.stack);
|
||||||
|
setTimeout(handleDisconnect, 2000);
|
||||||
|
} else {
|
||||||
|
console.log('Connected to MySQL database.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
db.connect(err => {
|
db.on('error', err => {
|
||||||
if (err) {
|
console.error('Database error:', err);
|
||||||
console.error('Database connection failed: ' + err.stack);
|
if (err.code === 'PROTOCOL_CONNECTION_LOST') {
|
||||||
return;
|
handleDisconnect();
|
||||||
}
|
} else {
|
||||||
console.log('Connected to MySQL database.');
|
throw err;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleDisconnect();
|
||||||
|
|
||||||
// GET route to fetch all SPBU locations
|
// GET route to fetch all SPBU locations
|
||||||
app.get('/api/spbus', (req, res) => {
|
app.get('/api/spbus', (req, res) => {
|
||||||
|
|||||||
+1
-1
@@ -271,7 +271,7 @@
|
|||||||
<!-- Apps Grid -->
|
<!-- Apps Grid -->
|
||||||
<div class="apps-grid">
|
<div class="apps-grid">
|
||||||
<!-- App 1: SPBU dan Layer Control -->
|
<!-- App 1: SPBU dan Layer Control -->
|
||||||
<a href="./pertemuan 1_stable/WebGIS_SPBU_dan_LayerControl/index.html" class="app-card">
|
<a href="./WebGIS_SPBU_dan_LayerControl/index.html" class="app-card">
|
||||||
<div class="app-card-header">🛢️</div>
|
<div class="app-card-header">🛢️</div>
|
||||||
<div class="app-card-content">
|
<div class="app-card-content">
|
||||||
<div class="app-card-title">SPBU & Kontrol Layer</div>
|
<div class="app-card-title">SPBU & Kontrol Layer</div>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "webgis-portal-unified",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Unified WebGIS portal combining SPBU, Road Management, Poverty Mapping, and Chloropleth apps",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"body-parser": "^2.2.2",
|
||||||
|
"cors": "^2.8.6",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"mysql2": "^3.22.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,696 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const mysql = require('mysql2');
|
||||||
|
const cors = require('cors');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const path = require('path');
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(cors());
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// PORT setup
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// JWT SECRET setup
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-key-donations-webgis';
|
||||||
|
|
||||||
|
// Database connection & auto-reconnect logic
|
||||||
|
let db;
|
||||||
|
let migrationsRun = false;
|
||||||
|
|
||||||
|
function handleDisconnect() {
|
||||||
|
const connectionString = process.env.DATABASE_URL || process.env.MYSQL_URL || process.env.MYSQL_PRIVATE_URL;
|
||||||
|
db = connectionString
|
||||||
|
? mysql.createConnection(connectionString)
|
||||||
|
: mysql.createConnection({
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
user: process.env.DB_USER || 'root',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
database: process.env.DB_NAME || 'webgisDarin',
|
||||||
|
port: process.env.DB_PORT || 3306
|
||||||
|
});
|
||||||
|
|
||||||
|
db.connect(err => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Database connection failed:', err);
|
||||||
|
setTimeout(handleDisconnect, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Connected to MySQL database (webgisDarin).');
|
||||||
|
|
||||||
|
// Poverty mapping migrations check
|
||||||
|
if (!migrationsRun) {
|
||||||
|
migrationsRun = true;
|
||||||
|
db.query("SHOW COLUMNS FROM distribution_history LIKE 'verification_status'", (err, results) => {
|
||||||
|
if (err || results.length > 0) return;
|
||||||
|
console.log('Applying distribution_history schema migration...');
|
||||||
|
const alterSql = `
|
||||||
|
ALTER TABLE distribution_history
|
||||||
|
ADD COLUMN verification_status enum('pending','verified','rejected') NOT NULL DEFAULT 'pending',
|
||||||
|
ADD COLUMN verified_by int DEFAULT NULL,
|
||||||
|
ADD COLUMN verified_at datetime DEFAULT NULL,
|
||||||
|
ADD KEY verified_by (verified_by),
|
||||||
|
ADD CONSTRAINT distribution_history_ibfk_4 FOREIGN KEY (verified_by) REFERENCES users (id) ON DELETE SET NULL
|
||||||
|
`;
|
||||||
|
db.query(alterSql, (alterErr) => {
|
||||||
|
if (alterErr) console.error('Failed to migrate distribution_history schema:', alterErr);
|
||||||
|
else console.log('distribution_history schema migrated successfully.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
db.query("SHOW COLUMNS FROM religious_places LIKE 'place_type'", (err2, res2) => {
|
||||||
|
if (err2 || res2.length > 0) return;
|
||||||
|
console.log('Applying religious_places place_type migration...');
|
||||||
|
const alterPlaces = `
|
||||||
|
ALTER TABLE religious_places
|
||||||
|
ADD COLUMN place_type enum('mosque','church_catholic','church_protestant','pura','vihara','kelenteng') NOT NULL DEFAULT 'mosque'
|
||||||
|
`;
|
||||||
|
db.query(alterPlaces, (aErr) => {
|
||||||
|
if (aErr) console.error('Failed to migrate religious_places schema:', aErr);
|
||||||
|
else console.log('religious_places schema migrated successfully.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
db.on('error', err => {
|
||||||
|
console.error('Database error:', err);
|
||||||
|
if (err.code === 'PROTOCOL_CONNECTION_LOST') {
|
||||||
|
handleDisconnect();
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleDisconnect();
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// MIDDLEWARES (Poverty Mapping Auth)
|
||||||
|
// ==========================================
|
||||||
|
const authenticateJWT = (req, res, next) => {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (authHeader) {
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
jwt.verify(token, JWT_SECRET, (err, user) => {
|
||||||
|
if (err) return res.sendStatus(403);
|
||||||
|
req.user = user;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.sendStatus(401);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const authorizeRoles = (...roles) => {
|
||||||
|
return (req, res, next) => {
|
||||||
|
if (!req.user || !roles.includes(req.user.role)) {
|
||||||
|
return res.status(403).json({ message: "Forbidden: insufficient permissions." });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkOwnership = (tableName) => {
|
||||||
|
return (req, res, next) => {
|
||||||
|
if (req.user.role === 'admin') return next();
|
||||||
|
|
||||||
|
let sql = `SELECT added_by FROM ${tableName} WHERE id = ?`;
|
||||||
|
if (tableName === 'households') {
|
||||||
|
sql = `SELECT surveyor_id AS added_by FROM poverty_assessments WHERE household_id = ? LIMIT 1`;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.query(sql, [req.params.id], (err, results) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
if (results.length === 0) return res.status(404).send('Not Found');
|
||||||
|
if (results[0].added_by !== req.user.id) return res.status(403).json({ message: "Forbidden: You did not add this entry." });
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// STATIC PORTAL & APP FILES MOUNTING
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// Serve root portal landing page
|
||||||
|
app.use(express.static(__dirname));
|
||||||
|
|
||||||
|
// Serve sub-applications statically under their respective subpaths
|
||||||
|
app.use('/WebGIS_SPBU_dan_LayerControl', express.static(path.join(__dirname, 'WebGIS_SPBU_dan_LayerControl')));
|
||||||
|
app.use('/WebGIS_ChloroplethData_ImportGeoJSON_FromArcMap', express.static(path.join(__dirname, 'WebGIS_ChloroplethData_ImportGeoJSON_FromArcMap')));
|
||||||
|
app.use('/WebGIS_ManajemenJalan_ManajemenHakBangunan', express.static(path.join(__dirname, 'WebGIS_ManajemenJalan_ManajemenHakBangunan')));
|
||||||
|
app.use('/WebGIS_PovertyMapping', express.static(path.join(__dirname, 'WebGIS_PovertyMapping/public')));
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// ROUTES - 1: SPBU App API
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// GET route to fetch all SPBU locations
|
||||||
|
app.get('/api/spbus', (req, res) => {
|
||||||
|
db.query('SELECT * FROM locations', (err, results) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.json(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST route to save a new SPBU location
|
||||||
|
app.post('/api/spbus', (req, res) => {
|
||||||
|
const { name, open_window, spbu_number, lat, lng } = req.body;
|
||||||
|
const query = 'INSERT INTO locations (name, open_window, spbu_number, lat, lng) VALUES (?, ?, ?, ?, ?)';
|
||||||
|
db.query(query, [name, open_window, spbu_number, lat, lng], (err, result) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.json({ message: 'SPBU saved successfully!', id: result.insertId });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT route to update an SPBU location
|
||||||
|
app.put('/api/spbus/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { name, open_window, spbu_number, lat, lng } = req.body;
|
||||||
|
const query = 'UPDATE locations SET name = ?, open_window = ?, spbu_number = ?, lat = ?, lng = ? WHERE id = ?';
|
||||||
|
db.query(query, [name, open_window, spbu_number, lat, lng, id], (err, result) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.json({ message: 'SPBU updated successfully!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE route to delete an SPBU location
|
||||||
|
app.delete('/api/spbus/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const query = 'DELETE FROM locations WHERE id = ?';
|
||||||
|
db.query(query, [id], (err, result) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.json({ message: 'SPBU deleted successfully!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// ROUTES - 2: Manajemen Jalan & Hak Bangunan App API
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// Route to save a Road
|
||||||
|
app.post('/api/roads', (req, res) => {
|
||||||
|
const { name, road_type, distance_meters, coordinates } = req.body;
|
||||||
|
const query = 'INSERT INTO roads (name, road_type, distance_meters, coordinates) VALUES (?, ?, ?, ?)';
|
||||||
|
db.query(query, [name, road_type, distance_meters, JSON.stringify(coordinates)], (err, result) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send({ message: 'Road saved successfully!', id: result.insertId });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to get all Roads
|
||||||
|
app.get('/api/roads', (req, res) => {
|
||||||
|
const query = 'SELECT * FROM roads';
|
||||||
|
db.query(query, (err, results) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to save Land
|
||||||
|
app.post('/api/lands', (req, res) => {
|
||||||
|
const { name, ownership_status, area_sq_meters, coordinates } = req.body;
|
||||||
|
const query = 'INSERT INTO lands (name, ownership_status, area_sq_meters, coordinates) VALUES (?, ?, ?, ?)';
|
||||||
|
db.query(query, [name, ownership_status, area_sq_meters, JSON.stringify(coordinates)], (err, result) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send({ message: 'Land saved successfully!', id: result.insertId });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to get all Lands
|
||||||
|
app.get('/api/lands', (req, res) => {
|
||||||
|
const query = 'SELECT * FROM lands';
|
||||||
|
db.query(query, (err, results) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to update a Road
|
||||||
|
app.put('/api/roads/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { name, road_type, distance_meters, coordinates } = req.body;
|
||||||
|
const query = 'UPDATE roads SET name = ?, road_type = ?, distance_meters = ?, coordinates = ? WHERE id = ?';
|
||||||
|
db.query(query, [name, road_type, distance_meters, JSON.stringify(coordinates), id], (err, result) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send({ message: 'Road updated successfully!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to delete a Road
|
||||||
|
app.delete('/api/roads/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const query = 'DELETE FROM roads WHERE id = ?';
|
||||||
|
db.query(query, [id], (err, result) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send({ message: 'Road deleted successfully!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to update a Land
|
||||||
|
app.put('/api/lands/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { name, ownership_status, area_sq_meters, coordinates } = req.body;
|
||||||
|
const query = 'UPDATE lands SET name = ?, ownership_status = ?, area_sq_meters = ?, coordinates = ? WHERE id = ?';
|
||||||
|
db.query(query, [name, ownership_status, area_sq_meters, JSON.stringify(coordinates), id], (err, result) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send({ message: 'Land updated successfully!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to delete a Land
|
||||||
|
app.delete('/api/lands/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const query = 'DELETE FROM lands WHERE id = ?';
|
||||||
|
db.query(query, [id], (err, result) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.status(200).send({ message: 'Land deleted successfully!' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// ROUTES - 3: Poverty Mapping App API
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// Initialize default admin if no admins exist
|
||||||
|
app.post('/api/auth/init', async (req, res) => {
|
||||||
|
try {
|
||||||
|
db.query("SELECT COUNT(*) as count FROM users WHERE role = 'admin'", async (err, results) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
if (results[0].count > 0) return res.status(403).json({ message: 'Admin already initialized.' });
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash('admin123', 10);
|
||||||
|
db.query("INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, 'admin')",
|
||||||
|
['admin', 'admin@webgis.com', hashedPassword], (err) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
res.json({ message: 'Default admin initialized. (username: admin, email: admin@webgis.com, pass: admin123)' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).send(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register new users (Admins only)
|
||||||
|
app.post('/api/auth/register', authenticateJWT, authorizeRoles('admin'), async (req, res) => {
|
||||||
|
const { username, email, password, role } = req.body;
|
||||||
|
try {
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
db.query('INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)',
|
||||||
|
[username, email, hashedPassword, role || 'surveyor'], (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
if (err.code === 'ER_DUP_ENTRY') return res.status(400).json({ message: 'Username or email already exists' });
|
||||||
|
return res.status(500).send(err);
|
||||||
|
}
|
||||||
|
res.status(201).json({ message: 'User created successfully', userId: result.insertId });
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).send(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login
|
||||||
|
app.post('/api/auth/login', (req, res) => {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
db.query('SELECT * FROM users WHERE email = ?', [email], async (err, results) => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
if (results.length === 0) return res.status(401).json({ message: 'Invalid email or password' });
|
||||||
|
|
||||||
|
const user = results[0];
|
||||||
|
const validPassword = await bcrypt.compare(password, user.password_hash);
|
||||||
|
if (!validPassword) return res.status(401).json({ message: 'Invalid email or password' });
|
||||||
|
|
||||||
|
const token = jwt.sign({ id: user.id, role: user.role, username: user.username }, JWT_SECRET, { expiresIn: '24h' });
|
||||||
|
res.json({ message: 'Login successful', token, role: user.role, username: user.username });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET endpoints
|
||||||
|
app.get('/api/places', (req, res) => {
|
||||||
|
db.query('SELECT * FROM religious_places', (err, results) => {
|
||||||
|
if (err) res.status(500).send(err);
|
||||||
|
else res.json(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/houses', (req, res) => {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
h.id,
|
||||||
|
h.kk_number,
|
||||||
|
h.head_nik,
|
||||||
|
h.head_name,
|
||||||
|
h.head_name AS name,
|
||||||
|
h.resident_count,
|
||||||
|
h.lat,
|
||||||
|
h.lng,
|
||||||
|
h.linked_place_id,
|
||||||
|
pa.id AS assessment_id,
|
||||||
|
pa.electricity_va,
|
||||||
|
pa.floor_material,
|
||||||
|
pa.wall_material,
|
||||||
|
pa.drinking_water_source,
|
||||||
|
pa.photo_evidence_url,
|
||||||
|
pa.verification_status,
|
||||||
|
pa.surveyor_id,
|
||||||
|
pa.verified_by
|
||||||
|
FROM households h
|
||||||
|
INNER JOIN poverty_assessments pa ON h.id = pa.household_id
|
||||||
|
`;
|
||||||
|
db.query(query, (err, results) => {
|
||||||
|
if (err) res.status(500).send(err);
|
||||||
|
else res.json(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST endpoints
|
||||||
|
app.post('/api/places', authenticateJWT, authorizeRoles('admin', 'surveyor'), (req, res) => {
|
||||||
|
const { name, lat, lng, radius, place_type } = req.body;
|
||||||
|
db.query('INSERT INTO religious_places (name, lat, lng, radius, place_type, added_by) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
|
[name, lat, lng, radius, place_type || 'mosque', req.user.id], (err, result) => {
|
||||||
|
if (err) res.status(500).send(err);
|
||||||
|
else res.json({ id: result.insertId, name, lat, lng, radius, place_type: place_type || 'mosque', added_by: req.user.id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/houses', authenticateJWT, authorizeRoles('admin', 'surveyor'), (req, res) => {
|
||||||
|
const {
|
||||||
|
kk_number,
|
||||||
|
head_nik,
|
||||||
|
head_name,
|
||||||
|
resident_count,
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
linked_place_id,
|
||||||
|
electricity_va,
|
||||||
|
floor_material,
|
||||||
|
wall_material,
|
||||||
|
drinking_water_source,
|
||||||
|
photo_evidence_url
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
db.beginTransaction(err => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
|
||||||
|
const insertHousehold = `
|
||||||
|
INSERT INTO households (kk_number, head_nik, head_name, resident_count, lat, lng, linked_place_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`;
|
||||||
|
db.query(insertHousehold, [kk_number, head_nik, head_name, resident_count, lat, lng, linked_place_id], (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
return db.rollback(() => {
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const householdId = result.insertId;
|
||||||
|
const insertAssessment = `
|
||||||
|
INSERT INTO poverty_assessments (household_id, surveyor_id, electricity_va, floor_material, wall_material, drinking_water_source, photo_evidence_url, verification_status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')
|
||||||
|
`;
|
||||||
|
db.query(insertAssessment, [householdId, req.user.id, electricity_va, floor_material, wall_material, drinking_water_source, photo_evidence_url || null], (err, resultAssess) => {
|
||||||
|
if (err) {
|
||||||
|
return db.rollback(() => {
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
db.commit(err => {
|
||||||
|
if (err) {
|
||||||
|
return db.rollback(() => {
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.status(201).json({
|
||||||
|
id: householdId,
|
||||||
|
kk_number,
|
||||||
|
head_nik,
|
||||||
|
head_name,
|
||||||
|
name: head_name,
|
||||||
|
resident_count: parseInt(resident_count),
|
||||||
|
lat: parseFloat(lat),
|
||||||
|
lng: parseFloat(lng),
|
||||||
|
linked_place_id,
|
||||||
|
assessment_id: resultAssess.insertId,
|
||||||
|
electricity_va,
|
||||||
|
floor_material,
|
||||||
|
wall_material,
|
||||||
|
drinking_water_source,
|
||||||
|
photo_evidence_url: photo_evidence_url || null,
|
||||||
|
verification_status: 'pending',
|
||||||
|
surveyor_id: req.user.id,
|
||||||
|
verified_by: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT endpoints (Updating location via Drag)
|
||||||
|
app.put('/api/places/:id', authenticateJWT, authorizeRoles('admin', 'surveyor'), checkOwnership('religious_places'), (req, res) => {
|
||||||
|
const { lat, lng } = req.body;
|
||||||
|
db.query('UPDATE religious_places SET lat=?, lng=? WHERE id=?',
|
||||||
|
[lat, lng, req.params.id], (err) => {
|
||||||
|
if (err) res.status(500).send(err);
|
||||||
|
else res.sendStatus(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/houses/:id', authenticateJWT, authorizeRoles('admin', 'surveyor'), checkOwnership('households'), (req, res) => {
|
||||||
|
const { lat, lng, linked_place_id } = req.body;
|
||||||
|
db.query('UPDATE households SET lat=?, lng=?, linked_place_id=? WHERE id=?',
|
||||||
|
[lat, lng, linked_place_id, req.params.id], (err) => {
|
||||||
|
if (err) res.status(500).send(err);
|
||||||
|
else res.sendStatus(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT endpoints (Updating Attributes)
|
||||||
|
app.put('/api/places/attr/:id', authenticateJWT, authorizeRoles('admin', 'surveyor'), checkOwnership('religious_places'), (req, res) => {
|
||||||
|
const { name, radius, place_type } = req.body;
|
||||||
|
db.query('UPDATE religious_places SET name=?, radius=?, place_type=? WHERE id=?',
|
||||||
|
[name, radius, place_type || 'mosque', req.params.id], (err) => {
|
||||||
|
if (err) res.status(500).send(err);
|
||||||
|
else res.sendStatus(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/houses/attr/:id', authenticateJWT, authorizeRoles('admin', 'surveyor'), checkOwnership('households'), (req, res) => {
|
||||||
|
const {
|
||||||
|
head_name,
|
||||||
|
kk_number,
|
||||||
|
head_nik,
|
||||||
|
resident_count,
|
||||||
|
electricity_va,
|
||||||
|
floor_material,
|
||||||
|
wall_material,
|
||||||
|
drinking_water_source
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
db.beginTransaction(err => {
|
||||||
|
if (err) return res.status(500).send(err);
|
||||||
|
|
||||||
|
const updateHousehold = 'UPDATE households SET head_name=?, kk_number=?, head_nik=?, resident_count=? WHERE id=?';
|
||||||
|
db.query(updateHousehold, [head_name, kk_number, head_nik, resident_count, req.params.id], (err) => {
|
||||||
|
if (err) {
|
||||||
|
return db.rollback(() => {
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAssessment = 'UPDATE poverty_assessments SET electricity_va=?, floor_material=?, wall_material=?, drinking_water_source=? WHERE household_id=?';
|
||||||
|
db.query(updateAssessment, [electricity_va, floor_material, wall_material, drinking_water_source, req.params.id], (err) => {
|
||||||
|
if (err) {
|
||||||
|
return db.rollback(() => {
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
db.commit(err => {
|
||||||
|
if (err) {
|
||||||
|
return db.rollback(() => {
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/houses/verify/:id', authenticateJWT, authorizeRoles('admin'), (req, res) => {
|
||||||
|
const verifySql = `
|
||||||
|
UPDATE poverty_assessments
|
||||||
|
SET verification_status = 'verified', verified_by = ?
|
||||||
|
WHERE household_id = ?
|
||||||
|
`;
|
||||||
|
db.query(verifySql, [req.user.id, req.params.id], (err, result) => {
|
||||||
|
if (err) return res.status(500).json({ message: 'Database error', error: err.message });
|
||||||
|
if (result.affectedRows === 0) {
|
||||||
|
return res.status(404).json({ message: 'Household assessment not found or already verified.' });
|
||||||
|
}
|
||||||
|
res.json({ message: 'Household verified successfully.' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin: list households pending verification
|
||||||
|
app.get('/api/houses/pending', authenticateJWT, authorizeRoles('admin'), (req, res) => {
|
||||||
|
const sql = `
|
||||||
|
SELECT pa.id AS assessment_id, h.id AS household_id, h.head_name, h.kk_number, h.head_nik, h.resident_count,
|
||||||
|
h.lat, h.lng, h.linked_place_id, pa.electricity_va, pa.floor_material, pa.wall_material, pa.drinking_water_source, pa.photo_evidence_url, pa.verification_status, pa.surveyor_id, u.username AS surveyor_name, rp.name AS place_name
|
||||||
|
FROM poverty_assessments pa
|
||||||
|
JOIN households h ON h.id = pa.household_id
|
||||||
|
LEFT JOIN users u ON u.id = pa.surveyor_id
|
||||||
|
LEFT JOIN religious_places rp ON rp.id = h.linked_place_id
|
||||||
|
WHERE pa.verification_status = 'pending'
|
||||||
|
ORDER BY pa.id DESC
|
||||||
|
`;
|
||||||
|
db.query(sql, (err, results) => {
|
||||||
|
if (err) return res.status(500).json({ message: 'Database error', error: err.message });
|
||||||
|
res.json(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Analytics & logistics
|
||||||
|
app.get('/api/analytics/sembako-demand', authenticateJWT, (req, res) => {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
rp.id AS place_id,
|
||||||
|
rp.name AS place_name,
|
||||||
|
rp.place_type AS place_type,
|
||||||
|
COUNT(DISTINCT h.id) AS households_needing_aid,
|
||||||
|
SUM(h.resident_count) AS total_residents_needing_aid
|
||||||
|
FROM religious_places rp
|
||||||
|
JOIN households h
|
||||||
|
ON h.linked_place_id = rp.id
|
||||||
|
JOIN poverty_assessments pa
|
||||||
|
ON pa.household_id = h.id
|
||||||
|
AND pa.verification_status = 'verified'
|
||||||
|
AND (
|
||||||
|
pa.electricity_va IN ('none', '450')
|
||||||
|
OR pa.floor_material IN ('dirt', 'bamboo')
|
||||||
|
)
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM distribution_history dh
|
||||||
|
WHERE dh.house_id = h.id
|
||||||
|
AND dh.aid_category = 'sembako'
|
||||||
|
AND dh.verification_status = 'verified'
|
||||||
|
)
|
||||||
|
GROUP BY rp.id, rp.name
|
||||||
|
ORDER BY households_needing_aid DESC
|
||||||
|
`;
|
||||||
|
db.query(sql, (err, results) => {
|
||||||
|
if (err) return res.status(500).json({ message: 'Database error', error: err.message });
|
||||||
|
res.json(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/distribution/log', authenticateJWT, authorizeRoles('admin', 'surveyor'), (req, res) => {
|
||||||
|
const { household_id, place_id, amount_value, notes } = req.body;
|
||||||
|
|
||||||
|
if (!household_id) {
|
||||||
|
return res.status(400).json({ message: 'household_id is required.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
INSERT INTO distribution_history
|
||||||
|
(house_id, place_id, distributed_by, aid_category, amount_value, notes, verification_status)
|
||||||
|
VALUES (?, ?, ?, 'sembako', ?, ?, 'pending')
|
||||||
|
`;
|
||||||
|
db.query(
|
||||||
|
sql,
|
||||||
|
[
|
||||||
|
household_id,
|
||||||
|
place_id || null,
|
||||||
|
req.user.id,
|
||||||
|
amount_value || 1,
|
||||||
|
notes || null
|
||||||
|
],
|
||||||
|
(err, result) => {
|
||||||
|
if (err) return res.status(500).json({ message: 'Failed to log distribution.', error: err.message });
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'Sembako distribution logged successfully.',
|
||||||
|
distribution_id: result.insertId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/distribution/pending', authenticateJWT, authorizeRoles('admin'), (req, res) => {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
dh.id,
|
||||||
|
dh.house_id,
|
||||||
|
dh.place_id,
|
||||||
|
dh.distributed_by,
|
||||||
|
dh.amount_value,
|
||||||
|
dh.notes,
|
||||||
|
dh.verification_status,
|
||||||
|
dh.verified_by,
|
||||||
|
dh.verified_at,
|
||||||
|
h.head_name,
|
||||||
|
h.kk_number,
|
||||||
|
h.head_nik,
|
||||||
|
h.resident_count,
|
||||||
|
r.name AS place_name,
|
||||||
|
u.username AS distributed_by_name
|
||||||
|
FROM distribution_history dh
|
||||||
|
LEFT JOIN households h ON h.id = dh.house_id
|
||||||
|
LEFT JOIN religious_places r ON r.id = dh.place_id
|
||||||
|
LEFT JOIN users u ON u.id = dh.distributed_by
|
||||||
|
WHERE dh.aid_category = 'sembako'
|
||||||
|
AND dh.verification_status = 'pending'
|
||||||
|
ORDER BY dh.id DESC
|
||||||
|
`;
|
||||||
|
db.query(sql, (err, results) => {
|
||||||
|
if (err) return res.status(500).json({ message: 'Database error', error: err.message });
|
||||||
|
res.json(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/distribution/verify/:id', authenticateJWT, authorizeRoles('admin'), (req, res) => {
|
||||||
|
const sql = `
|
||||||
|
UPDATE distribution_history
|
||||||
|
SET verification_status = 'verified', verified_by = ?, verified_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
AND aid_category = 'sembako'
|
||||||
|
AND verification_status = 'pending'
|
||||||
|
`;
|
||||||
|
db.query(sql, [req.user.id, req.params.id], (err, result) => {
|
||||||
|
if (err) return res.status(500).json({ message: 'Database error', error: err.message });
|
||||||
|
if (result.affectedRows === 0) {
|
||||||
|
return res.status(404).json({ message: 'Pending handover not found or already verified.' });
|
||||||
|
}
|
||||||
|
res.json({ message: 'Handover verified successfully.' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE endpoints
|
||||||
|
app.delete('/api/places/:id', authenticateJWT, authorizeRoles('admin'), (req, res) => {
|
||||||
|
db.query('DELETE FROM religious_places WHERE id=?', [req.params.id], (err) => {
|
||||||
|
if (err) res.status(500).send(err);
|
||||||
|
else res.sendStatus(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/houses/:id', authenticateJWT, authorizeRoles('admin'), (req, res) => {
|
||||||
|
db.query('DELETE FROM households WHERE id=?', [req.params.id], (err) => {
|
||||||
|
if (err) res.status(500).send(err);
|
||||||
|
else res.sendStatus(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Unified WebGIS Portal server running on port ${PORT}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user