diff --git a/sigap-website/package-lock.json b/sigap-website/package-lock.json index 0756af3..d1e4316 100644 --- a/sigap-website/package-lock.json +++ b/sigap-website/package-lock.json @@ -43,6 +43,7 @@ "input-otp": "^1.4.2", "lucide-react": "^0.468.0", "mapbox-gl": "^3.11.0", + "ml-kmeans": "^6.0.0", "motion": "^12.4.7", "next": "latest", "next-themes": "^0.4.4", @@ -52,8 +53,10 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", "react-map-gl": "^8.0.3", + "recharts": "^2.15.2", "resend": "^4.1.2", "sonner": "^2.0.1", + "uuid": "^11.1.0", "vaul": "^1.1.2", "zod": "^3.24.2", "zustand": "^5.0.3" @@ -64,6 +67,7 @@ "@types/node": "^22.10.2", "@types/react": "^19.0.2", "@types/react-dom": "19.0.2", + "@types/uuid": "^10.0.0", "postcss": "8.4.49", "prisma": "^6.4.1", "react-email": "3.0.7", @@ -309,6 +313,18 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", @@ -4468,6 +4484,19 @@ "webpack": ">=4.40.0" } }, + "node_modules/@sentry/webpack-plugin/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -6838,6 +6867,69 @@ "@types/node": "*" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/d3-voronoi": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.12.tgz", @@ -7014,6 +7106,13 @@ "@types/node": "*" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.14", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", @@ -8177,7 +8276,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { @@ -8186,6 +8284,33 @@ "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", "license": "BSD-3-Clause" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-geo": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.7.1.tgz", @@ -8195,6 +8320,112 @@ "d3-array": "1" } }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-voronoi": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.2.tgz", @@ -8241,6 +8472,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -8311,6 +8548,16 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -8881,6 +9128,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -8909,6 +9162,15 @@ "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -9445,6 +9707,21 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-any-array": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-2.0.1.tgz", + "integrity": "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==", + "license": "MIT" + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -9848,6 +10125,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -10057,6 +10340,87 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/ml-array-max": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-1.2.4.tgz", + "integrity": "sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.0" + } + }, + "node_modules/ml-array-min": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/ml-array-min/-/ml-array-min-1.2.3.tgz", + "integrity": "sha512-VcZ5f3VZ1iihtrGvgfh/q0XlMobG6GQ8FsNyQXD3T+IlstDv85g8kfV0xUG1QPRO/t21aukaJowDzMTc7j5V6Q==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.0" + } + }, + "node_modules/ml-array-rescale": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ml-array-rescale/-/ml-array-rescale-1.3.7.tgz", + "integrity": "sha512-48NGChTouvEo9KBctDfHC3udWnQKNKEWN0ziELvY3KG25GR5cA8K8wNVzracsqSW1QEkAXjTNx+ycgAv06/1mQ==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.0", + "ml-array-max": "^1.2.4", + "ml-array-min": "^1.2.3" + } + }, + "node_modules/ml-distance-euclidean": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ml-distance-euclidean/-/ml-distance-euclidean-2.0.0.tgz", + "integrity": "sha512-yC9/2o8QF0A3m/0IXqCTXCzz2pNEzvmcE/9HFKOZGnTjatvBbsn4lWYJkxENkA4Ug2fnYl7PXQxnPi21sgMy/Q==", + "license": "MIT" + }, + "node_modules/ml-kmeans": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ml-kmeans/-/ml-kmeans-6.0.0.tgz", + "integrity": "sha512-aziEZqeHxczaDvo1qkfCrC7XNVAPevs6PigAzy7dp9TzeQI7oGan6NfCgADwL/FAlA/wWi+1DkV8da6pXfuuPg==", + "license": "MIT", + "dependencies": { + "ml-distance-euclidean": "^2.0.0", + "ml-matrix": "^6.9.0", + "ml-nearest-vector": "^2.0.1", + "ml-random": "^0.5.0" + } + }, + "node_modules/ml-matrix": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/ml-matrix/-/ml-matrix-6.12.1.tgz", + "integrity": "sha512-TJ+8eOFdp+INvzR4zAuwBQJznDUfktMtOB6g/hUcGh3rcyjxbz4Te57Pgri8Q9bhSQ7Zys4IYOGhFdnlgeB6Lw==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.1", + "ml-array-rescale": "^1.3.7" + } + }, + "node_modules/ml-nearest-vector": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ml-nearest-vector/-/ml-nearest-vector-2.0.1.tgz", + "integrity": "sha512-gMPwNm3eed59ewJYiCK/+wElWBfNoD6JizH965ePiQgCo0pvQL63w4YdZhLs5eUV0iWcq6brVMUBL6iMySHnqg==", + "license": "MIT", + "dependencies": { + "ml-distance-euclidean": "^2.0.0" + } + }, + "node_modules/ml-random": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ml-random/-/ml-random-0.5.0.tgz", + "integrity": "sha512-zLJBmNb34LOz+vN6BD8l3aYm/VWYWbmAunrLMPs4dHf4gTl8BWlhil72j56HubPg86zrXioIs4qoHq7Topy6tw==", + "license": "MIT", + "dependencies": { + "ml-xsadd": "^2.0.0" + } + }, + "node_modules/ml-xsadd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ml-xsadd/-/ml-xsadd-2.0.0.tgz", + "integrity": "sha512-VoAYUqmPRmzKbbqRejjqceGFp3VF81Qe8XXFGU0UXLxB7Mf4GGvyGq5Qn3k4AiQgDEV6WzobqlPOd+j0+m6IrA==", + "license": "MIT" + }, "node_modules/module-details-from-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", @@ -10326,7 +10690,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10907,6 +11270,17 @@ "node": ">=0.4.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -11830,9 +12204,9 @@ } }, "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", "license": "MIT" }, "node_modules/react-map-gl": { @@ -11915,6 +12289,21 @@ } } }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -11937,6 +12326,22 @@ } } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -11974,6 +12379,44 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.2.tgz", + "integrity": "sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -13027,6 +13470,12 @@ "node": ">=0.8" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyqueue": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", @@ -13343,16 +13792,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { @@ -13385,6 +13834,40 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/victory-vendor/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/vt-pbf": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", diff --git a/sigap-website/package.json b/sigap-website/package.json index 48da17f..b824778 100644 --- a/sigap-website/package.json +++ b/sigap-website/package.json @@ -7,7 +7,8 @@ "db:seed": "npx prisma db seed" }, "prisma": { - "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" + "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts --no-reset", + "seed:reset": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" }, "dependencies": { "@evyweb/ioctopus": "^1.2.0", @@ -48,6 +49,7 @@ "input-otp": "^1.4.2", "lucide-react": "^0.468.0", "mapbox-gl": "^3.11.0", + "ml-kmeans": "^6.0.0", "motion": "^12.4.7", "next": "latest", "next-themes": "^0.4.4", @@ -57,8 +59,10 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", "react-map-gl": "^8.0.3", + "recharts": "^2.15.2", "resend": "^4.1.2", "sonner": "^2.0.1", + "uuid": "^11.1.0", "vaul": "^1.1.2", "zod": "^3.24.2", "zustand": "^5.0.3" @@ -69,6 +73,7 @@ "@types/node": "^22.10.2", "@types/react": "^19.0.2", "@types/react-dom": "19.0.2", + "@types/uuid": "^10.0.0", "postcss": "8.4.49", "prisma": "^6.4.1", "react-email": "3.0.7", @@ -77,5 +82,8 @@ "tailwindcss-animate": "^1.0.7", "ts-node": "^10.9.2", "typescript": "^5.7.2" + }, + "overrides": { + "react-is": "^19.0.0-rc-69d4b800-20241021" } } diff --git a/sigap-website/prisma/data/crime-category.ts b/sigap-website/prisma/data/crime-category.ts index 4d053b2..15b13c3 100644 --- a/sigap-website/prisma/data/crime-category.ts +++ b/sigap-website/prisma/data/crime-category.ts @@ -1,229 +1,270 @@ -const crimeCategories = [ +export const crimeCategoriesData = [ { - name: "TERHADAP KETERTIBAN UMUM", - description: - "Tindak pidana yang mengganggu ketertiban dan kenyamanan masyarakat secara umum.", + name: "Terhadap Ketertiban Umum", + description: "Kejahatan yang mengganggu ketertiban umum seperti unjuk rasa ilegal atau kerusuhan." }, { - name: "MEMBAHAYAKAN KAM UMUM", - description: - "Kejahatan yang berpotensi membahayakan keamanan dan keselamatan masyarakat.", + name: "Membahayakan Kam Umum", + description: "Tindakan yang membahayakan keamanan umum, termasuk penggunaan bahan peledak secara ilegal." }, { - name: "PEMBAKARAN", - description: - "Tindakan membakar properti atau bangunan secara sengaja yang dapat membahayakan orang lain.", + name: "Pembakaran", + description: "Tindakan pembakaran yang disengaja terhadap properti atau bangunan." }, { - name: "KEBAKARAN / MELETUS", - description: - "Kejadian kebakaran atau ledakan yang disebabkan oleh kelalaian atau tindakan kriminal.", + name: "Kebakaran / Meletus", + description: "Kejadian kebakaran atau ledakan yang menimbulkan kerusakan atau korban." }, { - name: "MEMBER SUAP", - description: - "Pemberian sesuatu kepada pejabat untuk mempengaruhi keputusan atau tindakan tertentu.", + name: "Member Suap", + description: "Memberikan suap kepada pejabat publik atau pihak lain untuk keuntungan pribadi." }, { - name: "SUMPAH PALSU", - description: - "Memberikan keterangan palsu di bawah sumpah, biasanya dalam proses hukum.", + name: "Sumpah Palsu", + description: "Memberikan keterangan tidak benar di bawah sumpah dalam proses hukum." }, { - name: "PEMALSUAN MATERAI", - description: - "Tindakan memalsukan materai resmi dengan tujuan merugikan pihak lain.", + name: "Pemalsuan Materai", + description: "Pembuatan atau penggunaan materai palsu untuk dokumen resmi." }, { - name: "PEMALSUAN SURAT", - description: - "Membuat, mengubah, atau memalsukan surat yang memiliki kekuatan hukum.", + name: "Pemalsuan Surat", + description: "Pemalsuan dokumen atau surat dengan tujuan menipu." }, { - name: "PERZINAHAN", - description: - "Hubungan seksual di luar pernikahan yang melanggar norma hukum dan sosial.", + name: "Perzinahan", + description: "Hubungan seksual antara orang yang salah satunya sudah terikat pernikahan dengan orang lain." }, { - name: "PERKOSAAN", - description: "Tindak pemaksaan hubungan seksual tanpa persetujuan korban.", + name: "Perkosaan", + description: "Pemaksaan hubungan seksual tanpa persetujuan korban." }, { - name: "PERJUDIAN", - description: - "Segala bentuk kegiatan taruhan atau perjudian yang melanggar hukum.", + name: "Perjudian", + description: "Kegiatan taruhan yang dilarang oleh hukum." }, { - name: "PENGHINAAN", - description: "Ucapan atau tindakan yang merendahkan martabat seseorang.", + name: "Penghinaan", + description: "Tindakan menghina atau merendahkan martabat orang lain secara lisan atau tulisan." }, { - name: "PENCULIKAN", - description: - "Mengambil atau menahan seseorang secara paksa dengan tujuan tertentu.", + name: "Penculikan", + description: "Pengambilan seseorang secara paksa atau tanpa izin untuk tujuan tertentu." }, { - name: "PERBUATAN TIDAK MENYENANGKAN", - description: - "Tindakan yang mengakibatkan ketidaknyamanan atau kerugian emosional pada orang lain.", + name: "Perbuatan Tidak Menyenangkan", + description: "Tindakan yang menyebabkan ketidaknyamanan atau ketakutan pada orang lain." }, { - name: "PEMBUNUHAN", - description: "Menghilangkan nyawa seseorang secara sengaja.", + name: "Pembunuhan", + description: "Tindakan menghilangkan nyawa orang lain secara sengaja." }, { - name: "PENGANIAYAAN RINGAN", - description: - "Tindakan kekerasan yang mengakibatkan luka ringan pada korban.", + name: "Penganiayaan Ringan", + description: "Tindakan kekerasan fisik ringan yang tidak menyebabkan luka berat." }, { - name: "PENGANIAYAAN BERAT", - description: "Kekerasan yang mengakibatkan luka serius atau kematian.", + name: "Penganiayaan Berat", + description: "Kekerasan fisik yang menyebabkan luka berat pada korban." }, { - name: "KELALAIAN AKIBATKAN ORANG MATI", - description: "Kelalaian yang menyebabkan kematian seseorang.", + name: "Kelalaian Akibatkan Orang Mati", + description: "Kelalaian yang menyebabkan kematian seseorang." }, { - name: "KELALAIAN AKIBATKAN ORANG LUKA", - description: "Kelalaian yang mengakibatkan luka pada orang lain.", + name: "Kelalaian Akibatkan Orang Luka", + description: "Kelalaian yang menyebabkan seseorang terluka." }, { - name: "PENCURIAN BIASA", - description: "Mengambil barang milik orang lain tanpa izin.", + name: "Pencurian Biasa", + description: "Pencurian yang dilakukan tanpa kekerasan atau perencanaan khusus." }, { - name: "CURAT", - description: - "Pencurian dengan pemberatan, seperti pembobolan rumah atau kendaraan.", + name: "Curat", + description: "Pencurian dengan pemberatan seperti membobol rumah atau bangunan." }, { - name: "CURINGAN", - description: "Pencurian ringan dengan nilai kerugian yang kecil.", - }, - { name: "CURAS", description: "Pencurian dengan kekerasan terhadap korban." }, - { name: "CURANMOR", description: "Pencurian kendaraan bermotor." }, - { - name: "PENGEROYOKAN", - description: - "Penyerangan secara bersama-sama terhadap satu atau beberapa orang.", + name: "Curingan", + description: "Pencurian ringan terhadap barang-barang bernilai kecil." }, { - name: "PREMANISME", - description: - "Tindakan kekerasan, pemerasan, atau ancaman oleh kelompok tertentu untuk menguasai wilayah.", + name: "Curas", + description: "Pencurian dengan kekerasan atau ancaman kekerasan." }, { - name: "PEMERASAN DAN PENGANCAMAN", - description: - "Tindakan meminta sesuatu dengan ancaman kekerasan atau pengungkapan informasi merugikan.", + name: "Curanmor", + description: "Pencurian kendaraan bermotor." }, { - name: "PENGGELAPAN", - description: - "Mengambil barang atau uang yang dipercayakan untuk kepentingan pribadi.", + name: "Pengeroyokan", + description: "Tindakan kekerasan oleh beberapa orang terhadap satu atau lebih korban." }, { - name: "PENIPUAN", - description: - "Menipu orang lain dengan tujuan mendapatkan keuntungan secara melawan hukum.", + name: "Premanisme", + description: "Tindakan intimidasi atau kekerasan oleh kelompok preman." }, { - name: "PENGRUSAKAN", - description: "Merusak properti orang lain secara sengaja.", + name: "Pemerasan Dan Pengancaman", + description: "Memaksa orang lain menyerahkan sesuatu melalui ancaman." }, { - name: "KENAKALAN REMAJA", - description: - "Perilaku menyimpang oleh remaja yang dapat meresahkan masyarakat.", + name: "Penggelapan", + description: "Penguasaan barang milik orang lain yang dipercayakan, namun tidak dikembalikan." }, { - name: "MENERIMA SUAP", - description: - "Menerima sesuatu dengan imbalan pengaruh keputusan atau tindakan tertentu.", + name: "Penipuan", + description: "Tindakan menipu untuk mendapatkan keuntungan pribadi." }, { - name: "PENADAHAN", - description: "Menyimpan atau menjual barang hasil kejahatan.", + name: "Pengrusakan", + description: "Merusak barang milik orang lain secara sengaja." }, { - name: "PEKERJAKAN ANAK", - description: - "Mempekerjakan anak di bawah umur dalam pekerjaan yang melanggar hukum.", + name: "Kenakalan Remaja", + description: "Perilaku menyimpang dari norma oleh anak remaja seperti tawuran atau balap liar." }, { - name: "AGRARIA", - description: "Kejahatan terkait sengketa tanah dan sumber daya agraria.", + name: "Menerima Suap", + description: "Menerima imbalan untuk mempengaruhi keputusan atau tindakan." }, { - name: "PERADILAN ANAK", - description: "Tindak pidana yang melibatkan anak dalam proses peradilan.", + name: "Penadahan", + description: "Membeli, menyimpan, atau menjual barang hasil kejahatan." }, { - name: "PERLINDUNGAN ANAK", - description: - "Kejahatan yang melanggar hak-hak anak dan kesejahteraan mereka.", + name: "Pekerjakan Anak", + description: "Mempekerjakan anak di bawah umur dalam pekerjaan yang dilarang oleh hukum." + }, + { + name: "Agraria", + description: "Sengketa dan kejahatan terkait kepemilikan dan penggunaan lahan." + }, + { + name: "Peradilan Anak", + description: "Proses hukum yang melibatkan anak sebagai pelaku tindak pidana." + }, + { + name: "Perlindungan Anak", + description: "Upaya perlindungan anak dari kekerasan, eksploitasi, dan penelantaran." }, { name: "PKDRT", - description: - "Tindak kekerasan dalam rumah tangga yang merugikan anggota keluarga.", + description: "Tindak kekerasan dalam rumah tangga baik fisik maupun psikis." }, { - name: "PERLINDUNGAN TKI", - description: - "Kejahatan yang melibatkan pelanggaran terhadap hak Tenaga Kerja Indonesia di luar negeri.", + name: "Perlindungan TKI", + description: "Perlindungan hukum terhadap Tenaga Kerja Indonesia di luar negeri." }, { - name: "PERLINDUNGAN SAKSI – KORBAN", - description: - "Tindakan yang mengancam keselamatan saksi atau korban dalam proses hukum.", + name: "Perlindungan Saksi – Korban", + description: "Perlindungan bagi saksi atau korban kejahatan dalam proses hukum." }, { name: "PTPPO", - description: - "Perdagangan orang, termasuk eksploitasi tenaga kerja dan seksual.", + description: "Perdagangan orang, termasuk eksploitasi tenaga kerja dan seksual." }, { - name: "PORNOGRAFI", - description: - "Produksi, distribusi, atau konsumsi materi pornografi yang melanggar hukum.", + name: "Pornografi", + description: "Produksi, distribusi, atau kepemilikan materi pornografi yang melanggar hukum." }, { - name: "SISTEM PERADILAN ANAK", - description: - "Pelaksanaan hukum dan keadilan yang berkaitan dengan anak sebagai pelaku kejahatan.", + name: "Sistem Peradilan Anak", + description: "Kerangka hukum dan institusi yang menangani kejahatan oleh anak." }, { - name: "PENYELENGGARAN PEMILU", - description: - "Kejahatan yang mengganggu proses pemilihan umum, seperti kecurangan suara.", + name: "Penyelenggaraan Pemilu", + description: "Kejahatan yang berkaitan dengan pelaksanaan pemilihan umum." }, { - name: "PEMERINTAH DAERAH", - description: - "Tindak pidana yang dilakukan oleh atau melibatkan aparat pemerintah daerah.", + name: "Pemerintah Daerah", + description: "Tindak pidana yang dilakukan atau melibatkan pejabat pemerintah daerah." }, { - name: "KEIMIGRASIAN", - description: - "Pelanggaran hukum yang terkait dengan masuk dan keluarnya orang dari suatu negara.", + name: "Keimigrasian", + description: "Kejahatan yang berkaitan dengan dokumen atau proses imigrasi." }, { - name: "EKSTRADISI", - description: - "Proses penyerahan tersangka atau terpidana ke negara lain untuk diadili.", + name: "Ekstradisi", + description: "Permintaan penyerahan pelaku kejahatan antar negara." }, { - name: "LAHGUN SENPI/HANDAK/SAJAM", - description: - "Penyalahgunaan senjata api, bahan peledak, atau senjata tajam.", + name: "Lahgun Senpi/Handak/Sajam", + description: "Penyalahgunaan senjata api, bahan peledak, atau senjata tajam." }, { - name: "PIDUM LAINNYA", - description: - "Tindak pidana umum lainnya yang tidak tercakup dalam kategori di atas.", + name: "Pidum Lainnya", + description: "Tindak pidana umum lainnya yang tidak termasuk dalam kategori tertentu." }, -]; - -export default crimeCategories; + { + name: "Money Loudering", + description: "Pencucian uang hasil kejahatan agar tampak legal." + }, + { + name: "Trafficking In Person", + description: "Perdagangan manusia untuk eksploitasi tenaga kerja atau seksual." + }, + { + name: "Selundup Senpi", + description: "Penyelundupan senjata api secara ilegal." + }, + { + name: "Trans Ekonomi Crime", + description: "Kejahatan ekonomi lintas negara atau lintas batas hukum nasional." + }, + { + name: "Illegal Logging", + description: "Penebangan hutan secara ilegal tanpa izin resmi." + }, + { + name: "Illegal Mining", + description: "Penambangan tanpa izin yang melanggar hukum." + }, + { + name: "Illegal Fishing", + description: "Penangkapan ikan secara ilegal tanpa izin atau merusak lingkungan." + }, + { + name: "BBM Illegal", + description: "Distribusi bahan bakar minyak tanpa izin atau bersubsidi secara ilegal." + }, + { + name: "Niaga Pupuk", + description: "Penyalahgunaan distribusi atau niaga pupuk bersubsidi." + }, + { + name: "ITE", + description: "Kejahatan yang dilakukan melalui sistem elektronik dan internet." + }, + { + name: "Satwa", + description: "Tindak kejahatan terhadap satwa dilindungi dan perdagangan ilegal hewan." + }, + { + name: "Upal", + description: "Pemalsuan dan peredaran uang palsu." + }, + { + name: "Fidusia", + description: "Kejahatan terkait jaminan fidusia, seperti penggelapan barang fidusia." + }, + { + name: "Perlindungan Konsumen", + description: "Pelanggaran hak konsumen atau penipuan dalam transaksi perdagangan." + }, + { + name: "Pidter Lainnya", + description: "Tindak pidana tertentu lainnya yang tidak diklasifikasikan secara spesifik." + }, + { + name: "Korupsi", + description: "Penyalahgunaan kekuasaan publik untuk keuntungan pribadi." + }, + { + name: "Konflik Etnis", + description: "Pertikaian antar kelompok etnis yang memicu kekerasan atau kerusuhan." + }, + { + name: "Separatisme", + description: "Gerakan pemisahan wilayah dari negara untuk membentuk pemerintahan sendiri." + } +] diff --git a/sigap-website/prisma/data/districts.ts b/sigap-website/prisma/data/districts.ts new file mode 100644 index 0000000..d8b6fc4 --- /dev/null +++ b/sigap-website/prisma/data/districts.ts @@ -0,0 +1,262 @@ +export const districtsName = [ + 'Ajung', + 'Ambulu', + 'Arjasa', + 'Balung', + 'Bangsalsari', + 'Gumukmas', + 'Jelbuk', + 'Jenggawah', + 'Jombang', + 'Kalisat', + 'Kaliwates', + 'Kencong', + 'Ledokombo', + 'Mayang', + 'Mumbulsari', + 'Pakusari', + 'Panti', + 'Patrang', + 'Puger', + 'Rambipuji', + 'Semboro', + 'Silo', + 'Sukorambi', + 'Sukowono', + 'Sumberbaru', + 'Sumberjambe', + 'Sumbersari', + 'Tanggul', + 'Tempurejo', + 'Umbulsari', + 'Wuluhan', +]; + +const tas = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + id: 'dXJuOm1ieHBsYzpINEpNWnc', + geometry: { type: 'Point', coordinates: [113.71102, -8.168297] }, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpINEpNWnc', + feature_type: 'neighborhood', + full_address: + 'Sumbersari, 68121, Sumbersari, Jember, East Java, Indonesia', + name: 'Sumbersari', + name_preferred: 'Sumbersari', + coordinates: { longitude: 113.71102, latitude: -8.168297 }, + place_formatted: '68121, Sumbersari, Jember, East Java, Indonesia', + bbox: [113.704642, -8.183543, 113.732346, -8.157071], + context: { + postcode: { mapbox_id: 'dXJuOm1ieHBsYzpBdFFPWnc', name: '68121' }, + locality: { + mapbox_id: 'dXJuOm1ieHBsYzpBcjZxWnc', + name: 'Sumbersari', + wikidata_id: 'Q7777272', + }, + place: { + mapbox_id: 'dXJuOm1ieHBsYzpFdWhu', + name: 'Jember', + wikidata_id: 'Q11080', + }, + region: { + mapbox_id: 'dXJuOm1ieHBsYzpBY1Ju', + name: 'East Java', + wikidata_id: 'Q3586', + region_code: 'JI', + region_code_full: 'ID-JI', + }, + country: { + mapbox_id: 'dXJuOm1ieHBsYzpJbWM', + name: 'Indonesia', + wikidata_id: 'Q252', + country_code: 'ID', + country_code_alpha_3: 'IDN', + }, + neighborhood: { + mapbox_id: 'dXJuOm1ieHBsYzpINEpNWnc', + name: 'Sumbersari', + wikidata_id: 'Q13097475', + }, + }, + }, + }, + { + type: 'Feature', + id: 'dXJuOm1ieHBsYzpBdFFPWnc', + geometry: { type: 'Point', coordinates: [113.711022, -8.168297] }, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBdFFPWnc', + feature_type: 'postcode', + full_address: '68121, Jember, East Java, Indonesia', + name: '68121', + name_preferred: '68121', + coordinates: { longitude: 113.711022, latitude: -8.168297 }, + place_formatted: 'Jember, East Java, Indonesia', + bbox: [113.704642, -8.183543, 113.732346, -8.157071], + context: { + locality: { + mapbox_id: 'dXJuOm1ieHBsYzpBcjZxWnc', + name: 'Sumbersari', + wikidata_id: 'Q7777272', + }, + place: { + mapbox_id: 'dXJuOm1ieHBsYzpFdWhu', + name: 'Jember', + wikidata_id: 'Q11080', + }, + region: { + mapbox_id: 'dXJuOm1ieHBsYzpBY1Ju', + name: 'East Java', + wikidata_id: 'Q3586', + region_code: 'JI', + region_code_full: 'ID-JI', + }, + country: { + mapbox_id: 'dXJuOm1ieHBsYzpJbWM', + name: 'Indonesia', + wikidata_id: 'Q252', + country_code: 'ID', + country_code_alpha_3: 'IDN', + }, + postcode: { mapbox_id: 'dXJuOm1ieHBsYzpBdFFPWnc', name: '68121' }, + }, + }, + }, + { + type: 'Feature', + id: 'dXJuOm1ieHBsYzpBcjZxWnc', + geometry: { type: 'Point', coordinates: [113.71979, -8.17341] }, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBcjZxWnc', + feature_type: 'locality', + full_address: 'Sumbersari, Jember, East Java, Indonesia', + name: 'Sumbersari', + name_preferred: 'Sumbersari', + coordinates: { longitude: 113.71979, latitude: -8.17341 }, + place_formatted: 'Jember, East Java, Indonesia', + bbox: [113.683722, -8.217454, 113.759958, -8.134822], + context: { + place: { + mapbox_id: 'dXJuOm1ieHBsYzpFdWhu', + name: 'Jember', + wikidata_id: 'Q11080', + }, + region: { + mapbox_id: 'dXJuOm1ieHBsYzpBY1Ju', + name: 'East Java', + wikidata_id: 'Q3586', + region_code: 'JI', + region_code_full: 'ID-JI', + }, + country: { + mapbox_id: 'dXJuOm1ieHBsYzpJbWM', + name: 'Indonesia', + wikidata_id: 'Q252', + country_code: 'ID', + country_code_alpha_3: 'IDN', + }, + locality: { + mapbox_id: 'dXJuOm1ieHBsYzpBcjZxWnc', + name: 'Sumbersari', + wikidata_id: 'Q7777272', + }, + }, + }, + }, + { + type: 'Feature', + id: 'dXJuOm1ieHBsYzpFdWhu', + geometry: { type: 'Point', coordinates: [113.69943, -8.172329] }, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpFdWhu', + feature_type: 'place', + full_address: 'Jember, East Java, Indonesia', + name: 'Jember', + name_preferred: 'Jember', + coordinates: { longitude: 113.69943, latitude: -8.172329 }, + place_formatted: 'East Java, Indonesia', + bbox: [113.236783, -8.588224, 114.043419, -7.968273], + context: { + region: { + mapbox_id: 'dXJuOm1ieHBsYzpBY1Ju', + name: 'East Java', + wikidata_id: 'Q3586', + region_code: 'JI', + region_code_full: 'ID-JI', + }, + country: { + mapbox_id: 'dXJuOm1ieHBsYzpJbWM', + name: 'Indonesia', + wikidata_id: 'Q252', + country_code: 'ID', + country_code_alpha_3: 'IDN', + }, + place: { + mapbox_id: 'dXJuOm1ieHBsYzpFdWhu', + name: 'Jember', + wikidata_id: 'Q11080', + }, + }, + }, + }, + { + type: 'Feature', + id: 'dXJuOm1ieHBsYzpBY1Ju', + geometry: { type: 'Point', coordinates: [112.737827, -7.245972] }, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBY1Ju', + feature_type: 'region', + full_address: 'East Java, Indonesia', + name: 'East Java', + name_preferred: 'East Java', + coordinates: { longitude: 112.737827, latitude: -7.245972 }, + place_formatted: 'Indonesia', + bbox: [110.880459, -8.878306, 116.357127, -4.945778], + context: { + country: { + mapbox_id: 'dXJuOm1ieHBsYzpJbWM', + name: 'Indonesia', + wikidata_id: 'Q252', + country_code: 'ID', + country_code_alpha_3: 'IDN', + }, + region: { + mapbox_id: 'dXJuOm1ieHBsYzpBY1Ju', + name: 'East Java', + region_code: 'JI', + region_code_full: 'ID-JI', + wikidata_id: 'Q3586', + }, + }, + }, + }, + { + type: 'Feature', + id: 'dXJuOm1ieHBsYzpJbWM', + geometry: { type: 'Point', coordinates: [106.827216, -6.175554] }, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpJbWM', + feature_type: 'country', + full_address: 'Indonesia', + name: 'Indonesia', + name_preferred: 'Indonesia', + coordinates: { longitude: 106.827216, latitude: -6.175554 }, + bbox: [94.915567, -11.092338, 141.022151, 6.160877], + context: { + country: { + mapbox_id: 'dXJuOm1ieHBsYzpJbWM', + name: 'Indonesia', + country_code: 'ID', + country_code_alpha_3: 'IDN', + wikidata_id: 'Q252', + }, + }, + }, + }, + ], + attribution: + 'NOTICE: © 2025 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.', +}; diff --git a/sigap-website/prisma/data/nav.ts b/sigap-website/prisma/data/nav.ts index 1c17f23..7336850 100644 --- a/sigap-website/prisma/data/nav.ts +++ b/sigap-website/prisma/data/nav.ts @@ -106,47 +106,79 @@ export const navData = { orderSeq: 1, isActive: true, }, - { - title: "Crime Categories", - url: "/dashboard/crime-management/crime-categories", - slug: "crime-categories", - icon: IconSlice, - orderSeq: 2, - isActive: true, - }, { title: "Cases", url: "/dashboard/crime-management/crime-incident", slug: "crime-incident", icon: IconAlertTriangle, + orderSeq: 2, + isActive: true, + // subSubItems: [ + // { + // title: "New Case", + // url: "/dashboard/crime-management/crime-incident/case-new", + // slug: "new-case", + // icon: IconAlertTriangle, + // orderSeq: 1, + // isActive: true, + // }, + // { + // title: "Active Cases", + // url: "/dashboard/crime-management/crime-incident/case-active", + // slug: "active-cases", + // icon: IconAlertTriangle, + // orderSeq: 2, + // isActive: true, + // }, + // { + // title: "Resolved Cases", + // url: "/dashboard/crime-management/crime-incident/case-closed", + // slug: "resolved-cases", + // icon: IconAlertTriangle, + // orderSeq: 3, + // isActive: true, + // }, + // ], + }, + { + title: "Evidence Management", + url: "/dashboard/crime-management/evidence", + slug: "evidence-management", + icon: IconPin, orderSeq: 3, isActive: true, - subSubItems: [ - { - title: "New Case", - url: "/dashboard/crime-management/crime-incident/case-new", - slug: "new-case", - icon: IconAlertTriangle, - orderSeq: 1, - isActive: true, - }, - { - title: "Active Cases", - url: "/dashboard/crime-management/crime-incident/case-active", - slug: "active-cases", - icon: IconAlertTriangle, - orderSeq: 2, - isActive: true, - }, - { - title: "Resolved Cases", - url: "/dashboard/crime-management/crime-incident/case-closed", - slug: "resolved-cases", - icon: IconAlertTriangle, - orderSeq: 3, - isActive: true, - }, - ], + }, + { + title: "Crime Officers", + url: "/dashboard/crime-management/officer", + slug: "crime-officer", + icon: IconUsersGroup, + orderSeq: 4, + isActive: true, + }, + { + title: "Analytics & Reporting", + url: "/dashboard/crime-management/analytics-reporting", + slug: "analytics-reporting", + icon: IconChartPie, + orderSeq: 5, + isActive: true, + }, + { + title: "Community Engagement", + url: "/dashboard/crime-management/community-engagement", + slug: "community-engagement", + icon: IconMessageCircle, + orderSeq: 6, + isActive: true, + }, + { + title: "Resurces Dispatch", + url: "/dashboard/crime-management/resource-dispatch", + slug: "crime-map", + icon: IconMap, + orderSeq: 7, + isActive: true, }, ], }, diff --git a/sigap-website/prisma/data/resources.ts b/sigap-website/prisma/data/resources.ts new file mode 100644 index 0000000..05a9bee --- /dev/null +++ b/sigap-website/prisma/data/resources.ts @@ -0,0 +1,93 @@ +export const resourcesData = [ + { + name: 'cities', + description: 'City data management', + attributes: { + fields: ['id', 'name', 'code', 'geographic_id', 'created_at', 'updated_at'] + } + }, + { + name: 'contact_messages', + description: 'Contact message management', + attributes: { + fields: ['id', 'name', 'email', 'phone', 'message_type', 'message_type_label', 'message', 'status', 'created_at', 'updated_at'] + } + }, + { + name: 'crime_incidents', + description: 'Crime case management', + attributes: { + fields: ['id', 'crime_id', 'crime_category_id', 'date', 'time', 'location', 'latitude', 'longitude', 'description', 'victim_count', 'status', 'created_at', 'updated_at'] + } + }, + { + name: 'crime_categories', + description: 'Crime category management', + attributes: { + fields: ['id', 'name', 'description', 'created_at', 'updated_at'] + } + }, + { + name: 'crimes', + description: 'Crime data management', + attributes: { + fields: ['id', 'district_id', 'city_id', 'year', 'number_of_crime', 'rate', 'heat_map', 'created_at', 'updated_at'] + } + }, + { + name: 'demographics', + description: 'Demographic data management', + attributes: { + fields: ['id', 'district_id', 'city_id', 'province_id', 'year', 'population', 'population_density', 'poverty_rate', 'created_at', 'updated_at'] + } + }, + { + name: 'districts', + description: 'District data management', + attributes: { + fields: ['id', 'city_id', 'name', 'code', 'created_at', 'updated_at'] + } + }, + { + name: 'geographics', + description: 'Geographic data management', + attributes: { + fields: ['id', 'district_id', 'latitude', 'longitude', 'land_area', 'polygon', 'created_at', 'updated_at'] + } + }, + { + name: 'profiles', + description: 'User profile management', + attributes: { + fields: ['id', 'user_id', 'avatar', 'username', 'first_name', 'last_name', 'bio', 'address', 'birth_date'] + } + }, + { + name: 'users', + description: 'User account management', + attributes: { + fields: ['id', 'roles_id', 'email', 'phone', 'encrypted_password', 'invited_at', 'confirmed_at', 'email_confirmed_at', 'recovery_sent_at', 'last_sign_in_at', 'app_metadata', 'user_metadata', 'created_at', 'updated_at', 'banned_until', 'is_anonymous'] + } + }, + { + name: 'roles', + description: 'Role management', + attributes: { + fields: ['id', 'name', 'description', 'created_at', 'updated_at'] + } + }, + { + name: 'resources', + description: 'Resource management', + attributes: { + fields: ['id', 'name', 'description', 'instance_role', 'relations', 'attributes', 'created_at', 'updated_at'] + } + }, + { + name: 'permissions', + description: 'Permission management', + attributes: { + fields: ['id', 'action', 'resource_id', 'role_id', 'created_at', 'updated_at'] + } + } +]; diff --git a/sigap-website/prisma/data/roles.ts b/sigap-website/prisma/data/roles.ts new file mode 100644 index 0000000..961ef50 --- /dev/null +++ b/sigap-website/prisma/data/roles.ts @@ -0,0 +1,14 @@ +export const rolesData = [ + { + name: 'admin', + description: 'Administrator with full access to all features.', + }, + { + name: 'viewer', + description: 'Read-only access to the data.', + }, + { + name: 'staff', + description: 'Staff with limited administrative access.', + }, +]; \ No newline at end of file diff --git a/sigap-website/prisma/schema.prisma b/sigap-website/prisma/schema.prisma index 797e37f..fbe4fec 100644 --- a/sigap-website/prisma/schema.prisma +++ b/sigap-website/prisma/schema.prisma @@ -11,15 +11,14 @@ datasource db { } model cities { - id String @id - geographic_id String? @db.Uuid - name String @db.VarChar(100) - created_at DateTime @default(now()) @db.Timestamptz(6) - updated_at DateTime @default(now()) @db.Timestamptz(6) - geographics geographics? @relation(fields: [geographic_id], references: [id]) - crimes crimes[] - demographics demographics[] - districts districts[] + id String @id @db.Char(20) + name String @db.VarChar(100) + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + geographics geographics[] + crimes crimes[] + demographics demographics[] + districts districts[] @@index([name]) } @@ -38,9 +37,9 @@ model contact_messages { } model crime_incidents { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - crime_id String? @db.Uuid - crime_category_id String? @db.Uuid + id String @id @db.Char(20) + crime_id String? + crime_category_id String? date DateTime @db.Timestamptz(6) time DateTime @db.Timestamptz(6) location String @db.VarChar(255) @@ -48,7 +47,7 @@ model crime_incidents { longitude Float description String victim_count Int - status crime_status @default(new) + status crime_status @default(open) created_at DateTime @default(now()) @db.Timestamptz(6) updated_at DateTime @default(now()) @db.Timestamptz(6) crime_categories crime_categories? @relation(fields: [crime_category_id], references: [id]) @@ -56,7 +55,7 @@ model crime_incidents { } model crime_categories { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + id String @id @db.Char(20) name String @db.VarChar(255) description String created_at DateTime @default(now()) @db.Timestamptz(6) @@ -65,7 +64,7 @@ model crime_categories { } model crimes { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + id String @id @db.Char(20) district_id String? city_id String? year Int @@ -78,30 +77,29 @@ model crimes { cities cities? @relation(fields: [city_id], references: [id]) districts districts? @relation(fields: [district_id], references: [id]) - @@unique([city_id, year]) - @@unique([district_id, year]) + @@unique([city_id, year, created_at]) + @@unique([district_id, year, created_at]) } model demographics { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - district_id String? @unique - city_id String? - province_id String? - year Int - population Int - population_density Float - poverty_rate Float - created_at DateTime @default(now()) @db.Timestamptz(6) - updated_at DateTime @default(now()) @db.Timestamptz(6) - cities cities? @relation(fields: [city_id], references: [id]) - districts districts? @relation(fields: [district_id], references: [id]) + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + district_id String? + city_id String? + year Int + population Int + population_density Float + number_of_unemployed Int + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + cities cities? @relation(fields: [city_id], references: [id]) + districts districts? @relation(fields: [district_id], references: [id]) - @@unique([city_id, year]) - @@unique([district_id, year]) + @@unique([city_id, year, created_at]) + @@unique([district_id, year, created_at]) } model districts { - id String @id + id String @id @db.Char(20) city_id String name String @db.VarChar(100) created_at DateTime @default(now()) @db.Timestamptz(6) @@ -116,6 +114,7 @@ model districts { model geographics { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + city_id String district_id String? @unique latitude Float? longitude Float? @@ -124,7 +123,7 @@ model geographics { geometry Json? created_at DateTime @default(now()) @db.Timestamptz(6) updated_at DateTime @default(now()) @db.Timestamptz(6) - cities cities[] + cities cities @relation(fields: [city_id], references: [id]) districts districts? @relation(fields: [district_id], references: [id]) } @@ -210,9 +209,10 @@ enum crime_rates { } enum crime_status { - new - in_progress + open + closed resolved + unresolved } // enum roles { diff --git a/sigap-website/prisma/seed.ts b/sigap-website/prisma/seed.ts index 3cc13c1..3180771 100644 --- a/sigap-website/prisma/seed.ts +++ b/sigap-website/prisma/seed.ts @@ -1,275 +1,80 @@ +// prisma/seeder.ts import { PrismaClient } from '@prisma/client'; +import { RoleSeeder } from './seeds/role'; +import { ResourceSeeder } from './seeds/resource'; +import { PermissionSeeder } from './seeds/permission'; +import { GeoJSONSeeder } from './seeds/geographic'; +import { execSync } from 'child_process'; +import { DemographicsSeeder } from './seeds/demographic'; +import { CrimeCategoriesSeeder } from './seeds/crime-category'; +import { CrimeIncidentsSeeder } from './seeds/crime-incident'; const prisma = new PrismaClient(); -async function main() { - console.log('Starting seeding...'); +// Interface untuk standarisasi struktur seeder +interface Seeder { + run: () => Promise; +} - // Create roles - const adminRole = await prisma.roles.upsert({ - where: { name: 'admin' }, - update: {}, - create: { +// Class utama untuk menjalankan semua seeders +class DatabaseSeeder { + private seeders: Seeder[] = []; + private shouldReset: boolean = false; // Set true jika ingin mereset database sebelum seeding - name: 'admin', - description: 'Administrator with full access to all features', - }, - }); + constructor(shouldReset: boolean = true) { + this.shouldReset = shouldReset; - const viewerRole = await prisma.roles.upsert({ - where: { name: 'viewer' }, - update: {}, - create: { - - name: 'viewer', - description: 'Read-only access to data', - }, - }); - - const staffRole = await prisma.roles.upsert({ - where: { name: 'staff' }, - update: {}, - create: { - - name: 'staff', - description: 'Staff with limited administrative access', - }, - }); - - console.log('Roles created:', { adminRole, viewerRole, staffRole }); - - // Create resources based on Prisma schema models - const resources = [ - { - name: 'cities', - description: 'City data management', - attributes: { - fields: ['id', 'name', 'code', 'geographic_id', 'created_at', 'updated_at'] - } - }, - { - name: 'contact_messages', - description: 'Contact message management', - attributes: { - fields: ['id', 'name', 'email', 'phone', 'message_type', 'message_type_label', 'message', 'status', 'created_at', 'updated_at'] - } - }, - { - name: 'crime_cases', - description: 'Crime case management', - attributes: { - fields: ['id', 'crime_id', 'crime_category_id', 'date', 'time', 'location', 'latitude', 'longitude', 'description', 'victim_count', 'status', 'created_at', 'updated_at'] - } - }, - { - name: 'crime_categories', - description: 'Crime category management', - attributes: { - fields: ['id', 'name', 'description', 'created_at', 'updated_at'] - } - }, - { - name: 'crimes', - description: 'Crime data management', - attributes: { - fields: ['id', 'district_id', 'city_id', 'year', 'number_of_crime', 'rate', 'heat_map', 'created_at', 'updated_at'] - } - }, - { - name: 'demographics', - description: 'Demographic data management', - attributes: { - fields: ['id', 'district_id', 'city_id', 'province_id', 'year', 'population', 'population_density', 'poverty_rate', 'created_at', 'updated_at'] - } - }, - { - name: 'districts', - description: 'District data management', - attributes: { - fields: ['id', 'city_id', 'name', 'code', 'created_at', 'updated_at'] - } - }, - { - name: 'geographics', - description: 'Geographic data management', - attributes: { - fields: ['id', 'district_id', 'latitude', 'longitude', 'land_area', 'polygon', 'created_at', 'updated_at'] - } - }, - { - name: 'profiles', - description: 'User profile management', - attributes: { - fields: ['id', 'user_id', 'avatar', 'username', 'first_name', 'last_name', 'bio', 'address', 'birth_date'] - } - }, - { - name: 'users', - description: 'User account management', - attributes: { - fields: ['id', 'roles_id', 'email', 'phone', 'encrypted_password', 'invited_at', 'confirmed_at', 'email_confirmed_at', 'recovery_sent_at', 'last_sign_in_at', 'app_metadata', 'user_metadata', 'created_at', 'updated_at', 'banned_until', 'is_anonymous'] - } - }, - { - name: 'roles', - description: 'Role management', - attributes: { - fields: ['id', 'name', 'description', 'created_at', 'updated_at'] - } - }, - { - name: 'resources', - description: 'Resource management', - attributes: { - fields: ['id', 'name', 'description', 'instance_role', 'relations', 'attributes', 'created_at', 'updated_at'] - } - }, - { - name: 'permissions', - description: 'Permission management', - attributes: { - fields: ['id', 'action', 'resource_id', 'role_id', 'created_at', 'updated_at'] - } - } + // Daftar semua seeders di sini + this.seeders = [ + new RoleSeeder(prisma), + new ResourceSeeder(prisma), + new PermissionSeeder(prisma), + new GeoJSONSeeder(prisma), + new DemographicsSeeder(prisma), + new CrimeCategoriesSeeder(prisma), + new CrimeIncidentsSeeder(prisma), ]; + } - // Create resources in the database - for (const resource of resources) { - const createdResource = await prisma.resources.upsert({ - where: { name: resource.name }, - update: {}, - create: { - name: resource.name, - description: resource.description, - attributes: resource.attributes - }, - }); - console.log(`Resource ${resource.name} created/updated with ID: ${createdResource.id}`); + async run() { + // Jalankan migrate reset jika diperlukan + if (this.shouldReset) { + console.log('🔄 Menjalankan prisma migrate reset...'); + try { + // Jalankan perintah dengan --force untuk melewati konfirmasi + execSync('npx prisma migrate reset --force', { stdio: 'inherit' }); + console.log('✅ Database telah direset'); + } catch (error) { + console.error('❌ Gagal mereset database:', error); + throw error; + } } - // Set up basic permissions for each role - const allResources = await prisma.resources.findMany(); + console.log('🌱 Mulai seeding database...'); - // Admin permissions - full access to all resources - for (const resource of allResources) { - await createPermissions(adminRole.id, resource.id, ['create', 'read', 'update', 'delete']); + for (const seeder of this.seeders) { + await seeder.run(); } - // Viewer permissions - read-only access to all resources - for (const resource of allResources) { - await createPermissions(viewerRole.id, resource.id, ['read']); - } - - // Staff permissions - mixed permissions based on resource - for (const resource of allResources) { - if (['roles', 'permissions', 'resources', 'users'].includes(resource.name)) { - // Staff can only read roles, permissions, resources and users - await createPermissions(staffRole.id, resource.id, ['read']); - } else { - // Staff can create, read, update but not delete other resources - await createPermissions(staffRole.id, resource.id, ['create', 'read', 'update']); - } - } - - console.log('Seeding completed!'); + console.log('✅ Seeding selesai!'); + } } -async function createPermissions(roleId: string, resourceId: string, actions: string[]) { - for (const action of actions) { - await prisma.permissions.createMany({ - data: { - action: action, - resource_id: resourceId, - role_id: roleId, - }, - skipDuplicates: true // Skip if the permission already exists - }).catch((error) => { - console.error(`Error creating permission for role ${roleId} on resource ${resourceId}:`, error); - }); - } +// File untuk menjalankan seeder +async function main() { + try { + // Parameter pertama mengontrol apakah akan melakukan reset database + // Default: true (akan melakukan reset) + const shouldReset = process.argv.includes('--no-reset') ? false : true; + + const seeder = new DatabaseSeeder(shouldReset); + await seeder.run(); + } catch (error) { + console.error('Error saat seeding:', error); + process.exit(1); + } finally { + await prisma.$disconnect(); + } } - -main() - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (e) => { - console.error(e); - await prisma.$disconnect(); - process.exit(1); - }); - -// import { PrismaClient } from '@prisma/client'; -// import fs from 'fs'; -// import * as turf from '@turf/turf'; - -// const prisma = new PrismaClient(); - -// async function main() { -// const geojson = JSON.parse(fs.readFileSync('prisma/data/geojson/jember/districts.geojson', 'utf-8')); - -// // 1. Insert Kota/Kabupaten: Jember -// const city = await prisma.cities.upsert( -// { -// where: { id: '3574' }, -// update: {}, -// create: { -// id: '3574', -// name: 'Jember', -// } -// } -// ) - -// console.log(`City Jember inserted with ID: ${city.id}`); - -// // 2. Loop Semua District di GeoJSON -// for (const feature of geojson.features) { -// const properties = feature.properties; -// const geometry = feature.geometry; - -// // Cleanup code -// const districtCode = properties.kode_kec.replace(/\./g, ''); - -// // Insert District -// const district = await prisma.districts.create({ -// data: { -// id: districtCode, -// name: properties.kecamatan, -// city_id: city.id, -// } -// }); - -// console.log(`Inserted district: ${district.name}`); - -// // 3. Hitung Centroid dan Area -// const centroid = turf.centroid(feature); - -// const [longitude, latitude] = centroid.geometry.coordinates; -// const area = turf.area(feature) / 1_000_000; // dari m² ke km² - -// // 4. Insert Geographics -// await prisma.geographics.create({ -// data: { -// district_id: district.id, -// latitude, -// longitude, -// land_area: area, -// geometry: feature.geometry, -// } -// }); - -// console.log(`Inserted geographics for district: ${district.name}`); -// } - -// console.log("All data imported successfully!"); -// } - -// main() -// .catch((e) => { -// console.error(e); -// process.exit(1); -// }) -// .finally(async () => { -// await prisma.$disconnect(); -// }); +main(); diff --git a/sigap-website/prisma/seeds/crime-category.ts b/sigap-website/prisma/seeds/crime-category.ts new file mode 100644 index 0000000..e96843f --- /dev/null +++ b/sigap-website/prisma/seeds/crime-category.ts @@ -0,0 +1,37 @@ +// prisma/seeds/CrimeCategoriesSeeder.ts +import { generateId } from "../../app/_utils/common"; +import { PrismaClient } from "@prisma/client"; +import { crimeCategoriesData } from "../data/crime-category"; + +export class CrimeCategoriesSeeder { + constructor(private prisma: PrismaClient) { } + + async run(): Promise { + console.log("Seeding crime categories..."); + + // Truncate table jika diperlukan + await this.prisma.$executeRaw`TRUNCATE TABLE "crime_categories" CASCADE`; + + for (const category of crimeCategoriesData) { + + const newId = generateId({ + prefix: "CC", + segments: { + sequentialDigits: 4, + }, + format: "{prefix}-{sequence}", + separator: "-" + }) + + await this.prisma.crime_categories.create({ + data: { + id: newId, + name: category.name, + description: category.description, + } + }); + } + + console.log(`✅ ${crimeCategoriesData.length} crime categories seeded`); + } +} diff --git a/sigap-website/prisma/seeds/crime-incident.ts b/sigap-website/prisma/seeds/crime-incident.ts new file mode 100644 index 0000000..ac9d61b --- /dev/null +++ b/sigap-website/prisma/seeds/crime-incident.ts @@ -0,0 +1,740 @@ +// prisma/seeds/CrimeIncidentsSeeder.ts +import { generateCode, generateId } from '../../app/_utils/common'; +import { PrismaClient, crime_status } from '@prisma/client'; +import axios from 'axios'; +import { kmeans } from 'ml-kmeans'; + +export class CrimeIncidentsSeeder { + private mapboxToken: string; + private totalIncidentsCreated: number = 0; + private readonly MAX_INCIDENTS: number = 500; + + // Store district demographic data to avoid repeated queries + private districtDemographicCache: Record< + string, + Record< + number, + { + populationDensity: number; + unemployment: number; + } + > + > = {}; + + // Store the k-means model for each year + private kmeansModels: Record< + number, + { + centroids: number[][]; + clusters: Record; + normalization?: { + year: number; + crimes: { min: number; max: number; range: number }; + density: { min: number; max: number; range: number }; + unemployment: { min: number; max: number; range: number }; + }; + } + > = {}; + + constructor(private prisma: PrismaClient) { + // You should store this in an environment variable + this.mapboxToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN || ''; + } + + async run(): Promise { + console.log( + `Seeding crime incidents data (limited to ${this.MAX_INCIDENTS} records)...` + ); + + // Mendapatkan semua districts dan categories + const districts = await this.prisma.districts.findMany(); + const cities = await this.prisma.cities.findMany(); + const crimeCategories = await this.prisma.crime_categories.findMany(); + + // Pre-load all demographics data for faster access + await this.preloadDemographicData(districts); + + // Menghapus data crime_incidents yang sudah ada + await this.prisma.$executeRaw`TRUNCATE TABLE "crime_incidents" CASCADE`; + await this.prisma.$executeRaw`TRUNCATE TABLE "crimes" CASCADE`; + + // Seed untuk 5 tahun terakhir + const currentYear = new Date().getFullYear(); + const years = [ + currentYear - 4, + currentYear - 3, + currentYear - 2, + currentYear - 1, + currentYear, + ]; + + // Fallback street names jika API gagal + const fallbackStreetNames = [ + 'Jalan Sudirman', + 'Jalan Thamrin', + 'Jalan Gatot Subroto', + 'Jalan Diponegoro', + 'Jalan Ahmad Yani', + 'Jalan Imam Bonjol', + 'Jalan Pahlawan', + 'Jalan Merdeka', + 'Jalan Pemuda', + 'Jalan Gajah Mada', + 'Jalan Hayam Wuruk', + 'Jalan Veteran', + 'Jalan Kartini', + 'Jalan Juanda', + 'Jalan Hasanudin', + 'Jalan Surya Kencana', + ]; + + // Calculate how many incidents to create per year (evenly distributed) + const incidentsPerYear = Math.floor(this.MAX_INCIDENTS / years.length); + + // For each year, create crime records and incidents + for (const year of years) { + // Skip if we've already reached the limit + if (this.totalIncidentsCreated >= this.MAX_INCIDENTS) { + break; + } + + // Simpan jumlah insiden per distrik dan kota + const districtCrimeCount: Record = {}; + const cityCrimeCount: Record = {}; + + // Store district data for K-means clustering + const districtData: Record< + string, + { + numberOfCrimes: number; + populationDensity: number; + unemploymentRate: number; + } + > = {}; + + // Inisialisasi counter untuk tiap kota dan distrik + cities.forEach((city) => { + cityCrimeCount[city.id] = 0; + }); + + districts.forEach((district) => { + districtCrimeCount[district.id] = 0; + }); + + // First, create all crime records for districts + for (const district of districts) { + const city = await this.prisma.cities.findFirst({ + where: { + id: district.city_id, + }, + }); + + if (!city) { + throw new Error(`City not found for district ID: ${district.name}`); + } + + const regencyCode = generateCode(city?.name); + + const newCrimeId = generateId({ + prefix: 'CR', + segments: { + codes: [regencyCode], + sequentialDigits: 4, + year: year, + }, + format: '{prefix}-{sequence}-{codes}-{year}', + separator: '-', + randomSequence: true, + }); + + // Buat crime record baru + await this.prisma.crimes.create({ + data: { + id: newCrimeId, + district_id: district.id, + city_id: district.city_id, + year, + number_of_crime: 0, // Akan diupdate nanti + rate: 'low', // Default rate + heat_map: this.generateHeatMap(district.id), + }, + }); + } + + // Calculate incidents per district for this year + const incidentsPerDistrict = Math.ceil( + incidentsPerYear / districts.length + ); + + // Generate incidents untuk tiap district + for (const district of districts) { + // Skip if we've already reached the limit + if (this.totalIncidentsCreated >= this.MAX_INCIDENTS) { + break; + } + + // Get the crime record for this district and year + const crime = await this.prisma.crimes.findFirst({ + where: { + district_id: district.id, + year, + }, + }); + + if (!crime) { + throw new Error( + `Crime record not found for district ID: ${district.name}` + ); + } + + // Get geographic data for the district once to use as base + const geoData = await this.prisma.geographics.findFirst({ + where: { district_id: district.id }, + }); + + // Base coordinates + const baseLatitude = geoData?.latitude || -8.0; + const baseLongitude = geoData?.longitude || 114.0; + + // Cache for street names by coordinates (to reduce API calls) + const streetCache: Record = {}; + + // Calculate how many incidents to create for this district + // Make sure we don't exceed the total limit + const maxIncidentsForThisDistrict = Math.min( + incidentsPerDistrict, + this.MAX_INCIDENTS - this.totalIncidentsCreated + ); + + for (let i = 0; i < maxIncidentsForThisDistrict; i++) { + // Pilih kategori secara acak + const randomCategory = + crimeCategories[Math.floor(Math.random() * crimeCategories.length)]; + + // Generate tanggal acak dalam rentang tahun ini + const startOfYear = new Date(year, 0, 1); + const endOfYear = new Date(year, 11, 31); + const randomDate = new Date( + this.getRandomNumber(startOfYear.getTime(), endOfYear.getTime()) + ); + + // Generate waktu acak + const hours = Math.floor(this.getRandomNumber(0, 23)); + const minutes = Math.floor(this.getRandomNumber(0, 59)); + const randomTime = new Date( + randomDate.getFullYear(), + randomDate.getMonth(), + randomDate.getDate(), + hours, + minutes, + 0 + ); + + // Generate latitude dan longitude dengan sedikit variasi dari pusat district + const latitude = baseLatitude + this.getRandomNumber(-0.01, 0.01); + const longitude = baseLongitude + this.getRandomNumber(-0.01, 0.01); + + // Generate status insiden acak + const statusOptions: crime_status[] = [ + 'open', + 'closed', + 'resolved', + 'unresolved', + ]; + const status = + statusOptions[Math.floor(Math.random() * statusOptions.length)]; + + // Generate jumlah korban acak + const victimCount = Math.floor(this.getRandomNumber(0, 5)); + + // Generate deskripsi insiden + const descriptions = [ + `Terjadi ${randomCategory.name.toLowerCase()} di daerah ${district.name}`, + `Dilaporkan kasus ${randomCategory.name.toLowerCase()} oleh warga setempat`, + `Kejadian ${randomCategory.name.toLowerCase()} melibatkan ${victimCount} korban`, + `Insiden ${randomCategory.name.toLowerCase()} terjadi pada malam hari`, + `Kasus ${randomCategory.name.toLowerCase()} sedang dalam penyelidikan`, + ]; + + const randomDescription = + descriptions[Math.floor(Math.random() * descriptions.length)]; + + // Get street name from Mapbox or use fallback + let location = ''; + const coordKey = `${latitude.toFixed(4)},${longitude.toFixed(4)}`; + + try { + if (streetCache[coordKey]) { + location = streetCache[coordKey]; + } else { + const streetName = await this.getStreetFromMapbox( + longitude, + latitude + ); + location = `${streetName}`; + streetCache[coordKey] = location; + } + } catch (error) { + // Fallback to random street name if API fails + const randomStreet = + fallbackStreetNames[ + Math.floor(Math.random() * fallbackStreetNames.length) + ]; + const randomHouseNumber = Math.floor(this.getRandomNumber(1, 200)); + location = `${district.name}, ${randomStreet} No. ${randomHouseNumber}`; + console.warn( + `Failed to get street name from Mapbox: ${error}. Using fallback.` + ); + } + + const districtCode = generateCode(district.name); + + const newCrimeIncidentId = generateId({ + prefix: 'CI', + segments: { + codes: [districtCode], + sequentialDigits: 4, + year: year, + }, + format: '{prefix}-{sequence}-{codes}-{year}', + separator: '-', + randomSequence: true, + }); + + // Insert data crime incident + await this.prisma.crime_incidents.create({ + data: { + id: newCrimeIncidentId, + crime_id: crime.id, + crime_category_id: randomCategory.id, + date: randomDate, + time: randomTime, + location: location, + latitude, + longitude, + description: randomDescription, + victim_count: victimCount, + status, + }, + }); + + // Increment counter untuk district dan city + districtCrimeCount[district.id]++; + cityCrimeCount[district.city_id]++; + this.totalIncidentsCreated++; + } + } + + // Collect all district data for K-means clustering + for (const district of districts) { + const crimeCount = districtCrimeCount[district.id]; + + // Get demographic data for the district and year + const demographics = this.districtDemographicCache[district.id]?.[year]; + const populationDensity = demographics?.populationDensity || 100; // Default if not found + const unemploymentRate = demographics?.unemployment || 5; // Default if not found + + districtData[district.id] = { + numberOfCrimes: crimeCount, + populationDensity: populationDensity, + unemploymentRate: unemploymentRate, + }; + } + + // Run K-means clustering to classify districts + await this.runKMeansClustering(districtData, year); + + // Create city crime records + for (const city of cities) { + const crimeCount = cityCrimeCount[city.id]; + + if (crimeCount > 0) { + const regencyCode = generateCode(city.name); + const newCrimeId = generateId({ + prefix: 'CR', + segments: { + codes: [regencyCode], + sequentialDigits: 4, + year: year, + }, + format: '{prefix}-{sequence}-{codes}-{year}', + separator: '-', + randomSequence: true, + }); + + // Get average population density and unemployment for city + const cityDistricts = districts.filter((d) => d.city_id === city.id); + let totalPopDensity = 0; + let totalUnemployment = 0; + let districtCount = 0; + + for (const d of cityDistricts) { + const demographics = this.districtDemographicCache[d.id]?.[year]; + if (demographics) { + totalPopDensity += demographics.populationDensity; + totalUnemployment += demographics.unemployment; + districtCount++; + } + } + + const avgPopDensity = + districtCount > 0 ? totalPopDensity / districtCount : 100; + const avgUnemployment = + districtCount > 0 ? totalUnemployment / districtCount : 5; + + // Determine city rate based on k-means clustering + const cityRate = this.predictClusterWithKMeans( + { + numberOfCrimes: crimeCount, + populationDensity: avgPopDensity, + unemploymentRate: avgUnemployment, + }, + year + ); + + // Buat record untuk kota + await this.prisma.crimes.create({ + data: { + id: newCrimeId, + city_id: city.id, + district_id: null, + year, + number_of_crime: crimeCount, + rate: cityRate, + heat_map: this.generateHeatMap(city.id), + }, + }); + } + } + + // Update district crime records with correct counts and rates using k-means results + for (const district of districts) { + const crimeCount = districtCrimeCount[district.id]; + // Get cluster assigned by K-means + const rate = + this.kmeansModels[year]?.clusters[district.id] || + this.getCrimeRate(crimeCount); + + await this.prisma.crimes.updateMany({ + where: { + district_id: district.id, + year, + }, + data: { + number_of_crime: crimeCount, + rate: rate, + }, + }); + } + } + + console.log( + `✅ ${this.totalIncidentsCreated} crime incidents seeded (limit: ${this.MAX_INCIDENTS})` + ); + } + + /** + * Run K-means clustering on district data with improved normalization + */ + private async runKMeansClustering( + districtData: Record< + string, + { + numberOfCrimes: number; + populationDensity: number; + unemploymentRate: number; + } + >, + year: number + ): Promise { + // Convert to array format needed by kmeans library + const data: number[][] = []; + const districtIds: string[] = []; + + // Extract all values for each feature to calculate statistics + const allCrimes: number[] = []; + const allDensities: number[] = []; + const allUnemployment: number[] = []; + + // First pass: collect all values + for (const [districtId, values] of Object.entries(districtData)) { + allCrimes.push(values.numberOfCrimes); + allDensities.push(values.populationDensity); + allUnemployment.push(values.unemploymentRate); + districtIds.push(districtId); + } + + // Calculate statistics for normalization + // Find min and max for each feature + const crimeStats = { + min: Math.min(...allCrimes), + max: Math.max(...allCrimes), + range: 0, + }; + crimeStats.range = crimeStats.max - crimeStats.min || 1; // Avoid division by zero + + const densityStats = { + min: Math.min(...allDensities), + max: Math.max(...allDensities), + range: 0, + }; + densityStats.range = densityStats.max - densityStats.min || 1; + + const unemploymentStats = { + min: Math.min(...allUnemployment), + max: Math.max(...allUnemployment), + range: 0, + }; + unemploymentStats.range = + unemploymentStats.max - unemploymentStats.min || 1; + + // Store normalization params for later prediction + this.normalizationParams = { + year, + crimes: crimeStats, + density: densityStats, + unemployment: unemploymentStats, + }; + + // Second pass: normalize using min-max scaling + for (const [districtId, values] of Object.entries(districtData)) { + // Min-max scaling: (value - min) / range -> scales to [0,1] + const normalizedCrimes = + (values.numberOfCrimes - crimeStats.min) / crimeStats.range; + const normalizedDensity = + (values.populationDensity - densityStats.min) / densityStats.range; + const normalizedUnemployment = + (values.unemploymentRate - unemploymentStats.min) / + unemploymentStats.range; + + data.push([normalizedCrimes, normalizedDensity, normalizedUnemployment]); + } + + if (data.length === 0) { + console.log(`No data for K-means clustering for year ${year}`); + return; + } + + try { + // Run K-means with 3 clusters (low, medium, high) + const result = kmeans(data, 3, { + initialization: 'kmeans++', + maxIterations: 100, + }); + + // Determine which cluster corresponds to which label (low, medium, high) + const clusterCentroids = result.centroids; + + // Sort clusters by the sum of their centroids (higher sum = higher crime rate) + const clusterSums = clusterCentroids.map((centroid) => + centroid.reduce((sum, val) => sum + val, 0) + ); + + const sortedIndices = clusterSums + .map((sum, index) => ({ sum, index })) + .sort((a, b) => a.sum - b.sum) + .map((item) => item.index); + + // Map sorted indices to labels + const labelMap: Record = { + [sortedIndices[0]]: 'low', + [sortedIndices[1]]: 'medium', + [sortedIndices[2]]: 'high', + }; + + // Create mapping from district ID to cluster label + const clusters: Record = {}; + for (let i = 0; i < districtIds.length; i++) { + const clusterId = result.clusters[i]; + clusters[districtIds[i]] = labelMap[clusterId]; + } + + // Store the K-means model and normalization params for this year + this.kmeansModels[year] = { + centroids: clusterCentroids, + clusters: clusters, + normalization: this.normalizationParams, + }; + + console.log(`✅ K-means clustering completed for year ${year}`); + } catch (error) { + console.error( + `Error running K-means clustering for year ${year}:`, + error + ); + // Fall back to simple classification if K-means fails + } + } + + /** + * Predict cluster for new data point using existing K-means model with improved normalization + */ + private predictClusterWithKMeans( + dataPoint: { + numberOfCrimes: number; + populationDensity: number; + unemploymentRate: number; + }, + year: number + ): 'low' | 'medium' | 'high' { + // If no model exists for this year, fall back to simple classification + if (!this.kmeansModels[year]) { + return this.getCrimeRate(dataPoint.numberOfCrimes); + } + + // Get normalization parameters for this year + const normParams = this.kmeansModels[year].normalization; + + if (!normParams) { + // Fallback to original method if normalization params aren't available + return this.getCrimeRate(dataPoint.numberOfCrimes); + } + + // Normalize the data point using the same parameters as during training + const normalizedPoint = [ + (dataPoint.numberOfCrimes - normParams.crimes.min) / + normParams.crimes.range, + (dataPoint.populationDensity - normParams.density.min) / + normParams.density.range, + (dataPoint.unemploymentRate - normParams.unemployment.min) / + normParams.unemployment.range, + ]; + + // Find closest centroid + let minDistance = Infinity; + let closestClusterIndex = 0; + + this.kmeansModels[year].centroids.forEach((centroid, index) => { + // Calculate Euclidean distance + const distance = Math.sqrt( + centroid.reduce( + (sum, val, i) => sum + Math.pow(val - normalizedPoint[i], 2), + 0 + ) + ); + + if (distance < minDistance) { + minDistance = distance; + closestClusterIndex = index; + } + }); + + // Map from cluster index to label based on centroid sums + const clusterSums = this.kmeansModels[year].centroids.map((centroid) => + centroid.reduce((sum, val) => sum + val, 0) + ); + + const sortedIndices = clusterSums + .map((sum, index) => ({ sum, index })) + .sort((a, b) => a.sum - b.sum) + .map((item) => item.index); + + // Map sorted indices to labels + const labelMap: Record = { + [sortedIndices[0]]: 'low', + [sortedIndices[1]]: 'medium', + [sortedIndices[2]]: 'high', + }; + + return labelMap[closestClusterIndex]; + } + + // Add this to the class properties + private normalizationParams: { + year: number; + crimes: { min: number; max: number; range: number }; + density: { min: number; max: number; range: number }; + unemployment: { min: number; max: number; range: number }; + } | null = null; + + /** + * Preload demographic data for all districts and years + */ + private async preloadDemographicData(districts: any[]): Promise { + console.log('Preloading demographic data...'); + + for (const district of districts) { + this.districtDemographicCache[district.id] = {}; + + const demographics = await this.prisma.demographics.findMany({ + where: { district_id: district.id }, + }); + + for (const demo of demographics) { + // Ensure populationDensity is properly retrieved + const populationDensity = demo.population_density || 0; + + this.districtDemographicCache[district.id][demo.year] = { + populationDensity: populationDensity, + unemployment: demo.number_of_unemployed || 5, + }; + } + } + + console.log('Demographic data preloaded'); + } + + /** + * Get street name from Mapbox API based on coordinates + */ + private async getStreetFromMapbox(lng: number, lat: number): Promise { + try { + const response = await axios.get( + `https://api.mapbox.com/search/geocode/v6/reverse?longitude=${lng}&latitude=${lat}&access_token=${this.mapboxToken}` + ); + + if ( + response.data && + response.data.features && + response.data.features.length > 0 + ) { + // Extract full_address from the first feature + const fullAddress = response.data.features[0].properties.full_address; + return ( + fullAddress || + `Jalan Tidak Diketahui No. ${Math.floor(this.getRandomNumber(1, 100))}` + ); + } + + // Fallback if no address found + return `Jalan Tidak Diketahui No. ${Math.floor(this.getRandomNumber(1, 100))}`; + } catch (error) { + console.error('Error fetching street from Mapbox:', error); + throw error; + } + } + + private getRandomNumber(min: number, max: number): number { + return Math.random() * (max - min) + min; + } + + /** + * Original simple version (kept for fallback) + */ + private getCrimeRate(numberOfCrimes: number): 'low' | 'medium' | 'high' { + // Simple logic for crime rate + if (numberOfCrimes < 10) return 'low'; + if (numberOfCrimes < 30) return 'medium'; + return 'high'; + } + + private generateHeatMap(id: string): any { + // Generate heat map dummy sebagai JSON + // Contoh: array koordinat dengan intensitas + const heatMapPoints = []; + const numPoints = Math.floor(this.getRandomNumber(5, 20)); + + for (let i = 0; i < numPoints; i++) { + heatMapPoints.push({ + lat: this.getRandomNumber(-7.5, -8.5), // Kisaran latitude untuk Jember + lng: this.getRandomNumber(113.5, 114.5), // Kisaran longitude untuk Jember + intensity: this.getRandomNumber(1, 10), + }); + } + + return { + id: id, + points: heatMapPoints, + }; + } +} diff --git a/sigap-website/prisma/seeds/demographic.ts b/sigap-website/prisma/seeds/demographic.ts new file mode 100644 index 0000000..9f96269 --- /dev/null +++ b/sigap-website/prisma/seeds/demographic.ts @@ -0,0 +1,127 @@ +// prisma/seeds/DemographicsSeeder.ts +import { PrismaClient } from "@prisma/client"; + +export class DemographicsSeeder { + constructor(private prisma: PrismaClient) { } + + async run(): Promise { + console.log("Seeding demographics data..."); + + // Mendapatkan semua districts + const districts = await this.prisma.districts.findMany(); + const cities = await this.prisma.cities.findMany(); + + // Seed demografis untuk 6 tahun terakhir + const currentYear = new Date().getFullYear(); + const years = [currentYear - 5, currentYear - 4, currentYear - 3, currentYear - 2, currentYear - 1, currentYear]; + + // Menghapus data demographics yang sudah ada + await this.prisma.$executeRaw`TRUNCATE TABLE "demographics" CASCADE`; + + let counter = 0; + + // Untuk tiap tahun + for (const year of years) { + // Pertama generate data level district + const districtDemographics = []; + + for (const district of districts) { + // Generate data demografis level district + const districtPopulation = this.getRandomNumber(10000, 100000); + const districtLandArea = await this.getDistrictLandArea(district.id); + const districtDensity = districtLandArea > 0 + ? districtPopulation / districtLandArea + : this.getRandomNumber(500, 3000); + const districtUnemployed = Math.floor(districtPopulation * this.getRandomNumber(0.03, 0.15)); + + // Simpan data district untuk agregasi level kota + districtDemographics.push({ + district_id: district.id, + city_id: district.city_id, + population: districtPopulation, + unemployed: districtUnemployed, + density: districtDensity + }); + + // Simpan data demografis level district + await this.prisma.demographics.create({ + data: { + city_id: district.city_id, + district_id: district.id, + year, + population: districtPopulation, + population_density: districtDensity, + number_of_unemployed: districtUnemployed + } + }); + counter++; + } + + // Kemudian generate data level kota berdasarkan agregasi dari district + for (const city of cities) { + // Filter district demographics untuk kota ini + const cityDistricts = districtDemographics.filter(d => d.city_id === city.id); + + if (cityDistricts.length > 0) { + // Agregasi data dari semua district dalam kota + const cityPopulation = cityDistricts.reduce((sum, d) => sum + d.population, 0); + const cityUnemployed = cityDistricts.reduce((sum, d) => sum + d.unemployed, 0); + + // Hitung total land area kota + const cityLandArea = await this.getCityLandArea(city.id); + + // Hitung kepadatan populasi kota + const cityDensity = cityLandArea > 0 + ? cityPopulation / cityLandArea + : cityDistricts.reduce((sum, d) => sum + d.density, 0) / cityDistricts.length; + + // Simpan data demografis level kota + await this.prisma.demographics.create({ + data: { + city_id: city.id, + district_id: cityDistricts[0].district_id, // Menggunakan district pertama sebagai placeholder + year, + population: cityPopulation, + population_density: cityDensity, + number_of_unemployed: cityUnemployed + } + }); + counter++; + } + } + } + + console.log(`✅ ${counter} demographics records seeded`); + } + + private getRandomNumber(min: number, max: number): number { + return Math.random() * (max - min) + min; + } + + private async getDistrictLandArea(districtId: string): Promise { + const geo = await this.prisma.geographics.findFirst({ + where: { district_id: districtId } + }); + + return geo?.land_area || 0; + } + + private async getCityLandArea(cityId: string): Promise { + const geo = await this.prisma.geographics.findFirst({ + where: { + city_id: cityId, + district_id: null + } + }); + + if (geo?.land_area) return geo.land_area; + + // Jika tidak ada data land area level kota, jumlahkan dari semua district + const districtsGeo = await this.prisma.geographics.findMany({ + where: { city_id: cityId, district_id: { not: null } } + }); + + return districtsGeo.reduce((sum, geo) => sum + (geo.land_area || 0), 0); + } +} + diff --git a/sigap-website/prisma/seeds/geographic.ts b/sigap-website/prisma/seeds/geographic.ts new file mode 100644 index 0000000..57741c9 --- /dev/null +++ b/sigap-website/prisma/seeds/geographic.ts @@ -0,0 +1,106 @@ +import { PrismaClient } from '@prisma/client'; +import fs from 'fs'; +import * as turf from '@turf/turf'; + +export class GeoJSONSeeder { + constructor(private prisma: PrismaClient) {} + + async run(): Promise { + console.log('Seeding GeoJSON data...'); + + await this.prisma.$executeRaw`TRUNCATE TABLE "geographics" CASCADE`; + + try { + // Load GeoJSON file + const regencyGeoJson = JSON.parse( + fs.readFileSync('prisma/data/geojson/jember/regency.geojson', 'utf-8') + ); + const districtGeoJson = JSON.parse( + fs.readFileSync('prisma/data/geojson/jember/districts.geojson', 'utf-8') + ); + + // 1. Insert Kota/Kabupaten: Jember + let regency; // Declare regency variable outside the loop + + for (const feature of regencyGeoJson.features) { + const properties = feature.properties; + const geometry = feature.geometry; + + // Cleanup code + const regencyCode = properties.kode_kk.replace(/\./g, ''); + + // Insert Regency + regency = await this.prisma.cities.create({ + data: { + id: regencyCode, + name: properties.kab_kota, + }, + }); + + // Insert Geographics for Regency + const centroid = turf.centroid(feature); + const [longitude, latitude] = centroid.geometry.coordinates; + const area = turf.area(feature) / 1_000_000; // dari m² ke km² + + await this.prisma.geographics.create({ + data: { + city_id: regency.id, + latitude, + longitude, + land_area: area, + geometry: feature.geometry, + }, + }); + + console.log(`Inserted regency: ${regency.name}`); + } + + // 2. Loop Semua District di GeoJSON + for (const feature of districtGeoJson.features) { + const properties = feature.properties; + const geometry = feature.geometry; + + // Cleanup code + const districtCode = properties.kode_kec.replace(/\./g, ''); + + // Insert District + const district = await this.prisma.districts.create({ + data: { + id: districtCode, + name: properties.kecamatan, + city_id: regency!.id, + }, + }); + + console.log(`Inserted district: ${district.name}`); + + // 3. Hitung Centroid dan Area + const centroid = turf.centroid(feature); + const [longitude, latitude] = centroid.geometry.coordinates; + const area = turf.area(feature) / 1_000_000; // dari m² ke km² + + // 4. Insert Geographics + await this.prisma.geographics.create({ + data: { + city_id: regency!.id, + district_id: district.id, + latitude, + longitude, + land_area: area, + geometry: feature.geometry, + }, + }); + + console.log(`Inserted geographics for district: ${district.name}`); + } + + console.log( + 'GeoJSON data seeded successfully!', + districtGeoJson.features.length, + 'districts inserted.' + ); + } catch (error) { + console.error('Error seeding GeoJSON data:', error); + } + } +} diff --git a/sigap-website/prisma/seeds/permission.ts b/sigap-website/prisma/seeds/permission.ts new file mode 100644 index 0000000..2551cc3 --- /dev/null +++ b/sigap-website/prisma/seeds/permission.ts @@ -0,0 +1,68 @@ +import { PrismaClient } from "@prisma/client"; + +export class PermissionSeeder { + constructor(private prisma: PrismaClient) { } + + async run(): Promise { + console.log('Seeding permissions...'); + + try { + // Fetch all resources and roles + const allResources = await this.prisma.resources.findMany(); + const adminRole = await this.prisma.roles.findUnique({ where: { name: 'admin' } }); + const viewerRole = await this.prisma.roles.findUnique({ where: { name: 'viewer' } }); + const staffRole = await this.prisma.roles.findUnique({ where: { name: 'staff' } }); + + if (!adminRole || !viewerRole || !staffRole) { + console.error('Roles not found. Please seed roles first.'); + return; + } + + // Admin permissions - full access to all resources + for (const resource of allResources) { + await this.createPermissions(adminRole.id, resource.id, ['create', 'read', 'update', 'delete']); + } + + // Viewer permissions - read-only access to all resources + for (const resource of allResources) { + await this.createPermissions(viewerRole.id, resource.id, ['read']); + } + + // Staff permissions - mixed permissions based on resource + for (const resource of allResources) { + if (['roles', 'permissions', 'resources', 'users'].includes(resource.name)) { + // Staff can only read roles, permissions, resources, and users + await this.createPermissions(staffRole.id, resource.id, ['read']); + } else { + // Staff can create, read, update but not delete other resources + await this.createPermissions(staffRole.id, resource.id, ['create', 'read', 'update']); + } + } + + console.log('Permissions seeded successfully!'); + } catch (error) { + console.error('Error seeding permissions:', error); + } + } + + private async createPermissions(roleId: string, resourceId: string, actions: string[]) { + for (const action of actions) { + try { + const permission = await this.prisma.permissions.createMany({ + data: { + action: action, + resource_id: resourceId, + role_id: roleId, + }, + skipDuplicates: true, // Skip if the permission already exists + }); + + + console.log(`Created permission: ${action} for role ${roleId} on resource ${resourceId}`); + + } catch (error) { + console.error(`Error creating permission for role ${roleId} on resource ${resourceId}:`, error); + } + } + } +} \ No newline at end of file diff --git a/sigap-website/prisma/seeds/resource.ts b/sigap-website/prisma/seeds/resource.ts new file mode 100644 index 0000000..6d14036 --- /dev/null +++ b/sigap-website/prisma/seeds/resource.ts @@ -0,0 +1,21 @@ +import { PrismaClient } from "@prisma/client"; +import { resourcesData } from "../data/resources"; + +export class ResourceSeeder { + constructor(private prisma: PrismaClient) { } + + async run(): Promise { + // Create resources based on Prisma schema models + try { + await this.prisma.resources.createMany({ + data: resourcesData, + skipDuplicates: true, // Skip duplicates if they exist + }); + + console.log('Resources created successfully:', resourcesData.map(resource => resource.name).join(', ')); + + } catch (error) { + console.error('Error creating resources:', error); + } + } +} diff --git a/sigap-website/prisma/seeds/role.ts b/sigap-website/prisma/seeds/role.ts new file mode 100644 index 0000000..a4c9a2d --- /dev/null +++ b/sigap-website/prisma/seeds/role.ts @@ -0,0 +1,25 @@ +import { PrismaClient } from "@prisma/client"; +import { rolesData } from "../data/roles"; + +export class RoleSeeder { + constructor(private prisma: PrismaClient) { } + + async run(): Promise { + console.log('Seeding roles...'); + + try { + const newRole = await this.prisma.roles.createMany({ + data: rolesData, + skipDuplicates: true, + }) + + console.log('Roles seeded:', rolesData.map(role => role.name).join(', ')); + + } catch (error) { + console.error('Error seeding roles:', error); + } + + + + } +} \ No newline at end of file