diff --git a/Rents_Only.postman_collection.json b/Rents_Only.postman_collection.json new file mode 100644 index 0000000..aa26f91 --- /dev/null +++ b/Rents_Only.postman_collection.json @@ -0,0 +1,204 @@ +{ + "info": { + "name": "Dubai DLD - Rents Only", + "_postman_id": "f3c2a1b8-6d5e-4e9a-93d1-rent-only-001", + "description": "Collection with only recent rents connection examples for the /api/rents/recent endpoint.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:3000" + } + ], + "item": [ + { + "name": "Recent Rents - No Filters (default limit=30)", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "rents", + "recent" + ] + } + } + }, + { + "name": "Filter by area_name (contains, case-insensitive)", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?area_name=business%20bay&limit=30", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "rents", + "recent" + ], + "query": [ + { "key": "area_name", "value": "business bay" }, + { "key": "limit", "value": "30" } + ] + } + } + }, + { + "name": "Filter by property_type (matches prop_type_en or prop_sub_type_en)", + "request": { + "method": "GET", + "header": [ + { "key": "Accept", "value": "application/json" } + ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?property_type=unit&limit=30", + "host": [ "{{baseUrl}}" ], + "path": [ "api", "rents", "recent" ], + "query": [ + { "key": "property_type", "value": "unit" }, + { "key": "limit", "value": "30" } + ] + } + } + }, + { + "name": "Filter by rooms (decimal values: 1, 2, 3, 4, 5)", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?rooms=3&limit=30", + "host": [ "{{baseUrl}}" ], + "path": [ "api", "rents", "recent" ], + "query": [ + { "key": "rooms", "value": "3" }, + { "key": "limit", "value": "30" } + ] + } + } + }, + { + "name": "Filter by project (matches project_en or master_project_en)", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?project=burj%20views&limit=30", + "host": [ "{{baseUrl}}" ], + "path": [ "api", "rents", "recent" ], + "query": [ + { "key": "project", "value": "burj views" }, + { "key": "limit", "value": "30" } + ] + } + } + }, + { + "name": "Filter by size_min and size_max (range)", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?size_min=1000&size_max=5000&limit=30", + "host": [ "{{baseUrl}}" ], + "path": [ "api", "rents", "recent" ], + "query": [ + { "key": "size_min", "value": "1000" }, + { "key": "size_max", "value": "5000" }, + { "key": "limit", "value": "30" } + ] + } + } + }, + { + "name": "Filter by only size_min (greater or equal)", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?size_min=1500&limit=30", + "host": [ "{{baseUrl}}" ], + "path": [ "api", "rents", "recent" ], + "query": [ + { "key": "size_min", "value": "1500" }, + { "key": "limit", "value": "30" } + ] + } + } + }, + { + "name": "Filter by only size_max (less or equal)", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?size_max=3000&limit=30", + "host": [ "{{baseUrl}}" ], + "path": [ "api", "rents", "recent" ], + "query": [ + { "key": "size_max", "value": "3000" }, + { "key": "limit", "value": "30" } + ] + } + } + }, + { + "name": "Combined filters (area + type + rooms + project + size range)", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?area_name=business%20bay&property_type=unit&rooms=2&project=sami%20q%20tower&size_min=50&size_max=100&limit=50", + "host": [ "{{baseUrl}}" ], + "path": [ "api", "rents", "recent" ], + "query": [ + { "key": "area_name", "value": "business bay" }, + { "key": "property_type", "value": "unit" }, + { "key": "rooms", "value": "2" }, + { "key": "project", "value": "sami q tower" }, + { "key": "size_min", "value": "50" }, + { "key": "size_max", "value": "100" }, + { "key": "limit", "value": "50" } + ] + } + } + }, + { + "name": "Limit override (top N)", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?limit=100", + "host": [ "{{baseUrl}}" ], + "path": [ "api", "rents", "recent" ], + "query": [ + { "key": "limit", "value": "100" } + ] + } + } + } + ] +} + + + diff --git a/Rents_Only_Paginated.postman_collection.json b/Rents_Only_Paginated.postman_collection.json new file mode 100644 index 0000000..47c9e52 --- /dev/null +++ b/Rents_Only_Paginated.postman_collection.json @@ -0,0 +1,155 @@ +{ + "info": { + "name": "Dubai DLD - Rents Only (Paginated)", + "_postman_id": "c0a5b3e6-6b0f-4b9e-9f77-rents-only-v2", + "description": "Only /api/rents/recent examples, covering filters, legacy limit, and server-side pagination.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { "key": "baseUrl", "value": "http://localhost:3000" } + ], + "item": [ + { + "name": "Recent - No filters (all rows, no LIMIT)", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { "raw": "{{baseUrl}}/api/rents/recent", "host": ["{{baseUrl}}"], "path": ["api","rents","recent"] } + } + }, + { + "name": "Legacy: limit only (top N without OFFSET)", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?limit=10", + "host": ["{{baseUrl}}"], + "path": ["api","rents","recent"], + "query": [ {"key":"limit","value":"10"} ] + } + } + }, + { + "name": "Pagination: page 1, size 30", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?page=1&page_size=30", + "host": ["{{baseUrl}}"], + "path": ["api","rents","recent"], + "query": [ {"key":"page","value":"1"}, {"key":"page_size","value":"30"} ] + } + } + }, + { + "name": "Pagination: page 2, size 30", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?page=2&page_size=30", + "host": ["{{baseUrl}}"], + "path": ["api","rents","recent"], + "query": [ {"key":"page","value":"2"}, {"key":"page_size","value":"30"} ] + } + } + }, + { + "name": "Filter: area_name + pagination", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?area_name=business%20bay&page=1&page_size=50", + "host": ["{{baseUrl}}"], + "path": ["api","rents","recent"], + "query": [ + {"key":"area_name","value":"business bay"}, + {"key":"page","value":"1"}, + {"key":"page_size","value":"50"} + ] + } + } + }, + { + "name": "Filter: property_type + rooms + pagination", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?property_type=unit&rooms=2&page=1&page_size=30", + "host": ["{{baseUrl}}"], + "path": ["api","rents","recent"], + "query": [ + {"key":"property_type","value":"unit"}, + {"key":"rooms","value":"2"}, + {"key":"page","value":"1"}, + {"key":"page_size","value":"30"} + ] + } + } + }, + { + "name": "Filter: size range + pagination", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?size_min=1000&size_max=5000&page=1&page_size=30", + "host": ["{{baseUrl}}"], + "path": ["api","rents","recent"], + "query": [ + {"key":"size_min","value":"1000"}, + {"key":"size_max","value":"5000"}, + {"key":"page","value":"1"}, + {"key":"page_size","value":"30"} + ] + } + } + }, + { + "name": "Filter: project + pagination", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?project=burj%20views&page=1&page_size=30", + "host": ["{{baseUrl}}"], + "path": ["api","rents","recent"], + "query": [ + {"key":"project","value":"burj views"}, + {"key":"page","value":"1"}, + {"key":"page_size","value":"30"} + ] + } + } + }, + { + "name": "Combined filters + pagination", + "request": { + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], + "url": { + "raw": "{{baseUrl}}/api/rents/recent?area_name=business%20bay&property_type=unit&rooms=2&project=sami%20q%20tower&size_min=50&size_max=100&page=1&page_size=50", + "host": ["{{baseUrl}}"], + "path": ["api","rents","recent"], + "query": [ + {"key":"area_name","value":"business bay"}, + {"key":"property_type","value":"unit"}, + {"key":"rooms","value":"2"}, + {"key":"project","value":"sami q tower"}, + {"key":"size_min","value":"50"}, + {"key":"size_max","value":"100"}, + {"key":"page","value":"1"}, + {"key":"page_size","value":"50"} + ] + } + } + } + ] +} + + + diff --git a/configure_db.sh b/configure_db.sh old mode 100755 new mode 100644 diff --git a/node_modules/.bin/baseline-browser-mapping b/node_modules/.bin/baseline-browser-mapping index d296188..e69de29 120000 --- a/node_modules/.bin/baseline-browser-mapping +++ b/node_modules/.bin/baseline-browser-mapping @@ -1 +0,0 @@ -../baseline-browser-mapping/dist/cli.js \ No newline at end of file diff --git a/node_modules/.bin/browserslist b/node_modules/.bin/browserslist index 3cd991b..e69de29 120000 --- a/node_modules/.bin/browserslist +++ b/node_modules/.bin/browserslist @@ -1 +0,0 @@ -../browserslist/cli.js \ No newline at end of file diff --git a/node_modules/.bin/create-jest b/node_modules/.bin/create-jest index 8d6301e..e69de29 120000 --- a/node_modules/.bin/create-jest +++ b/node_modules/.bin/create-jest @@ -1 +0,0 @@ -../create-jest/bin/create-jest.js \ No newline at end of file diff --git a/node_modules/.bin/esparse b/node_modules/.bin/esparse index 7423b18..e69de29 120000 --- a/node_modules/.bin/esparse +++ b/node_modules/.bin/esparse @@ -1 +0,0 @@ -../esprima/bin/esparse.js \ No newline at end of file diff --git a/node_modules/.bin/esvalidate b/node_modules/.bin/esvalidate index 16069ef..e69de29 120000 --- a/node_modules/.bin/esvalidate +++ b/node_modules/.bin/esvalidate @@ -1 +0,0 @@ -../esprima/bin/esvalidate.js \ No newline at end of file diff --git a/node_modules/.bin/import-local-fixture b/node_modules/.bin/import-local-fixture index ff4b104..e69de29 120000 --- a/node_modules/.bin/import-local-fixture +++ b/node_modules/.bin/import-local-fixture @@ -1 +0,0 @@ -../import-local/fixtures/cli.js \ No newline at end of file diff --git a/node_modules/.bin/jest b/node_modules/.bin/jest index 61c1861..e69de29 120000 --- a/node_modules/.bin/jest +++ b/node_modules/.bin/jest @@ -1 +0,0 @@ -../jest/bin/jest.js \ No newline at end of file diff --git a/node_modules/.bin/js-yaml b/node_modules/.bin/js-yaml index 9dbd010..e69de29 120000 --- a/node_modules/.bin/js-yaml +++ b/node_modules/.bin/js-yaml @@ -1 +0,0 @@ -../js-yaml/bin/js-yaml.js \ No newline at end of file diff --git a/node_modules/.bin/jsesc b/node_modules/.bin/jsesc index 7237604..e69de29 120000 --- a/node_modules/.bin/jsesc +++ b/node_modules/.bin/jsesc @@ -1 +0,0 @@ -../jsesc/bin/jsesc \ No newline at end of file diff --git a/node_modules/.bin/json5 b/node_modules/.bin/json5 index 217f379..e69de29 120000 --- a/node_modules/.bin/json5 +++ b/node_modules/.bin/json5 @@ -1 +0,0 @@ -../json5/lib/cli.js \ No newline at end of file diff --git a/node_modules/.bin/mime b/node_modules/.bin/mime index fbb7ee0..e69de29 120000 --- a/node_modules/.bin/mime +++ b/node_modules/.bin/mime @@ -1 +0,0 @@ -../mime/cli.js \ No newline at end of file diff --git a/node_modules/.bin/node-which b/node_modules/.bin/node-which index 6f8415e..e69de29 120000 --- a/node_modules/.bin/node-which +++ b/node_modules/.bin/node-which @@ -1 +0,0 @@ -../which/bin/node-which \ No newline at end of file diff --git a/node_modules/.bin/nodemon b/node_modules/.bin/nodemon index 1056ddc..e69de29 120000 --- a/node_modules/.bin/nodemon +++ b/node_modules/.bin/nodemon @@ -1 +0,0 @@ -../nodemon/bin/nodemon.js \ No newline at end of file diff --git a/node_modules/.bin/nodetouch b/node_modules/.bin/nodetouch index 3409fdb..e69de29 120000 --- a/node_modules/.bin/nodetouch +++ b/node_modules/.bin/nodetouch @@ -1 +0,0 @@ -../touch/bin/nodetouch.js \ No newline at end of file diff --git a/node_modules/.bin/parser b/node_modules/.bin/parser index ce7bf97..e69de29 120000 --- a/node_modules/.bin/parser +++ b/node_modules/.bin/parser @@ -1 +0,0 @@ -../@babel/parser/bin/babel-parser.js \ No newline at end of file diff --git a/node_modules/.bin/resolve b/node_modules/.bin/resolve index b6afda6..e69de29 120000 --- a/node_modules/.bin/resolve +++ b/node_modules/.bin/resolve @@ -1 +0,0 @@ -../resolve/bin/resolve \ No newline at end of file diff --git a/node_modules/.bin/semver b/node_modules/.bin/semver index 5aaadf4..e69de29 120000 --- a/node_modules/.bin/semver +++ b/node_modules/.bin/semver @@ -1 +0,0 @@ -../semver/bin/semver.js \ No newline at end of file diff --git a/node_modules/.bin/update-browserslist-db b/node_modules/.bin/update-browserslist-db index b11e16f..e69de29 120000 --- a/node_modules/.bin/update-browserslist-db +++ b/node_modules/.bin/update-browserslist-db @@ -1 +0,0 @@ -../update-browserslist-db/cli.js \ No newline at end of file diff --git a/node_modules/.bin/uuid b/node_modules/.bin/uuid index 588f70e..e69de29 120000 --- a/node_modules/.bin/uuid +++ b/node_modules/.bin/uuid @@ -1 +0,0 @@ -../uuid/dist/bin/uuid \ No newline at end of file diff --git a/node_modules/@babel/parser/bin/babel-parser.js b/node_modules/@babel/parser/bin/babel-parser.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/LICENSE.md b/node_modules/@hapi/hoek/LICENSE.md old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/README.md b/node_modules/@hapi/hoek/README.md old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/applyToDefaults.js b/node_modules/@hapi/hoek/lib/applyToDefaults.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/assert.js b/node_modules/@hapi/hoek/lib/assert.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/bench.js b/node_modules/@hapi/hoek/lib/bench.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/block.js b/node_modules/@hapi/hoek/lib/block.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/clone.js b/node_modules/@hapi/hoek/lib/clone.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/contain.js b/node_modules/@hapi/hoek/lib/contain.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/deepEqual.js b/node_modules/@hapi/hoek/lib/deepEqual.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/error.js b/node_modules/@hapi/hoek/lib/error.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/escapeHeaderAttribute.js b/node_modules/@hapi/hoek/lib/escapeHeaderAttribute.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/escapeHtml.js b/node_modules/@hapi/hoek/lib/escapeHtml.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/escapeJson.js b/node_modules/@hapi/hoek/lib/escapeJson.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/escapeRegex.js b/node_modules/@hapi/hoek/lib/escapeRegex.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/flatten.js b/node_modules/@hapi/hoek/lib/flatten.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/ignore.js b/node_modules/@hapi/hoek/lib/ignore.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/index.d.ts b/node_modules/@hapi/hoek/lib/index.d.ts old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/index.js b/node_modules/@hapi/hoek/lib/index.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/intersect.js b/node_modules/@hapi/hoek/lib/intersect.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/isPromise.js b/node_modules/@hapi/hoek/lib/isPromise.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/merge.js b/node_modules/@hapi/hoek/lib/merge.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/once.js b/node_modules/@hapi/hoek/lib/once.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/reach.js b/node_modules/@hapi/hoek/lib/reach.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/reachTemplate.js b/node_modules/@hapi/hoek/lib/reachTemplate.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/stringify.js b/node_modules/@hapi/hoek/lib/stringify.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/types.js b/node_modules/@hapi/hoek/lib/types.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/utils.js b/node_modules/@hapi/hoek/lib/utils.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/lib/wait.js b/node_modules/@hapi/hoek/lib/wait.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/hoek/package.json b/node_modules/@hapi/hoek/package.json old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/topo/LICENSE.md b/node_modules/@hapi/topo/LICENSE.md old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/topo/README.md b/node_modules/@hapi/topo/README.md old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/topo/lib/index.d.ts b/node_modules/@hapi/topo/lib/index.d.ts old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/topo/lib/index.js b/node_modules/@hapi/topo/lib/index.js old mode 100755 new mode 100644 diff --git a/node_modules/@hapi/topo/package.json b/node_modules/@hapi/topo/package.json old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/address/README.md b/node_modules/@sideway/address/README.md old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/address/lib/decode.js b/node_modules/@sideway/address/lib/decode.js old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/address/lib/domain.js b/node_modules/@sideway/address/lib/domain.js old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/address/lib/email.js b/node_modules/@sideway/address/lib/email.js old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/address/lib/errors.js b/node_modules/@sideway/address/lib/errors.js old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/address/lib/index.d.ts b/node_modules/@sideway/address/lib/index.d.ts old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/address/lib/index.js b/node_modules/@sideway/address/lib/index.js old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/address/lib/ip.js b/node_modules/@sideway/address/lib/ip.js old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/address/lib/tlds.js b/node_modules/@sideway/address/lib/tlds.js old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/address/lib/uri.js b/node_modules/@sideway/address/lib/uri.js old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/address/package.json b/node_modules/@sideway/address/package.json old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/formula/README.md b/node_modules/@sideway/formula/README.md old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/formula/lib/index.d.ts b/node_modules/@sideway/formula/lib/index.d.ts old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/formula/lib/index.js b/node_modules/@sideway/formula/lib/index.js old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/formula/package.json b/node_modules/@sideway/formula/package.json old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/pinpoint/LICENSE.md b/node_modules/@sideway/pinpoint/LICENSE.md old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/pinpoint/README.md b/node_modules/@sideway/pinpoint/README.md old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/pinpoint/lib/index.d.ts b/node_modules/@sideway/pinpoint/lib/index.d.ts old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/pinpoint/lib/index.js b/node_modules/@sideway/pinpoint/lib/index.js old mode 100755 new mode 100644 diff --git a/node_modules/@sideway/pinpoint/package.json b/node_modules/@sideway/pinpoint/package.json old mode 100755 new mode 100644 diff --git a/node_modules/aws-ssl-profiles/package.json b/node_modules/aws-ssl-profiles/package.json old mode 100755 new mode 100644 diff --git a/node_modules/baseline-browser-mapping/dist/cli.js b/node_modules/baseline-browser-mapping/dist/cli.js old mode 100755 new mode 100644 diff --git a/node_modules/browserslist/cli.js b/node_modules/browserslist/cli.js old mode 100755 new mode 100644 diff --git a/node_modules/bson/etc/prepare.js b/node_modules/bson/etc/prepare.js old mode 100755 new mode 100644 diff --git a/node_modules/cjs-module-lexer/LICENSE b/node_modules/cjs-module-lexer/LICENSE old mode 100755 new mode 100644 diff --git a/node_modules/cjs-module-lexer/README.md b/node_modules/cjs-module-lexer/README.md old mode 100755 new mode 100644 diff --git a/node_modules/cjs-module-lexer/lexer.d.ts b/node_modules/cjs-module-lexer/lexer.d.ts old mode 100755 new mode 100644 diff --git a/node_modules/cjs-module-lexer/lexer.js b/node_modules/cjs-module-lexer/lexer.js old mode 100755 new mode 100644 diff --git a/node_modules/cjs-module-lexer/package.json b/node_modules/cjs-module-lexer/package.json old mode 100755 new mode 100644 diff --git a/node_modules/create-jest/bin/create-jest.js b/node_modules/create-jest/bin/create-jest.js old mode 100755 new mode 100644 diff --git a/node_modules/esprima/bin/esparse.js b/node_modules/esprima/bin/esparse.js old mode 100755 new mode 100644 diff --git a/node_modules/esprima/bin/esvalidate.js b/node_modules/esprima/bin/esvalidate.js old mode 100755 new mode 100644 diff --git a/node_modules/exit/test/fixtures/create-files.sh b/node_modules/exit/test/fixtures/create-files.sh old mode 100755 new mode 100644 diff --git a/node_modules/import-local/fixtures/cli.js b/node_modules/import-local/fixtures/cli.js old mode 100755 new mode 100644 diff --git a/node_modules/istanbul-lib-instrument/node_modules/.bin/semver b/node_modules/istanbul-lib-instrument/node_modules/.bin/semver index 5aaadf4..e69de29 120000 --- a/node_modules/istanbul-lib-instrument/node_modules/.bin/semver +++ b/node_modules/istanbul-lib-instrument/node_modules/.bin/semver @@ -1 +0,0 @@ -../semver/bin/semver.js \ No newline at end of file diff --git a/node_modules/istanbul-lib-instrument/node_modules/semver/bin/semver.js b/node_modules/istanbul-lib-instrument/node_modules/semver/bin/semver.js old mode 100755 new mode 100644 diff --git a/node_modules/jest-cli/bin/jest.js b/node_modules/jest-cli/bin/jest.js old mode 100755 new mode 100644 diff --git a/node_modules/jest-snapshot/node_modules/.bin/semver b/node_modules/jest-snapshot/node_modules/.bin/semver index 5aaadf4..e69de29 120000 --- a/node_modules/jest-snapshot/node_modules/.bin/semver +++ b/node_modules/jest-snapshot/node_modules/.bin/semver @@ -1 +0,0 @@ -../semver/bin/semver.js \ No newline at end of file diff --git a/node_modules/jest-snapshot/node_modules/semver/bin/semver.js b/node_modules/jest-snapshot/node_modules/semver/bin/semver.js old mode 100755 new mode 100644 diff --git a/node_modules/jest/bin/jest.js b/node_modules/jest/bin/jest.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/LICENSE.md b/node_modules/joi/LICENSE.md old mode 100755 new mode 100644 diff --git a/node_modules/joi/README.md b/node_modules/joi/README.md old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/annotate.js b/node_modules/joi/lib/annotate.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/base.js b/node_modules/joi/lib/base.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/cache.js b/node_modules/joi/lib/cache.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/common.js b/node_modules/joi/lib/common.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/compile.js b/node_modules/joi/lib/compile.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/errors.js b/node_modules/joi/lib/errors.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/extend.js b/node_modules/joi/lib/extend.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/index.js b/node_modules/joi/lib/index.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/manifest.js b/node_modules/joi/lib/manifest.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/messages.js b/node_modules/joi/lib/messages.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/modify.js b/node_modules/joi/lib/modify.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/ref.js b/node_modules/joi/lib/ref.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/schemas.js b/node_modules/joi/lib/schemas.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/state.js b/node_modules/joi/lib/state.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/template.js b/node_modules/joi/lib/template.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/trace.js b/node_modules/joi/lib/trace.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/alternatives.js b/node_modules/joi/lib/types/alternatives.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/any.js b/node_modules/joi/lib/types/any.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/array.js b/node_modules/joi/lib/types/array.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/binary.js b/node_modules/joi/lib/types/binary.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/boolean.js b/node_modules/joi/lib/types/boolean.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/date.js b/node_modules/joi/lib/types/date.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/function.js b/node_modules/joi/lib/types/function.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/keys.js b/node_modules/joi/lib/types/keys.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/link.js b/node_modules/joi/lib/types/link.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/number.js b/node_modules/joi/lib/types/number.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/object.js b/node_modules/joi/lib/types/object.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/string.js b/node_modules/joi/lib/types/string.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/types/symbol.js b/node_modules/joi/lib/types/symbol.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/validator.js b/node_modules/joi/lib/validator.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/lib/values.js b/node_modules/joi/lib/values.js old mode 100755 new mode 100644 diff --git a/node_modules/joi/package.json b/node_modules/joi/package.json old mode 100755 new mode 100644 diff --git a/node_modules/js-yaml/bin/js-yaml.js b/node_modules/js-yaml/bin/js-yaml.js old mode 100755 new mode 100644 diff --git a/node_modules/jsesc/bin/jsesc b/node_modules/jsesc/bin/jsesc old mode 100755 new mode 100644 diff --git a/node_modules/json5/lib/cli.js b/node_modules/json5/lib/cli.js old mode 100755 new mode 100644 diff --git a/node_modules/make-dir/node_modules/.bin/semver b/node_modules/make-dir/node_modules/.bin/semver index 5aaadf4..e69de29 120000 --- a/node_modules/make-dir/node_modules/.bin/semver +++ b/node_modules/make-dir/node_modules/.bin/semver @@ -1 +0,0 @@ -../semver/bin/semver.js \ No newline at end of file diff --git a/node_modules/make-dir/node_modules/semver/bin/semver.js b/node_modules/make-dir/node_modules/semver/bin/semver.js old mode 100755 new mode 100644 diff --git a/node_modules/micromatch/LICENSE b/node_modules/micromatch/LICENSE old mode 100755 new mode 100644 diff --git a/node_modules/mime/cli.js b/node_modules/mime/cli.js old mode 100755 new mode 100644 diff --git a/node_modules/mime/src/build.js b/node_modules/mime/src/build.js old mode 100755 new mode 100644 diff --git a/node_modules/mongodb/etc/prepare.js b/node_modules/mongodb/etc/prepare.js old mode 100755 new mode 100644 diff --git a/node_modules/natural/lib/natural/util/stopwords_nl.js b/node_modules/natural/lib/natural/util/stopwords_nl.js old mode 100755 new mode 100644 diff --git a/node_modules/nodemon/bin/nodemon.js b/node_modules/nodemon/bin/nodemon.js old mode 100755 new mode 100644 diff --git a/node_modules/nodemon/node_modules/.bin/semver b/node_modules/nodemon/node_modules/.bin/semver index 5aaadf4..e69de29 120000 --- a/node_modules/nodemon/node_modules/.bin/semver +++ b/node_modules/nodemon/node_modules/.bin/semver @@ -1 +0,0 @@ -../semver/bin/semver.js \ No newline at end of file diff --git a/node_modules/nodemon/node_modules/semver/bin/semver.js b/node_modules/nodemon/node_modules/semver/bin/semver.js old mode 100755 new mode 100644 diff --git a/node_modules/pretty-format/README.md b/node_modules/pretty-format/README.md old mode 100755 new mode 100644 diff --git a/node_modules/prompts/readme.md b/node_modules/prompts/readme.md old mode 100755 new mode 100644 diff --git a/node_modules/resolve/bin/resolve b/node_modules/resolve/bin/resolve old mode 100755 new mode 100644 diff --git a/node_modules/semver/bin/semver.js b/node_modules/semver/bin/semver.js old mode 100755 new mode 100644 diff --git a/node_modules/sift/README.md b/node_modules/sift/README.md old mode 100755 new mode 100644 diff --git a/node_modules/simple-update-notifier/node_modules/.bin/semver b/node_modules/simple-update-notifier/node_modules/.bin/semver index 5aaadf4..e69de29 120000 --- a/node_modules/simple-update-notifier/node_modules/.bin/semver +++ b/node_modules/simple-update-notifier/node_modules/.bin/semver @@ -1 +0,0 @@ -../semver/bin/semver.js \ No newline at end of file diff --git a/node_modules/simple-update-notifier/node_modules/semver/bin/semver.js b/node_modules/simple-update-notifier/node_modules/semver/bin/semver.js old mode 100755 new mode 100644 diff --git a/node_modules/sisteransi/package.json b/node_modules/sisteransi/package.json old mode 100755 new mode 100644 diff --git a/node_modules/sisteransi/readme.md b/node_modules/sisteransi/readme.md old mode 100755 new mode 100644 diff --git a/node_modules/superagent/node_modules/.bin/mime b/node_modules/superagent/node_modules/.bin/mime index fbb7ee0..e69de29 120000 --- a/node_modules/superagent/node_modules/.bin/mime +++ b/node_modules/superagent/node_modules/.bin/mime @@ -1 +0,0 @@ -../mime/cli.js \ No newline at end of file diff --git a/node_modules/superagent/node_modules/.bin/semver b/node_modules/superagent/node_modules/.bin/semver index 5aaadf4..e69de29 120000 --- a/node_modules/superagent/node_modules/.bin/semver +++ b/node_modules/superagent/node_modules/.bin/semver @@ -1 +0,0 @@ -../semver/bin/semver.js \ No newline at end of file diff --git a/node_modules/superagent/node_modules/mime/cli.js b/node_modules/superagent/node_modules/mime/cli.js old mode 100755 new mode 100644 diff --git a/node_modules/superagent/node_modules/semver/bin/semver.js b/node_modules/superagent/node_modules/semver/bin/semver.js old mode 100755 new mode 100644 diff --git a/node_modules/touch/bin/nodetouch.js b/node_modules/touch/bin/nodetouch.js old mode 100755 new mode 100644 diff --git a/node_modules/update-browserslist-db/cli.js b/node_modules/update-browserslist-db/cli.js old mode 100755 new mode 100644 diff --git a/node_modules/uuid/dist/bin/uuid b/node_modules/uuid/dist/bin/uuid old mode 100755 new mode 100644 diff --git a/node_modules/which/bin/node-which b/node_modules/which/bin/node-which old mode 100755 new mode 100644 diff --git a/node_modules/wrap-ansi/index.js b/node_modules/wrap-ansi/index.js old mode 100755 new mode 100644 diff --git a/public/js/index.js b/public/js/index.js new file mode 100644 index 0000000..70ec75c --- /dev/null +++ b/public/js/index.js @@ -0,0 +1,183 @@ +// Dubai DLD Analytics Dashboard JavaScript +// No inline scripts or handlers to satisfy CSP + +const API_BASE = '/api'; + +// Helper functions +function showLoading(show) { + document.getElementById('loading').style.display = show ? 'block' : 'none'; +} + +function showError(message) { + const errorDiv = document.getElementById('error'); + errorDiv.textContent = message; + errorDiv.style.display = 'block'; +} + +function hideError() { + document.getElementById('error').style.display = 'none'; +} + +// Clear results +function clearResults() { + document.getElementById('cardsContainer').innerHTML = ''; + document.getElementById('chartsContainer').innerHTML = ''; + document.getElementById('queryInput').value = ''; + hideError(); +} + +// Render cards +function renderCards(cards) { + const container = document.getElementById('cardsContainer'); + container.innerHTML = cards.map(card => ` +
+
${card.title}
+
${card.value}
+
${card.subtitle}
+
+ `).join(''); +} + +// Render charts +function renderCharts(visualizations) { + const container = document.getElementById('chartsContainer'); + container.innerHTML = ''; + + visualizations.forEach((viz, index) => { + const chartDiv = document.createElement('div'); + chartDiv.className = 'chart-container'; + chartDiv.innerHTML = ` +
${viz.title}
+ + `; + container.appendChild(chartDiv); + + const canvas = document.getElementById(`chart${index}`); + const ctx = canvas.getContext('2d'); + + new Chart(ctx, { + type: viz.type, + data: viz.data, + options: viz.options || {} + }); + }); +} + +// Process query +async function processQuery(query) { + if (!query || query.trim() === '') { + showError('Please enter a query'); + return; + } + + showLoading(true); + hideError(); + + try { + const response = await fetch(`${API_BASE}/query`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ query }) + }); + + const data = await response.json(); + + if (data.success && data.data) { + // Render cards and charts + if (data.data.cards && data.data.cards.length > 0) { + renderCards(data.data.cards); + } + if (data.data.visualizations && data.data.visualizations.length > 0) { + renderCharts(data.data.visualizations); + } + } else { + showError(data.message || 'Failed to process query'); + } + } catch (error) { + showError('Network error: ' + error.message); + } finally { + showLoading(false); + } +} + +// Event listeners setup +document.addEventListener('DOMContentLoaded', () => { + // Analyze button + const btnAnalyze = document.getElementById('btn-analyze'); + btnAnalyze.addEventListener('click', async () => { + const query = document.getElementById('queryInput').value; + await processQuery(query); + }); + + // Clear button + const btnClear = document.getElementById('btn-clear'); + btnClear.addEventListener('click', clearResults); + + // Predefined query buttons + document.getElementById('btn-rental-trend').addEventListener('click', async () => { + const query = 'Give me the last 6 months rental price trend for Business Bay'; + document.getElementById('queryInput').value = query; + await processQuery(query); + }); + + document.getElementById('btn-top-areas').addEventListener('click', async () => { + const query = 'Which area is having more rental transactions?'; + document.getElementById('queryInput').value = query; + await processQuery(query); + }); + + document.getElementById('btn-project-summary').addEventListener('click', async () => { + const query = 'Brief about the Project'; + document.getElementById('queryInput').value = query; + await processQuery(query); + }); + + document.getElementById('btn-commercial-leasing').addEventListener('click', async () => { + const query = 'Top 5 areas for Commercial leasing and why?'; + document.getElementById('queryInput').value = query; + await processQuery(query); + }); + + document.getElementById('btn-residential-leasing').addEventListener('click', async () => { + const query = 'Top 5 areas for Residential leasing and why?'; + document.getElementById('queryInput').value = query; + await processQuery(query); + }); + + document.getElementById('btn-example-marina').addEventListener('click', async () => { + const query = 'Give me the last 6 months rental price trend for Dubai Marina'; + document.getElementById('queryInput').value = query; + await processQuery(query); + }); + + document.getElementById('btn-example-top-areas').addEventListener('click', async () => { + const query = 'Which area is having more rental transactions?'; + document.getElementById('queryInput').value = query; + await processQuery(query); + }); + + document.getElementById('btn-example-villas').addEventListener('click', async () => { + const query = 'Show me villa transactions in Dubai Marina'; + document.getElementById('queryInput').value = query; + await processQuery(query); + }); + + document.getElementById('btn-example-3bhk').addEventListener('click', async () => { + const query = 'Avg price of 3 BHK apartment by area in last 6 months'; + document.getElementById('queryInput').value = query; + await processQuery(query); + }); + + // Enter key support + document.getElementById('queryInput').addEventListener('keypress', async (e) => { + if (e.key === 'Enter') { + const query = e.target.value; + await processQuery(query); + } + }); +}); + + + diff --git a/public/js/rents.js b/public/js/rents.js new file mode 100644 index 0000000..a611527 --- /dev/null +++ b/public/js/rents.js @@ -0,0 +1,590 @@ +// Rents page logic moved to external file to satisfy CSP (no inline scripts or handlers) + +const API_BASE = '/api'; + +// Area dropdown values for rents +const areas = [ + 'al mamzer', 'jabal ali first', 'al nahda second', 'burj khalifa', + 'al merkadh', 'dubai investment park second', 'al yelayiss 2', + 'dubai investment park first', 'al yelayiss 1', 'al jadaf', + 'al nahda first', 'al suq al kabeer', 'mirdif', 'business bay', + 'madinat hind 4', 'al thanyah third', + 'al qusais industrial fourth', 'al qusais industrial fifth', + 'al warqa first', 'muhaisanah fourth', 'al qusais second', + 'al karama', 'al hudaiba', 'madinat dubai almelaheyah', + 'al safouh second', 'al qusais first', 'wadi al amardi', 'al rega', + 'wadi al safa 5', 'marsa dubai', + 'hadaeq sheikh mohammed bin rashid', 'me\'aisem first', + 'ras al khor industrial third', 'al qusais industrial first', + 'al murqabat', 'wadi al safa 6', 'naif', 'nad al shiba third', + 'al saffa first', 'um suqaim third', 'al khairan first', + 'madinat al mataar', 'saih shuaib 2', 'mankhool', + 'al thanyah fifth', 'al hebiah fourth', 'trade center second', + 'al warsan first', 'nadd hessa', 'port saeed', + 'trade center first', 'al wasl', 'jumeirah first', + 'al goze industrial second', 'nad al shiba first', 'al muteena', + 'al barsha first', 'jabal ali industrial first', 'palm jumeirah', + 'al raffa', 'margham', 'al barshaa south third', 'al satwa', + 'al khawaneej second', 'al bada', 'al hamriya', 'al thanyah first', + 'al barsha south fourth', 'um hurair second', 'al baraha', + 'ras al khor industrial second', 'al goze industrial first', + 'al hebiah fifth', 'jumeirah second', 'al mararr', + 'wadi al safa 3', 'um suqaim second', 'hor al anz', 'al kheeran', + 'wadi al safa 2', 'hor al anz east', 'al manara', 'eyal nasser', + 'al yufrah 1', 'al khawaneej first', 'hatta', 'warsan fourth', + 'um ramool', 'al goze fourth', 'al goze third', + 'al goze industrial third', 'al barsha south fifth', + 'al hebiah sixth', 'saih shuaib 3', 'al goze first', 'abu hail', + 'al hebiah third', 'al hebiah second', 'rega al buteen', + 'al goze industrial fourth', 'nad al hamar', 'al jafliya', + 'al barshaa south second', 'al qusais industrial second', + 'oud metha', 'um suqaim first', 'jabal ali', 'jumeirah third', + 'lehbab second', 'al dhagaya', 'al lusaily', 'hessyan first', + 'al khabeesi', 'wadi al safa 7', 'al hebiah first', + 'al thanayah fourth', 'al ras', 'muhaisanah second', + 'muhaisanah first', 'al-cornich', 'al mizhar third', + 'al rashidiya', 'cornich deira', 'lehbab first', 'um al sheif', + 'al garhoud', 'al yufrah 2', 'al yufrah 3', 'al saffa second', + 'madinat hind 1', 'al sabkha', 'ras al khor industrial first', + 'al buteen', 'jabal ali industrial second', 'al safouh first', + 'al aweer first', 'al eyas', 'al mizhar first', + 'al qusais industrial third', 'um hurair first', 'al kifaf', + 'zaabeel first', 'al waheda', 'zaabeel second', 'ghadeer al tair', + 'al twar first', 'grayteesah', 'al warsan third', 'al ttay', + 'nad al shiba second', 'al barsha second', 'al athbah', + 'al warqa fourth' +]; + +// Projects list from rents +const projects = [ + 'starz tower by danube', 'azizi feirouz i', + 'candace acacia hotel apartments', 'burj al nujoom', + 'azizi riviera 35', 'shams townhouses', 'reem-mira community ph 1', + 'schon business park', 'damac hills (2) - albizia', + 'reem - mira oasis community', 'trident grand', 'collective 2.0', + 'lakeside', 'marina vista', 'madinat jumeriah living - phase 2', + 'creek beach - surf', 'creek beach - bayshore', + 'creek beach - sunset', 'creek beach - breeze', + 'creek beach - vida residences', 'creek beach - orchid', + 'creek beach - savanna-cedar-mangrove', + 'creek beach - canopy - moor', 'creek beach - summer', + 'creek beach - grove', 'creek beach - rosewater', + 'creek beach - lotus', 'azizi riviera 46', 'sahara meadows2', + 'palace beach residence', 'blue wave', 'new dubai gate1', + 'elite 4 sports residence', 'binghatti canal', 'azizi riviera 4', + 'liva', 'una', 'azizi park avenue', 'la residence 4 at the lotus', + 'damac towers by paramount', 'la residence 3 at the lotus', + 'damac hills (2) - viridis', 'vera tower', 'maple iii', + 'the lakes deema 2', 'harbour gate', 'luma22', 'sunset gardens b', + 'reem-mira community ph5', 'canal front residences cf1 & cf2', + 'westburry tower', 'arabian ranches lll - june', 'remraam', + 'remraam - al ramth 2', 'remraam - al ramth', + 'living legends phase 5', 'damac hills (2) - pacifica', + 'qpoint liwan-plot r054', 'qline (qpoint phaseii) 3c-lw-r-054', + 'hds business centre', 'jumeirah business centre 4', + 'gold crest views2', 'le grand chateau', 'the spirit', + 'the valley - orania', 'latifa tower', + 'serenia residences the palm', 'mag218', 'empire heights', + 'the fields at d11 - mbrmc', 'equiti arcade', 'al jawhara tower', + 'mudon views', 'sondos orchid', 'sky courts', + 'the pulse beachfront 2', 'damac hills - rochester', + 'glitz residence 3', 'burj vista', 'grand boulevard', 'sidra', + 'the regent', 'the valley - eden', 'collective', 'iris bay', + 'stallion tower', 'sondos zinnia', 'damac heights', + 'the polo residence', + 'mohammed bin rashid al maktoum city- district one, phase 1', + 'the grand', 'maple 2', 'al habtoor city', 'al habtoor tower', + 'ajmal sarah', 'the haven', 'elite ii sports residence', + 'arabian gate', 'hayat boulevard', 'prime gardens', 'coopet', + 'azizi riviera 41', 'the apricot', 'executive bay a', + 'executive bay b', 'elite 10 sports residence', 'mag 5 boulevard', + 'the court', + 'mohammed bin rashid al maktoum city-district one phase ii villas', + 'manhattan', 'panorama', 'badra phase 1', + 'mirdif hills- nasayem avenue', 'jannat', 'midtown - mesk', + 'midtown - afnan', 'midtown - dania', 'midtown - noor', + 'azizi riviera 20', 'trillionaire residences by binghatti', + 'boris becker business tower', 'rukan', 'opalz by danube', + 'oakley square residences', 'royale residence1', 'diamond views 3', + 'ag tower', 'the address dubai opera', 'burj park v', 'il primo', + 'grande', 'act one | act two', 'the mansion', 'burj park iii', + 'opera grand', 'the st. regis residences, downtown dubai', + 'm burj', 'the residence | burj khalifa', 'burj khalifa towers', + 'sharena residence 1', 'sherena residence', 'burj royale', + 'azizi riviera 33', 'the vybe', 'damac hills (2) - victoria 2', + 'emirates living - springs 2', 'arabella 2 - townhouses at mudon', + 'binghatti onyx', 'escan marina tower', 'fawad azizi residence', + 'azizi riviera 17', 'the hills', 'park ridge', 'peninsula one', + 'emirates garden 1 (lavender - gardenia - rose)', + 'port de la mer - la voile', + 'terhab hotel & towers at jumeirah village triangle', 'la rosa 3', + 'confident lancaster', 'elite 8 sports residence', + 'silicon gates 4', 'reem - mira oasis community phase 3', + 'port de la mer - la cote', 'atrium gold towers', + 'alduaa marina tower', 'jumeirah living marina gate', + 'urban oasis', 'd1', 'palazzo versace', 'azizi riviera 39', + 'fair view residency', 'regina', 'i - rise tower', 'time 1', + 'sobha hartland - the crest', '17 icon bay', 'rukan 3', 'it plaza', + 'dd stand point', 'axis silver', 'maha townhouses', + 'damac hills (2) - claret', 'qpoint liwan-plot r071', + 'no.17, no.18a,no.18b and no.19 citywalk residential', + 'elysee iii by pantheon', 'royale garden residence', + 'park heights i', 'azizi riviera 22', 'the pad', 'hockey tower', + 'saba tower 3', 'citadel tower', 'new dubai gate2', + 'azure residence', 'rasha 2', 'azizi riviera 67', + 'damac hills (2) - mimosa', 'mudon al ranim 2', + 'sobha hartland waves', 'emirates living - springs 7', + 'the bridge', 'sapphire residence', 'azizi riviera 63', + 'bay square', 'laguna tower', 'glitz residence 1', + 'axis residences 3', '15 northside', 'santevill', 'syann park1', + 'jadeel -madinat jumeirah living', 'qpoint liwan-plot r068', + 'jasmine lane', 'the onyx', + 'mohammed bin rashid al maktoum city , district one phase iii , residences 20', + 'rigel', 'elash', 'parkside', 'international city emarati', + 'donna towers', 'emirates living - springs 12', 'al furjan', + 'the crescent', 'gold crest executive', 'uniestate prime tower', + 'al khail heights', 'fairmont palm residence', + 'emirates living - springs 10', 'damac hills (2) - centaury', + 'upside living', 'lawnsi', 'tanaro', 'mag 22', + 'palace residences - dubai creek harbour', 'smart heights', + 'binghatti avenue', 'vincitore benessere', 'suberbia', + 'alef noon residence', 'damac hills (2) - amargo', + 'green community west- extension- phase iii', 'town central', + 'celestia', 'celia residence', 'merano tower', 'sami q tower', + 'living legends phase 3', 'town square zahra', 'dezire residences', + 'harbour residences', 'aykon city 2', 'aykon city', 'aykon city 3', + 'binghatti emerald', 'plaza residences', 'the lakes ghadeer', + 'the binary by omniyat', 'stadium point', 'b2b tower', + 'the pulse townhouses', 'elite residence', 'palma residence', + 'uniestate supreme residence', 'ghalia', 'the concourse', + 'farhad azizi residence', 'silver tower', 'marquis signature', + 'creekside 18', 'sandoval lane', 'miraclz tower', 'orion building', + 'mayfair residency', 'dunes village', 'the dubai creek residences', + 'asayel at madinat jumeirah living', 'ontario tower', + 'damac hills - carson', 'global golf residence 2', 'lake view', + 'damac hills (2) - avencia', 'azizi shaista residence', + 'jumeirah business centre 5', 'the haven residences', + 'avenue residence three', 'serena resedence', + 'lawnz residence by danube', 'azizi riviera 45', 'park islands', + 'villa lantana 1', 'the terraces', '1 residences', 'fahad 2', + 'au tower', 'prive by damac', 'the valley - talia', 'ac3', 'mudon', + 'marina residence', 'reem townhouses', 'orra marina', + 'ellington beach house', 'q - zone (qpoint - phase iii) mu007', + 'qpoint liwan- plot mu007', 'aura', 'binghatti tulip', + 'downtown views ii', 'mada\'in', + 'mohammed bin rashid al maktoum city , district one phase iii , residences 30', + 'matex tower', 'mag 318', 'mag eye phase 1', 'jewelz residence', + 'the ivy', 'palace estates', 'tiara residence', 'la vista 05', + 'reem - mira oasis community phase 2', 'mulberry at park heights', + 'greenview 3', 'royale residence2', 'binghatti venus', + 'green valley tower', 'ocean heights', 'one za\'abeel', 'maple', + 'westwood grande ii by imtiaz', 'grandeur residences', + 'creek edge', 'rawda apartments', 'azizi riviera 43', + 'silicon gate 1', 'emirates living - springs 5', + 'azizi riviera 61', 'la riviera azure', 'lake central', + 'the matrix', 'lagoon views at district one', + 'emirates living - springs 11', 'the pulse boulevard apartments', + 'dubai marina mall', 'reva residences', + 'jumeirah business centre 6', 'me do re', 'signature livings', + 'vida residence downtown dubai', 'windsor manor', 'amaranta 2', + 'madina tower', 'creek rise', 'azizi riviera 27', + 'jumeirah business centre2', 'urbana ii', 'avenue residence 2', + 'damac hills (2) - coursetia', 'town square jenna and warda', + 'arabian ranches iii - sun', 'azizi riviera 3', 'harbour views', + 'the signature', 'centrium tower', 'binghatti corner', + 'premiers twin tower', 'mag 5', 'bloom towers', 'the point', + 'continental tower', 'clayton residency', 'global green view ii', + 'binghatti orchid', 'dunes tower', 'reef residence', + 'azizi riviera 25', 'hera tower', 'lakeside residence', + 'manam prime', 'trafalgar central', 'elite sports residence', + 'dd boulevard central', 'the pulse residence', 'zada tower', + 'blue bay', 'warsan village - b & c', + 'mohammad aqil ali & ahmad ali alzarooni', 'peninsula five', + 'sulafa tower', 'the concourse 2', 'tower 108', 'cadi4', + 'dusk by binghatti', 'ariyana', 'zumurud dubai marina', + 'joya verde residences dubai', 'wilton terraces', + 'avenue residence1', 'elite 6 sports residence', + 'diamond views 1- villas b', 'myka residence', + 'vida residences dubai marina', 'hamilton residency', + 'madison residency', 'the royal atlantis,resort and residences', + 'ariana park', 'belgravia iii', 'j&g plexs', 'alandalus townhouse', + 'golf ville', 'majestine', 'creek palace', 'jumeirah gate', + 'silicon avenue', 'noor townhouses', 'golf tower', + 'damac hills (2) - amazonia', + 'the address residence fountain views ii', + 'the address residence fountain views iii', 'boulevard point', + 'vida dubai mall', 'the address residence-fountain views', + 'luxury family residence ii', 'iris crystal', 'montrose', 'mirar', + '29 boulevard', 'azizi riviera 12', 'azizi samia residence', + 'dm marina plaza', 'golf views', 'la visita 02', 'the autograph', + 'canal views', 'bloom heights', 'la rive', 'la vista 04', + 'rukan residence', 'qpoint liwan-plot mu002', 'lake terrace', + 'damac hills (2) - avencia-2', 'burj crown', + 'uniestate sports tower', 'cappadocia', 'riah towers', + 'yaass tower', 'townhouses at jumeirah islands', + 'damac hills (2) - aquilegia', + 'mohammed bin rashid al maktoum city , district one phase iii , residences 24', + 'oberoi centre', 'elite 5 sports residence', + 'the sterling east house', 'sol avenue', 'forte', + 'arabian ranches iii - spring', 'daytona house', 'the palm tower', + 'hamza tower', 'oxford boulevard', 'the pulse- beachfront', + 'sobha creek vistas reserve', 'pearl house i by imtiaz', + 'silicon information technology tower', 'binghatti mirage', + 'al ferdows', 'damac hills (2) - victoria', 'torch tower', + 'azizi riviera 11', + 'no.20, no.21a, no.21b and no.22 citywalk residential', + 'las casas', + 'mohammed bin rashid al maktoum city , district one phase iii , residences 1', + 'azizi riviera 9', 'burlington tower', + 'damac hills - golf promenade', 'al waleed garden 2', + 'residences du port, dubai marina, autograph collection residences', + 'damac hills - brookfield-3', 'damac hills - artesia', + 'qpoint liwan-plot r010', 'qline (qpoint phase ii) r010', + 'sobha hartland waves grande', 'al andalus phase 2', + 'alandalus building e', 'alandalus building g', 'alandalus', + 'alandalus building d', 'champions tower1', 'park lane tower', + 'the paragon by igo', 'vancouver', 'sidra tower', 'elan', + 'mohammed bin rashid al maktoum city , district one phase iii , residences 18', + 'solitaire cascades', 'bay central west & central towers', + 'q-zone (qpoint - phase iii), mu005&6', 'qpoint liwan-plot mu005', + 'qpointliwan-plot mu006', 'damac hills (2) - aster', + 'qpoint liwan-plot r055', + 'no.1, no.2a, no.2b, no.3a and 3b citywalk residential', + 'marquise square tower', 'silver star tower', + 'emirates living - springs 3', + 'mohammed bin rashid al maktoum city , district one phase iii , residences 13', + 'churchill tower', 'park horizon', 'binghatti crescent', + 'la rosa 5', 'sobha hartland one park avenue', 'la violeta 2', + 'villanova amaranta 4', 'beach vista', 'spica', 'water\'s edge', + 'beach isle', 'park field', 'azizi riviera 13', 'liv marina', + 'crystal tower', 'diamond views 4', 'gardenia 3', 'aura by grovy', + 'mbl residence', 'cayan tower', 'claren 1', 'equiti apartments', + 'la-riviera apartments', 'arabian & spainsh tower (phase 2)', + 'canal residence west 2', 'elite business bay residence', + 'palladium', 'the vortex tower', 'the valley-nara', + 'belgravia heights i', 'creek crescent', 'jumeirah park 7b&c', + 'condor classic', 'samana golf avenue', 'ellington house', + 'arabian ranches lll - caya', 'safi townhouses', 'the wings', + 'olivz residence', 'al jawzaa', 'prive residence', + 'damac hills (2) - zinnia', 'town square hayat', + 'the opus by omniyat', 'maria tower', 'naseem townhouses', + 'downtown views', 'blvd heights', 'sol bay', + 'damac hills - golf vista', 'urbana iii', 'indigo tower', + 'azizi riviera 23', 'the pulse beachfront 3', 'apex atrium', + 'creek gate', 'azizi riviera 7', 'concept 7 residences', + 'silicon heights', 'the pulse residence park', + 'platinum by vision', 'paramount tower hotel & residences', + 'azizi riviera 2', 'tenora', 'binghatti creek', + 'villanova amaranta 3', 'armada towers', 'town square safi', + 'crystal residence', 'harmony', 'oia residence', + 'casa flores and eden apartments', 'azizi riviera 14', + 'clover bay', 'binghatti heights', 'gardenia livings', + 'damac hills-park residences 1', 'arabian ranches iii - joy', + 'silverene', 'elite downtown residence', 'profile residence', + 'the anwa by omniyat', 'prime tower', 'damac hills (2) - vardon', + 'shamal waves', 'tiffany towers', 'topaz residences 2', + 'qpoint liwan-plot r069', 'azizi riviera 26', 'pearls by vision', + 'azizi riviera 47', 'azizi riviera 42', 'azizi gardens', + 'imperial', 'reem-mira community ph 4', 'azizi aura residences', + 'prime business avenue', + 'lincoln park(west side & lincoln park - b)', + 'mohammed bin rashid al maktoum city district one- c villas', + 'wavez residence', '51@business bay', 'arabian ranches lll - ruba', + 'living legends phase 7', 'elz residence', 'burj views' +]; + +function populateAreas() { + const areaSelect = document.getElementById('area_name'); + areas.forEach(area => { + const option = document.createElement('option'); + option.value = area; + option.textContent = area.charAt(0).toUpperCase() + area.slice(1).replace(/\b\w/g, l => l.toUpperCase()); + areaSelect.appendChild(option); + }); +} + +function loadProjects() { + const projectSelect = document.getElementById('project'); + const sortedProjects = projects.slice().sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })); + sortedProjects.forEach(project => { + const option = document.createElement('option'); + option.value = project; + option.textContent = project.length > 70 ? project.substring(0, 70) + '...' : project; + projectSelect.appendChild(option); + }); +} + +function showLoading(show) { + document.getElementById('loading').style.display = show ? 'block' : 'none'; +} + +function showError(message) { + const errorDiv = document.getElementById('error'); + errorDiv.textContent = message; + errorDiv.style.display = 'block'; +} + +function hideError() { + document.getElementById('error').style.display = 'none'; +} + +function showResults() { + document.getElementById('resultsSection').style.display = 'block'; +} + +function hideResults() { + document.getElementById('resultsSection').style.display = 'none'; +} + +function formatDate(dateString) { + if (!dateString) return 'N/A'; + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); +} + +function formatNumber(num, decimals = 0) { + if (num === null || num === undefined) return 'N/A'; + return parseFloat(num).toLocaleString('en-US', { maximumFractionDigits: decimals }); +} + +function formatCurrency(amount) { + if (!amount) return 'N/A'; + return 'AED ' + parseFloat(amount).toLocaleString('en-US', { maximumFractionDigits: 0 }); +} + +let currentPage = 1; +let lastPaging = { total: 0, page: null, page_size: 30, total_pages: 1, has_next: false, has_prev: false }; + +async function searchRents(pageOverride) { + const formData = new FormData(document.getElementById('filtersForm')); + const params = new URLSearchParams(); + + const area_name = formData.get('area_name'); + const property_type = formData.get('property_type'); + const size_min = formData.get('size_min'); + const size_max = formData.get('size_max'); + const rooms = formData.get('rooms'); + const project = formData.get('project'); + const pageSize = formData.get('page_size') || '30'; + const page = pageOverride != null ? pageOverride : (formData.get('page') || currentPage || 1); + + if (area_name) params.append('area_name', area_name); + if (property_type && property_type !== 'all') params.append('property_type', property_type); + if (size_min && !isNaN(parseFloat(size_min))) params.append('size_min', size_min); + if (size_max && !isNaN(parseFloat(size_max))) params.append('size_max', size_max); + if (rooms && rooms !== 'all') params.append('rooms', rooms); + if (project && project !== 'all') params.append('project', project); + // pagination params + if (page) params.append('page', page); + if (pageSize) params.append('page_size', pageSize); + + showLoading(true); + hideError(); + hideResults(); + + try { + const response = await fetch(`${API_BASE}/rents/recent?${params.toString()}`); + const data = await response.json(); + if (data.success) { + currentPage = data.data.page || 1; + lastPaging = { + total: data.data.total ?? data.data.count, + page: data.data.page, + page_size: data.data.page_size ?? parseInt(pageSize,10), + total_pages: data.data.total_pages ?? 1, + has_next: !!data.data.has_next, + has_prev: !!data.data.has_prev + }; + displayResults(data.data); + } else { + showError(data.message || 'Failed to fetch rents'); + } + } catch (error) { + showError('Network error: ' + error.message); + } finally { + showLoading(false); + } +} + +function displayResults(data) { + const { rents, count, total, page, page_size, total_pages, has_next, has_prev } = data; + const tbody = document.getElementById('rentsBody'); + const resultsCount = document.getElementById('resultsCount'); + const showing = count; + const grandTotal = total ?? count; + resultsCount.textContent = `Showing ${showing} of ${grandTotal} rents`; + const pageInfo = document.getElementById('pageInfo'); + if (pageInfo) { + if (page && total_pages) { + pageInfo.textContent = `Page ${page} of ${total_pages}`; + } else { + pageInfo.textContent = ''; + } + } + const prevBtn = document.getElementById('prevPage'); + const nextBtn = document.getElementById('nextPage'); + if (prevBtn) prevBtn.disabled = !(has_prev); + if (nextBtn) nextBtn.disabled = !(has_next); + + if (rents.length === 0) { + const colCount = document.querySelectorAll('#rentsTable thead th').length; + tbody.innerHTML = ( + '' + + `` + + '' + + '' + + '' + + '
No rents found matching your filters
' + + '' + + '' + ); + showResults(); + return; + } + + tbody.innerHTML = rents.map(r => ( + '' + + `${r.rent_id ?? 'N/A'}` + + `${formatDate(r.registration_date)}` + + `${formatDate(r.start_date)}` + + `${formatDate(r.end_date)}` + + `${r.version_en ?? 'N/A'}` + + `${r.area_en ?? 'N/A'}` + + `${formatCurrency(r.contract_amount)}` + + `${formatCurrency(r.annual_amount)}` + + `${r.is_free_hold_en ?? 'N/A'}` + + `${formatNumber(r.actual_area)}` + + `${r.prop_type_en ?? 'N/A'}` + + `${r.prop_sub_type_en ?? 'N/A'}` + + `${r.rooms != null ? formatNumber(r.rooms, 1) : 'N/A'}` + + `${r.usage_en ?? 'N/A'}` + + `${r.nearest_metro_en ?? 'N/A'}` + + `${r.nearest_mall_en ?? 'N/A'}` + + `${r.nearest_landmark_en ?? 'N/A'}` + + `${r.parking != null ? formatNumber(r.parking, 1) : 'N/A'}` + + `${r.total_properties ?? '0'}` + + `${r.master_project_en ?? 'N/A'}` + + `${r.project_en ?? 'N/A'}` + + `${formatDate(r.created_at)}` + + `${formatDate(r.updated_at)}` + + '' + )).join(''); + + showResults(); +} + +function resetFilters() { + document.getElementById('filtersForm').reset(); + document.getElementById('size_min').value = ''; + document.getElementById('size_max').value = ''; + document.getElementById('sizeValue').textContent = 'All Sizes'; + hideResults(); + hideError(); +} + +document.addEventListener('DOMContentLoaded', () => { + populateAreas(); + loadProjects(); + + const sizeMinHidden = document.getElementById('size_min'); + const sizeMaxHidden = document.getElementById('size_max'); + const sizeMinRange = document.getElementById('size_min_range'); + const sizeMaxRange = document.getElementById('size_max_range'); + const sizeFill = document.getElementById('sizeFill'); + const sizeValue = document.getElementById('sizeValue'); + const MIN_VAL = parseFloat(sizeMinRange.min); + const MAX_VAL = parseFloat(sizeMinRange.max); + const STEP = parseFloat(sizeMinRange.step) || 100; + + function updateSizeLabel() { + const min = parseFloat(sizeMinRange.value); + const max = parseFloat(sizeMaxRange.value); + const hasMin = !isNaN(min); + const hasMax = !isNaN(max); + if (!hasMin && !hasMax) { + sizeValue.textContent = 'All Sizes'; + } else if (hasMin && hasMax) { + sizeValue.textContent = `${min.toLocaleString('en-US')} - ${max.toLocaleString('en-US')} sq.ft`; + } else if (hasMin) { + sizeValue.textContent = `≥ ${min.toLocaleString('en-US')} sq.ft`; + } else { + sizeValue.textContent = `≤ ${max.toLocaleString('en-US')} sq.ft`; + } + // Persist only when different from full range + sizeMinHidden.value = (min > MIN_VAL) ? min : ''; + sizeMaxHidden.value = (max < MAX_VAL) ? max : ''; + // Update fill track + const left = ((Math.max(MIN_VAL, Math.min(min, max)) - MIN_VAL) / (MAX_VAL - MIN_VAL)) * 100; + const right = ((Math.max(MIN_VAL, Math.max(min, max)) - MIN_VAL) / (MAX_VAL - MIN_VAL)) * 100; + sizeFill.style.left = `${left}%`; + sizeFill.style.width = `${Math.max(0, right - left)}%`; + } + + function clampRanges() { + if (parseFloat(sizeMinRange.value) > parseFloat(sizeMaxRange.value) - STEP) { + sizeMinRange.value = (parseFloat(sizeMaxRange.value) - STEP).toString(); + } + if (parseFloat(sizeMaxRange.value) < parseFloat(sizeMinRange.value) + STEP) { + sizeMaxRange.value = (parseFloat(sizeMinRange.value) + STEP).toString(); + } + } + sizeMinRange.addEventListener('input', () => { clampRanges(); updateSizeLabel(); }); + sizeMaxRange.addEventListener('input', () => { clampRanges(); updateSizeLabel(); }); + + // Ensure both handles are draggable when overlapping by toggling z-index on interaction + function bringMinToFront() { + sizeMinRange.style.zIndex = '6'; + sizeMaxRange.style.zIndex = '5'; + } + function bringMaxToFront() { + sizeMinRange.style.zIndex = '5'; + sizeMaxRange.style.zIndex = '6'; + } + ['mousedown','pointerdown','touchstart'].forEach(evt => { + sizeMinRange.addEventListener(evt, bringMinToFront, { passive: true }); + sizeMaxRange.addEventListener(evt, bringMaxToFront, { passive: true }); + }); + // Initialize + sizeMinRange.value = MIN_VAL; + sizeMaxRange.value = MAX_VAL; + updateSizeLabel(); + + document.getElementById('filtersForm').addEventListener('submit', async (e) => { + e.preventDefault(); + currentPage = 1; + const pageInput = document.getElementById('page'); + if (pageInput) pageInput.value = '1'; + await searchRents(1); + }); + + const resetBtn = document.getElementById('resetBtn'); + if (resetBtn) { + resetBtn.addEventListener('click', resetFilters); + } + + // pagination buttons + const prevBtn = document.getElementById('prevPage'); + const nextBtn = document.getElementById('nextPage'); + if (prevBtn) { + prevBtn.addEventListener('click', async () => { + if (lastPaging.has_prev && currentPage > 1) { + currentPage -= 1; + const pageInput2 = document.getElementById('page'); + if (pageInput2) pageInput2.value = String(currentPage); + await searchRents(currentPage); + } + }); + } + if (nextBtn) { + nextBtn.addEventListener('click', async () => { + if (lastPaging.has_next) { + currentPage += 1; + const pageInput3 = document.getElementById('page'); + if (pageInput3) pageInput3.value = String(currentPage); + await searchRents(currentPage); + } + }); + } +}); + + diff --git a/public/rents.html b/public/rents.html new file mode 100644 index 0000000..ef3646f --- /dev/null +++ b/public/rents.html @@ -0,0 +1,501 @@ + + + + + + Recent Rents - Dubai DLD Analytics + + + +
+
+

🏠 Recent Property Rents

+

Filter and explore the latest rental contracts in Dubai

+
+ +
+

Filters

+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ +
+
+
+
+
+
+ + +
+
+
All Sizes
+
+ + + +
+
+
+ +
+ + +
+
+
+ +
+ 🔍 Fetching rents... +
+ +
+ + +
+ + + + + + + diff --git a/setup.sh b/setup.sh old mode 100755 new mode 100644 diff --git a/setup_docker.sh b/setup_docker.sh old mode 100755 new mode 100644 diff --git a/src/app.js b/src/app.js index ff53073..5800011 100644 --- a/src/app.js +++ b/src/app.js @@ -12,7 +12,22 @@ const app = express(); const PORT = process.env.PORT || 3000; // Security middleware -app.use(helmet()); +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'"], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'"], + fontSrc: ["'self'", "https:", "data:"], + objectSrc: ["'none'"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"], + }, + }, + crossOriginEmbedderPolicy: false +})); app.use(cors()); // Rate limiting diff --git a/src/routes/api.js b/src/routes/api.js index 97e79da..c59b805 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -224,7 +224,7 @@ router.get('/database/info', async (req, res) => { for (const table of tables) { try { - const [countResult] = await database.query(`SELECT COUNT(*) as count FROM ${table}`); + const countResult = await database.query(`SELECT COUNT(*) as count FROM ${table}`); info[table] = { count: countResult[0].count, status: 'available' @@ -465,4 +465,144 @@ router.get('/transactions/recent', async (req, res) => { } }); +// Recent rents endpoint with filters +router.get('/rents/recent', async (req, res) => { + try { + const { + area_name, + property_type, + size_min, + size_max, + rooms, + project, + page, + page_size, + limit + } = req.query; + + // Validate limit + let limitNum = undefined; + if (limit !== undefined && limit !== null && `${limit}`.trim() !== '') { + const parsed = parseInt(limit, 10); + if (isNaN(parsed) || parsed < 1) { + return res.status(400).json({ + success: false, + message: 'Limit must be a positive integer when provided' + }); + } + limitNum = parsed; + } + + // Build WHERE clause + let baseWhere = ' WHERE 1=1'; + const params = []; + + // Area filter (case-insensitive) + if (area_name && area_name.trim() !== '') { + baseWhere += ' AND LOWER(area_en) LIKE ?'; + params.push(`%${area_name.toLowerCase().trim()}%`); + } + + // Property type filter + if (property_type && property_type.trim() !== '' && property_type !== 'all') { + baseWhere += ' AND (LOWER(prop_type_en) LIKE ? OR LOWER(prop_sub_type_en) LIKE ?)'; + const propType = `%${property_type.toLowerCase().trim()}%`; + params.push(propType, propType); + } + + // Actual area filtering (min/max) + const minNum = size_min !== undefined ? parseFloat(size_min) : undefined; + const maxNum = size_max !== undefined ? parseFloat(size_max) : undefined; + const hasMin = minNum !== undefined && !isNaN(minNum); + const hasMax = maxNum !== undefined && !isNaN(maxNum); + if (hasMin && hasMax) { + baseWhere += ' AND actual_area BETWEEN ? AND ?'; + params.push(minNum, maxNum); + } else if (hasMin) { + baseWhere += ' AND actual_area >= ?'; + params.push(minNum); + } else if (hasMax) { + baseWhere += ' AND actual_area <= ?'; + params.push(maxNum); + } + + // Rooms filter (decimal values: 1.0, 2.0, etc.) + if (rooms && rooms !== 'all' && rooms !== '') { + const roomsNum = parseFloat(rooms); + if (!isNaN(roomsNum)) { + baseWhere += ' AND rooms = ?'; + params.push(roomsNum); + } + } + + // Project filter + if (project && project.trim() !== '' && project !== 'all') { + baseWhere += ' AND (LOWER(project_en) LIKE ? OR LOWER(master_project_en) LIKE ?)'; + const projectName = `%${project.toLowerCase().trim()}%`; + params.push(projectName, projectName); + } + + // Pagination logic + const wantsPagination = (page !== undefined) || (page_size !== undefined) || (limitNum !== undefined); + const defaultPage = 1; + const defaultPageSize = 30; + const pageNum = (() => { const n = parseInt(page ?? defaultPage, 10); return isNaN(n) || n < 1 ? defaultPage : n; })(); + const pageSizeNum = (() => { const n = parseInt((page_size ?? (limitNum !== undefined ? limitNum : defaultPageSize)), 10); return isNaN(n) || n < 1 ? defaultPageSize : n; })(); + const offsetNum = (pageNum - 1) * pageSizeNum; + + const orderBy = ' ORDER BY registration_date DESC, rent_id DESC'; + let dataSql; + let total = null; + + if (!wantsPagination) { + dataSql = `SELECT * FROM rents ${baseWhere} ${orderBy}`; + } else if (limitNum !== undefined && page === undefined && page_size === undefined) { + dataSql = `SELECT * FROM rents ${baseWhere} ${orderBy} LIMIT ${limitNum}`; + } else { + const countSql = `SELECT COUNT(*) AS total FROM rents ${baseWhere}`; + const countRows = await database.query(countSql, params); + total = countRows && countRows[0] ? countRows[0].total : 0; + dataSql = `SELECT * FROM rents ${baseWhere} ${orderBy} LIMIT ${pageSizeNum} OFFSET ${offsetNum}`; + } + + console.log(`🔍 Fetching recent rents with filters:`, { + area_name, property_type, size_min, size_max, rooms, project, limit: limitNum, page, page_size + }); + + const rents = await database.query(dataSql, params); + const totalPages = (total !== null) ? Math.max(1, Math.ceil(total / pageSizeNum)) : 1; + + res.json({ + success: true, + data: { + rents: rents, + count: rents.length, + total: total !== null ? total : rents.length, + page: total !== null ? pageNum : null, + page_size: total !== null ? pageSizeNum : (limitNum !== undefined ? limitNum : null), + total_pages: total !== null ? totalPages : 1, + has_next: total !== null ? pageNum < totalPages : false, + has_prev: total !== null ? pageNum > 1 : false, + filters: { + area_name: area_name || null, + property_type: property_type || null, + rooms: rooms || null, + project: project || null, + limit: limitNum ?? null, + page: total !== null ? pageNum : null, + page_size: total !== null ? pageSizeNum : (limitNum !== undefined ? limitNum : null) + } + } + }); + + } catch (error) { + console.error('❌ Rents query error:', error); + res.status(500).json({ + success: false, + message: 'Failed to retrieve rents', + error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' + }); + } +}); + module.exports = router; diff --git a/src/routes/static.js b/src/routes/static.js index a1d53c2..d177673 100644 --- a/src/routes/static.js +++ b/src/routes/static.js @@ -16,5 +16,10 @@ router.get('/transactions', (req, res) => { res.sendFile(path.join(__dirname, '../../public/transactions.html')); }); +// Serve rents.html +router.get('/rents', (req, res) => { + res.sendFile(path.join(__dirname, '../../public/rents.html')); +}); + module.exports = router; diff --git a/src/services/chartFormatter.js b/src/services/chartFormatter.js index 8da6b2d..db5a84c 100644 --- a/src/services/chartFormatter.js +++ b/src/services/chartFormatter.js @@ -507,28 +507,54 @@ class ChartFormatter { // Original card logic for other queries // Calculate summary statistics - const prices = data.map(row => row.avg_price || row.avg_value || 0).filter(p => p > 0); - const counts = data.map(row => row.transaction_count || row.count || 0); + // Filter out NULL, NaN, and invalid values + const prices = data + .map(row => { + const price = row.avg_price || row.avg_value; + // Check if price is a valid number + if (price === null || price === undefined || isNaN(price) || price <= 0) { + return null; + } + return Number(price); + }) + .filter(p => p !== null && !isNaN(p) && p > 0); + const counts = data.map(row => { + const count = row.transaction_count || + row.count || + row.rental_count || + row.commercial_count || + row.residential_count || + row.total_count || + 0; + return Number(count) || 0; + }); + + // Only create price cards if we have valid prices if (prices.length > 0) { const avgPrice = prices.reduce((sum, price) => sum + price, 0) / prices.length; const maxPrice = Math.max(...prices); const minPrice = Math.min(...prices); - cards.push({ - title: 'Average Price', - value: this.formatCurrency(avgPrice), - subtitle: parsedQuery.areas && parsedQuery.areas.length > 0 ? parsedQuery.areas.join(', ') : 'All Areas', - trend: this.calculateTrend(prices), - icon: 'trending-up' - }); + // Validate that averages are valid numbers + if (!isNaN(avgPrice) && isFinite(avgPrice) && avgPrice > 0) { + cards.push({ + title: 'Average Price', + value: this.formatCurrency(avgPrice), + subtitle: parsedQuery.areas && parsedQuery.areas.length > 0 ? parsedQuery.areas.join(', ') : 'All Areas', + trend: this.calculateTrend(prices), + icon: 'trending-up' + }); - cards.push({ - title: 'Price Range', - value: `${this.formatCurrency(minPrice)} - ${this.formatCurrency(maxPrice)}`, - subtitle: 'Min to Max', - icon: 'range' - }); + if (!isNaN(minPrice) && !isNaN(maxPrice) && isFinite(minPrice) && isFinite(maxPrice)) { + cards.push({ + title: 'Price Range', + value: `${this.formatCurrency(minPrice)} - ${this.formatCurrency(maxPrice)}`, + subtitle: 'Min to Max', + icon: 'range' + }); + } + } } if (counts.length > 0) { diff --git a/src/services/hybridQueryGenerator.js b/src/services/hybridQueryGenerator.js index 7aeaebf..6ec7b26 100644 --- a/src/services/hybridQueryGenerator.js +++ b/src/services/hybridQueryGenerator.js @@ -45,6 +45,8 @@ class HybridQueryGenerator { FROM ${dataSource.table} WHERE ${dataSource.areaColumn} = ? AND ${dataSource.dateColumn} >= ? + AND ${dataSource.priceColumn} IS NOT NULL + AND ${dataSource.priceColumn} > 0 `; const params = [areas[0], time_period.startDate]; @@ -81,6 +83,8 @@ class HybridQueryGenerator { WHERE ${dataSource.areaColumn} = ? AND ${dataSource.dateColumn} >= ? AND usage_en = 'residential' + AND ${dataSource.priceColumn} IS NOT NULL + AND ${dataSource.priceColumn} > 0 `; const params = [areas[0], time_period.startDate]; @@ -140,6 +144,8 @@ class HybridQueryGenerator { AVG(actual_area) as avg_area FROM rents WHERE 1=1 + AND annual_amount IS NOT NULL + AND annual_amount > 0 `; const params = []; diff --git a/src/services/queryTemplates.js b/src/services/queryTemplates.js index b83bcd8..21c3fe4 100644 --- a/src/services/queryTemplates.js +++ b/src/services/queryTemplates.js @@ -17,6 +17,8 @@ class QueryTemplates { FROM rents WHERE area_en = ? AND start_date >= ? + AND annual_amount IS NOT NULL + AND annual_amount > 0 ${params.property_filter ? 'AND prop_sub_type_en = ?' : ''} GROUP BY DATE_FORMAT(start_date, '%Y-%m') ORDER BY month @@ -36,6 +38,8 @@ class QueryTemplates { FROM rents WHERE area_en = ? AND start_date >= ? + AND annual_amount IS NOT NULL + AND annual_amount > 0 ${params.property_filter ? 'AND prop_sub_type_en = ?' : ''} GROUP BY DATE_FORMAT(start_date, '%Y-%u') ORDER BY week @@ -249,6 +253,8 @@ class QueryTemplates { SUM(annual_amount) as total_rental_value FROM rents WHERE start_date >= ? + AND annual_amount IS NOT NULL + AND annual_amount > 0 GROUP BY area_en ORDER BY rental_count DESC LIMIT 10 @@ -270,6 +276,8 @@ class QueryTemplates { FROM rents WHERE usage_en = 'commercial' AND start_date >= ? + AND annual_amount IS NOT NULL + AND annual_amount > 0 GROUP BY area_en ORDER BY commercial_count DESC, avg_commercial_rent DESC LIMIT ${limit} @@ -292,6 +300,8 @@ class QueryTemplates { FROM rents WHERE usage_en = 'residential' AND start_date >= ? + AND annual_amount IS NOT NULL + AND annual_amount > 0 GROUP BY area_en ORDER BY residential_count DESC, avg_residential_rent DESC LIMIT ${limit} @@ -315,6 +325,8 @@ class QueryTemplates { AND (prop_sub_type_en = 'flat' OR prop_sub_type_en = 'villa') AND start_date >= ? AND area_en IS NOT NULL + AND annual_amount IS NOT NULL + AND annual_amount > 0 GROUP BY month, area_en HAVING transaction_count >= 1 ), @@ -339,6 +351,66 @@ class QueryTemplates { params: [params.roomType, params.startDate] }), + // Filtered property queries (transactions with area + property type) + filtered_transactions: (params) => ({ + sql: ` + SELECT + area_en, + prop_type_en, + prop_sb_type_en, + COUNT(*) as transaction_count, + AVG(trans_value) as avg_price, + AVG(actual_area) as avg_area, + SUM(trans_value) as total_value + FROM transactions + WHERE 1=1 + AND trans_value IS NOT NULL + AND trans_value > 0 + ${params.area ? 'AND area_en = ?' : ''} + ${params.property_filter ? 'AND (prop_type_en = ? OR prop_sb_type_en = ?)' : ''} + GROUP BY area_en, prop_type_en, prop_sb_type_en + ORDER BY transaction_count DESC + LIMIT 50 + `, + params: (() => { + const p = []; + if (params.area) p.push(params.area); + if (params.property_filter) { + p.push(params.property_filter); + p.push(params.property_filter); + } + return p; + })() + }), + + // Filtered rental queries (rents with area + property type) + filtered_rentals: (params) => ({ + sql: ` + SELECT + area_en, + prop_sub_type_en, + COUNT(*) as rental_count, + AVG(annual_amount) as avg_price, + AVG(actual_area) as avg_area, + SUM(annual_amount) as total_value + FROM rents + WHERE 1=1 + AND annual_amount IS NOT NULL + AND annual_amount > 0 + ${params.area ? 'AND area_en = ?' : ''} + ${params.property_filter ? 'AND prop_sub_type_en = ?' : ''} + GROUP BY area_en, prop_sub_type_en + ORDER BY rental_count DESC + LIMIT 50 + `, + params: (() => { + const p = []; + if (params.area) p.push(params.area); + if (params.property_filter) p.push(params.property_filter); + return p; + })() + }), + // Context-aware query with refinements context_aware_query: (params) => { let sql = ` @@ -359,6 +431,8 @@ class QueryTemplates { FROM rents WHERE area_en = ? AND start_date >= ? + AND annual_amount IS NOT NULL + AND annual_amount > 0 `; const sqlParams = [params.area, params.startDate]; @@ -471,6 +545,27 @@ class QueryTemplates { return 'residential_leasing_areas'; } + // Filter queries - transactions with area and/or property type + if (intent === 'filter') { + // Check if it's about rentals or transactions + if (query.includes('rental') || query.includes('rent')) { + return 'filtered_rentals'; + } + // Default to transactions for filter queries + return 'filtered_transactions'; + } + + // Queries with property types and areas but no specific intent + if (parsedQuery.property_types && parsedQuery.property_types.length > 0 && + (parsedQuery.areas && parsedQuery.areas.length > 0 || query.includes('transaction'))) { + // Check if it's about rentals + if (query.includes('rental') || query.includes('rent')) { + return 'filtered_rentals'; + } + // Default to transactions + return 'filtered_transactions'; + } + // General summary queries if (intent === 'summary' && areas && areas.length > 0) { return 'project_transaction_summary'; diff --git a/src/services/sqlGenerator.js b/src/services/sqlGenerator.js index f4a9706..9b113b4 100644 --- a/src/services/sqlGenerator.js +++ b/src/services/sqlGenerator.js @@ -65,6 +65,8 @@ class SQLGenerator { AVG(actual_area) as avg_area FROM rents WHERE 1=1 + AND annual_amount IS NOT NULL + AND annual_amount > 0 `; const params = []; @@ -106,6 +108,8 @@ class SQLGenerator { AVG(actual_area) as avg_area FROM rents WHERE 1=1 + AND annual_amount IS NOT NULL + AND annual_amount > 0 `; const params = []; @@ -208,6 +212,8 @@ class SQLGenerator { AVG(actual_area) as avg_area FROM rents WHERE 1=1 + AND annual_amount IS NOT NULL + AND annual_amount > 0 `; const params = []; @@ -233,6 +239,8 @@ class SQLGenerator { AVG(actual_area) as avg_area FROM rents WHERE 1=1 + AND annual_amount IS NOT NULL + AND annual_amount > 0 `; const params = []; @@ -270,6 +278,8 @@ class SQLGenerator { AVG(actual_area) as avg_area FROM rents WHERE 1=1 + AND annual_amount IS NOT NULL + AND annual_amount > 0 `; const params = []; diff --git a/test_api.sh b/test_api.sh old mode 100755 new mode 100644