feat: add initial data seeding for resources, roles, demographics, crime incidents, and geographic data
This commit is contained in:
parent
2bf335d59b
commit
0cbcd3e636
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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.',
|
||||
};
|
|
@ -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: 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,
|
||||
// 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: "Resolved Cases",
|
||||
url: "/dashboard/crime-management/crime-incident/case-closed",
|
||||
slug: "resolved-cases",
|
||||
icon: IconAlertTriangle,
|
||||
title: "Evidence Management",
|
||||
url: "/dashboard/crime-management/evidence",
|
||||
slug: "evidence-management",
|
||||
icon: IconPin,
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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']
|
||||
}
|
||||
}
|
||||
];
|
|
@ -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.',
|
||||
},
|
||||
];
|
|
@ -11,12 +11,11 @@ datasource db {
|
|||
}
|
||||
|
||||
model cities {
|
||||
id String @id
|
||||
geographic_id String? @db.Uuid
|
||||
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? @relation(fields: [geographic_id], references: [id])
|
||||
geographics geographics[]
|
||||
crimes crimes[]
|
||||
demographics demographics[]
|
||||
districts districts[]
|
||||
|
@ -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
|
||||
district_id String?
|
||||
city_id String?
|
||||
province_id String?
|
||||
year Int
|
||||
population Int
|
||||
population_density Float
|
||||
poverty_rate 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 {
|
||||
|
|
|
@ -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<void>;
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
|
||||
// Set up basic permissions for each role
|
||||
const allResources = await prisma.resources.findMany();
|
||||
|
||||
// Admin permissions - full access to all resources
|
||||
for (const resource of allResources) {
|
||||
await createPermissions(adminRole.id, resource.id, ['create', 'read', 'update', 'delete']);
|
||||
}
|
||||
|
||||
// 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']);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Seeding completed!');
|
||||
console.log('🌱 Mulai seeding database...');
|
||||
|
||||
for (const seeder of this.seeders) {
|
||||
await seeder.run();
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
console.log('✅ Seeding selesai!');
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
await prisma.$disconnect();
|
||||
const seeder = new DatabaseSeeder(shouldReset);
|
||||
await seeder.run();
|
||||
} catch (error) {
|
||||
console.error('Error saat seeding:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
|
|
@ -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<void> {
|
||||
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`);
|
||||
}
|
||||
}
|
|
@ -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<string, 'low' | 'medium' | 'high'>;
|
||||
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<void> {
|
||||
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<string, number> = {};
|
||||
const cityCrimeCount: Record<string, number> = {};
|
||||
|
||||
// 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<string, string> = {};
|
||||
|
||||
// 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<void> {
|
||||
// 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<number, 'low' | 'medium' | 'high'> = {
|
||||
[sortedIndices[0]]: 'low',
|
||||
[sortedIndices[1]]: 'medium',
|
||||
[sortedIndices[2]]: 'high',
|
||||
};
|
||||
|
||||
// Create mapping from district ID to cluster label
|
||||
const clusters: Record<string, 'low' | 'medium' | 'high'> = {};
|
||||
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<number, 'low' | 'medium' | 'high'> = {
|
||||
[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<void> {
|
||||
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<string> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
// prisma/seeds/DemographicsSeeder.ts
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
export class DemographicsSeeder {
|
||||
constructor(private prisma: PrismaClient) { }
|
||||
|
||||
async run(): Promise<void> {
|
||||
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<number> {
|
||||
const geo = await this.prisma.geographics.findFirst({
|
||||
where: { district_id: districtId }
|
||||
});
|
||||
|
||||
return geo?.land_area || 0;
|
||||
}
|
||||
|
||||
private async getCityLandArea(cityId: string): Promise<number> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
export class PermissionSeeder {
|
||||
constructor(private prisma: PrismaClient) { }
|
||||
|
||||
async run(): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import { resourcesData } from "../data/resources";
|
||||
|
||||
export class ResourceSeeder {
|
||||
constructor(private prisma: PrismaClient) { }
|
||||
|
||||
async run(): Promise<void> {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import { rolesData } from "../data/roles";
|
||||
|
||||
export class RoleSeeder {
|
||||
constructor(private prisma: PrismaClient) { }
|
||||
|
||||
async run(): Promise<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue