diff --git a/app.py b/app.py new file mode 100644 index 0000000..092b73f --- /dev/null +++ b/app.py @@ -0,0 +1,321 @@ +from flask import Flask, jsonify, request +from flask_cors import CORS +import psycopg2 +import psycopg2.extras +import json + +app = Flask(__name__) +# Mengizinkan semua origin (termasuk port 5173 milik React) untuk mengakses API +CORS(app, resources={r"/api/*": {"origins": "*"}}) + +# ========================================================================= +# KONEKSI DATABASE NEON (SERVERLESS POSTGRESQL) +# ========================================================================= +# Ganti string di bawah ini dengan Connection String asli dari dashboard Neon kamu! +NEON_DB_URI = "postgresql://neondb_owner:npg_l27gXGaeChPq@ep-quiet-morning-aobokj6d.c-2.ap-southeast-1.aws.neon.tech/neondb?sslmode=require" + +def get_db_connection(): + return psycopg2.connect(NEON_DB_URI) + +def generate_next_id(prefix, table_name): + """Mencari akhiran ID numerik tertinggi secara aman menggunakan Tuple index""" + conn = None + cursor = None + try: + conn = get_db_connection() + cursor = conn.cursor() # Menggunakan cursor biasa (tuple) agar lebih aman & cepat + cursor.execute(f"SELECT id FROM {table_name} ORDER BY id DESC LIMIT 1") + row = cursor.fetchone() + + if not row or row[0] is None: + return f"{prefix}001" + + # Ambil string ID (misal 'RI001' atau 'P003') dari index ke-0 tuple + last_id = str(row[0]) + current_num = int(''.join(filter(str.isdigit, last_id))) + next_num = current_num + 1 + return f"{prefix}{next_num:03d}" + except Exception as e: + print(f"Error generating ID untuk {table_name}: {e}") + import random + return f"{prefix}{random.randint(100, 999)}" + finally: + if cursor: cursor.close() + if conn: conn.close() + +# ========================================================================= +# API ENDPOINTS (GET METHOD) +# ========================================================================= + +@app.route('/api/jalan', methods=['GET']) +def get_jalan(): + conn = get_db_connection() + cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + + # Mengambil field panjang yang sekarang sudah terisi otomatis dalam satuan meter + cursor.execute("SELECT id, nama, status, panjang, ST_AsGeoJSON(geom) as geom_json FROM jalan") + rows = cursor.fetchall() + + for row in rows: + if row['geom_json']: + row['geom'] = json.loads(row['geom_json']) + del row['geom_json'] + + # Opsional: Membulatkan nilai panjang jalan menjadi 2 angka di belakang koma demi estetika UI + if row['panjang'] is not None: + row['panjang'] = round(row['panjang'], 2) + + cursor.close() + conn.close() + return jsonify(rows) + +@app.route('/api/area', methods=['GET']) +def get_area(): + conn = get_db_connection() + cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + + # Mengambil semua field data area dari database + cursor.execute("SELECT id, nama, luas, ST_AsGeoJSON(geom) as geom_json FROM area") + rows = cursor.fetchall() + + for row in rows: + if row['geom_json']: + row['geom'] = json.loads(row['geom_json']) + del row['geom_json'] + + # Opsional: Membulatkan nilai luas wilayah menjadi 2 angka di belakang koma untuk merapikan UI + if row['luas'] is not None: + row['luas'] = round(row['luas'], 2) + + cursor.close() + conn.close() + return jsonify(rows) + +@app.route('/api/ibadah', methods=['GET']) +def get_ibadah(): + conn = get_db_connection() + cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cursor.execute("SELECT id, nama, jenis, kontak, radius, ST_AsGeoJSON(geom) as geom_json FROM rumah_ibadah") + rows = cursor.fetchall() + + for row in rows: + if row['geom_json']: + row['geom'] = json.loads(row['geom_json']) + del row['geom_json'] + + cursor.close() + conn.close() + return jsonify(rows) + +@app.route('/api/miskin', methods=['GET']) +def get_miskin(): + conn = get_db_connection() + cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + + try: + cursor.execute(""" + SELECT id, kepala_keluarga, jumlah_anggota, status_bantuan, jenis_bantuan, id_ibadah_terdekat, + ST_AsGeoJSON(geom) as geom_json + FROM penduduk_miskin + """) + rows = cursor.fetchall() + + for row in rows: + if row['geom_json']: + row['geom'] = json.loads(row['geom_json']) + del row['geom_json'] + + # Memisahkan kembali string koma menjadi Array murni untuk kebutuhan state di React + if row['jenis_bantuan']: + row['jenis_bantuan'] = [b.strip() for b in row['jenis_bantuan'].split(',')] + else: + row['jenis_bantuan'] = [] + + response = rows + except Exception as e: + response = {"status": "error", "message": str(e)} + print(f"ERROR: Failed to fetch penduduk miskin: {e}") + finally: + cursor.close() + conn.close() + + return jsonify(response) + +# ========================================================================= +# API ENDPOINTS (POST METHOD) +# ========================================================================= + +@app.route('/api/ibadah/create', methods=['POST']) +def create_ibadah(): + data = request.json + next_id = generate_next_id('RI', 'rumah_ibadah') + + conn = get_db_connection() + cursor = conn.cursor() + + query = """ + INSERT INTO rumah_ibadah (id, nama, jenis, kontak, radius, geom) + VALUES (%s, %s, %s, %s, %s, ST_GeomFromText(%s, 4326)) + """ + wkt_point = f"POINT({data['lng']} {data['lat']})" + + try: + cursor.execute(query, ( + next_id, + data['nama'], + data['jenis'], + data.get('kontak', ''), # Menangani jika kontak kosong + int(data['radius']), + wkt_point + )) + conn.commit() + response = {"status": "success", "generated_id": next_id} + print("SUCCESS: Rumah ibadah successfully saved to database!") + except Exception as e: + conn.rollback() + response = {"status": "error", "message": str(e)} + print(f"ERROR: Rumah ibadah insertion failed: {e}") + finally: + cursor.close() + conn.close() + + return jsonify(response) + +@app.route('/api/miskin/create', methods=['POST']) +def create_miskin(): + data = request.json + next_id = generate_next_id('P', 'penduduk_miskin') + wkt_point = f"POINT({data['lng']} {data['lat']})" + + conn = get_db_connection() + cursor = conn.cursor() + + query = """ + INSERT INTO penduduk_miskin (id, kepala_keluarga, jumlah_anggota, status_bantuan, jenis_bantuan, id_ibadah_terdekat, geom) + VALUES ( + %s, %s, %s, %s, %s, + (SELECT id FROM rumah_ibadah ORDER BY geom::geography <-> ST_GeomFromText(%s, 4326)::geography LIMIT 1), + ST_GeomFromText(%s, 4326) + ) + RETURNING id_ibadah_terdekat; + """ + + # Mengubah array Python ['PKH', 'BPJS_Kesehatan'] menjadi gabungan string 'PKH,BPJS_Kesehatan' + if isinstance(data.get('jenis_bantuan'), list): + jenis_bantuan_str = ",".join(data['jenis_bantuan']) + else: + jenis_bantuan_str = data.get('jenis_bantuan', '') + + try: + cursor.execute(query, ( + next_id, + data['kepala_keluarga'], + data['jumlah_anggota'], + data['status_bantuan'], + jenis_bantuan_str, + wkt_point, + wkt_point + )) + + # Mengambil ID rumah ibadah terdekat yang berhasil dihitung otomatis oleh PostGIS + closest_ibadah_id = cursor.fetchone()[0] + conn.commit() + + response = { + "status": "success", + "generated_id": next_id, + "closest_hub": closest_ibadah_id, + "message": "Data penduduk miskin berhasil disimpan" + } + print("SUCCESS: Penduduk miskin successfully saved to database!") + + except Exception as e: + conn.rollback() + response = {"status": "error", "message": str(e)} + print(f"ERROR: Penduduk miskin insertion failed: {e}") + + finally: + cursor.close() + conn.close() + + return jsonify(response) + +@app.route('/api/jalan/create', methods=['POST']) +def create_jalan(): + data = request.json + conn = get_db_connection() + cursor = conn.cursor() + + coords_str = ", ".join([f"{pt[1]} {pt[0]}" for pt in data['coords']]) + wkt_line = f"LINESTRING({coords_str})" + + # MENGGUNAKAN ST_Length agar kolom panjang (double precision) terisi otomatis dengan angka murni meter + query = """ + INSERT INTO jalan (nama, status, panjang, geom) + VALUES ( + %s, + %s, + ST_Length(ST_GeomFromText(%s, 4326)::geography), + ST_GeomFromText(%s, 4326) + ) + """ + + try: + # Kita passing wkt_line dua kali (untuk hitung panjang dan untuk geometri) + cursor.execute(query, (data['nama'], data['status'], wkt_line, wkt_line)) + conn.commit() + response = {"status": "success", "message": "Jalan successfully created"} + print("SUCCESS: Jalan successfully saved to database!") + except Exception as e: + conn.rollback() + response = {"status": "error", "message": str(e)} + print(f"ERROR: Jalan insertion failed: {e}") + finally: + cursor.close() + conn.close() + + return jsonify(response) + +@app.route('/api/area/create', methods=['POST']) +def create_area(): + data = request.json + conn = get_db_connection() + cursor = conn.cursor() + + coords = list(data['coords']) + if coords[0] != coords[-1]: + coords.append(coords[0]) + + coords_str = ", ".join([f"{pt[1]} {pt[0]}" for pt in coords]) + wkt_poly = f"POLYGON(({coords_str}))" + + # We use exactly two %s because both ST_Area and ST_GeomFromText + # will read from the same exact wkt_poly variable we pass in. + query = """ + INSERT INTO area (nama, luas, geom) + VALUES ( + %s, + ST_Area(ST_GeomFromText(%s, 4326)::geography), + ST_GeomFromText(%s, 4326) + ) + """ + + try: + cursor.execute(query, (data['nama'], wkt_poly, wkt_poly)) + conn.commit() + response = {"status": "success", "message": "Area successfully created"} + # GANTI DARI EMOJI JADI TEKS BIASA BIAR TERMINAL WINDOWS TIDAK CRASH: + print("SUCCESS: Area successfully saved to database!") + except Exception as e: + conn.rollback() + response = {"status": "error", "message": str(e)} + # GANTI JUGA YANG INI: + print(f"ERROR: Database insertion failed: {e}") + finally: + cursor.close() + conn.close() + + return jsonify(response) + +if __name__ == '__main__': + app.run(debug=True, port=5000) \ No newline at end of file