feat: add initial data seeding for resources, roles, demographics, crime incidents, and geographic data

This commit is contained in:
vergiLgood1 2025-04-15 22:47:07 +07:00
parent 2bf335d59b
commit 0cbcd3e636
16 changed files with 2333 additions and 471 deletions

View File

@ -43,6 +43,7 @@
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
"mapbox-gl": "^3.11.0", "mapbox-gl": "^3.11.0",
"ml-kmeans": "^6.0.0",
"motion": "^12.4.7", "motion": "^12.4.7",
"next": "latest", "next": "latest",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
@ -52,8 +53,10 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-map-gl": "^8.0.3", "react-map-gl": "^8.0.3",
"recharts": "^2.15.2",
"resend": "^4.1.2", "resend": "^4.1.2",
"sonner": "^2.0.1", "sonner": "^2.0.1",
"uuid": "^11.1.0",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^3.24.2", "zod": "^3.24.2",
"zustand": "^5.0.3" "zustand": "^5.0.3"
@ -64,6 +67,7 @@
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/react": "^19.0.2", "@types/react": "^19.0.2",
"@types/react-dom": "19.0.2", "@types/react-dom": "19.0.2",
"@types/uuid": "^10.0.0",
"postcss": "8.4.49", "postcss": "8.4.49",
"prisma": "^6.4.1", "prisma": "^6.4.1",
"react-email": "3.0.7", "react-email": "3.0.7",
@ -309,6 +313,18 @@
"node": ">=6.0.0" "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": { "node_modules/@babel/template": {
"version": "7.26.9", "version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
@ -4468,6 +4484,19 @@
"webpack": ">=4.40.0" "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": { "node_modules/@socket.io/component-emitter": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@ -6838,6 +6867,69 @@
"@types/node": "*" "@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": { "node_modules/@types/d3-voronoi": {
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.12.tgz", "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.12.tgz",
@ -7014,6 +7106,13 @@
"@types/node": "*" "@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": { "node_modules/@types/ws": {
"version": "8.5.14", "version": "8.5.14",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
@ -8177,7 +8276,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/d3-array": { "node_modules/d3-array": {
@ -8186,6 +8284,33 @@
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==",
"license": "BSD-3-Clause" "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": { "node_modules/d3-geo": {
"version": "1.7.1", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.7.1.tgz", "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.7.1.tgz",
@ -8195,6 +8320,112 @@
"d3-array": "1" "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": { "node_modules/d3-voronoi": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.2.tgz", "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": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -8311,6 +8548,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/dom-serializer": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@ -8881,6 +9128,12 @@
"node": ">=0.10.0" "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": { "node_modules/events": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@ -8909,6 +9162,15 @@
"integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==",
"license": "MIT" "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": { "node_modules/fast-glob": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "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" "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": { "node_modules/is-arrayish": {
"version": "0.3.2", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@ -9848,6 +10125,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -10057,6 +10340,87 @@
"node": ">=16 || 14 >=14.17" "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": { "node_modules/module-details-from-path": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", "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", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -10907,6 +11270,17 @@
"node": ">=0.4.0" "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": { "node_modules/proto-list": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@ -11830,9 +12204,9 @@
} }
}, },
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-map-gl": { "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": { "node_modules/react-style-singleton": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", "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": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -11974,6 +12379,44 @@
"node": ">=8.10.0" "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": { "node_modules/require-from-string": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@ -13027,6 +13470,12 @@
"node": ">=0.8" "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": { "node_modules/tinyqueue": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
@ -13343,16 +13792,16 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "9.0.1", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [ "funding": [
"https://github.com/sponsors/broofa", "https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan" "https://github.com/sponsors/ctavan"
], ],
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/esm/bin/uuid"
} }
}, },
"node_modules/v8-compile-cache-lib": { "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" "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": { "node_modules/vt-pbf": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",

View File

@ -7,7 +7,8 @@
"db:seed": "npx prisma db seed" "db:seed": "npx prisma db seed"
}, },
"prisma": { "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": { "dependencies": {
"@evyweb/ioctopus": "^1.2.0", "@evyweb/ioctopus": "^1.2.0",
@ -48,6 +49,7 @@
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
"mapbox-gl": "^3.11.0", "mapbox-gl": "^3.11.0",
"ml-kmeans": "^6.0.0",
"motion": "^12.4.7", "motion": "^12.4.7",
"next": "latest", "next": "latest",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
@ -57,8 +59,10 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-map-gl": "^8.0.3", "react-map-gl": "^8.0.3",
"recharts": "^2.15.2",
"resend": "^4.1.2", "resend": "^4.1.2",
"sonner": "^2.0.1", "sonner": "^2.0.1",
"uuid": "^11.1.0",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^3.24.2", "zod": "^3.24.2",
"zustand": "^5.0.3" "zustand": "^5.0.3"
@ -69,6 +73,7 @@
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/react": "^19.0.2", "@types/react": "^19.0.2",
"@types/react-dom": "19.0.2", "@types/react-dom": "19.0.2",
"@types/uuid": "^10.0.0",
"postcss": "8.4.49", "postcss": "8.4.49",
"prisma": "^6.4.1", "prisma": "^6.4.1",
"react-email": "3.0.7", "react-email": "3.0.7",
@ -77,5 +82,8 @@
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.7.2" "typescript": "^5.7.2"
},
"overrides": {
"react-is": "^19.0.0-rc-69d4b800-20241021"
} }
} }

View File

@ -1,229 +1,270 @@
const crimeCategories = [ export const crimeCategoriesData = [
{ {
name: "TERHADAP KETERTIBAN UMUM", name: "Terhadap Ketertiban Umum",
description: description: "Kejahatan yang mengganggu ketertiban umum seperti unjuk rasa ilegal atau kerusuhan."
"Tindak pidana yang mengganggu ketertiban dan kenyamanan masyarakat secara umum.",
}, },
{ {
name: "MEMBAHAYAKAN KAM UMUM", name: "Membahayakan Kam Umum",
description: description: "Tindakan yang membahayakan keamanan umum, termasuk penggunaan bahan peledak secara ilegal."
"Kejahatan yang berpotensi membahayakan keamanan dan keselamatan masyarakat.",
}, },
{ {
name: "PEMBAKARAN", name: "Pembakaran",
description: description: "Tindakan pembakaran yang disengaja terhadap properti atau bangunan."
"Tindakan membakar properti atau bangunan secara sengaja yang dapat membahayakan orang lain.",
}, },
{ {
name: "KEBAKARAN / MELETUS", name: "Kebakaran / Meletus",
description: description: "Kejadian kebakaran atau ledakan yang menimbulkan kerusakan atau korban."
"Kejadian kebakaran atau ledakan yang disebabkan oleh kelalaian atau tindakan kriminal.",
}, },
{ {
name: "MEMBER SUAP", name: "Member Suap",
description: description: "Memberikan suap kepada pejabat publik atau pihak lain untuk keuntungan pribadi."
"Pemberian sesuatu kepada pejabat untuk mempengaruhi keputusan atau tindakan tertentu.",
}, },
{ {
name: "SUMPAH PALSU", name: "Sumpah Palsu",
description: description: "Memberikan keterangan tidak benar di bawah sumpah dalam proses hukum."
"Memberikan keterangan palsu di bawah sumpah, biasanya dalam proses hukum.",
}, },
{ {
name: "PEMALSUAN MATERAI", name: "Pemalsuan Materai",
description: description: "Pembuatan atau penggunaan materai palsu untuk dokumen resmi."
"Tindakan memalsukan materai resmi dengan tujuan merugikan pihak lain.",
}, },
{ {
name: "PEMALSUAN SURAT", name: "Pemalsuan Surat",
description: description: "Pemalsuan dokumen atau surat dengan tujuan menipu."
"Membuat, mengubah, atau memalsukan surat yang memiliki kekuatan hukum.",
}, },
{ {
name: "PERZINAHAN", name: "Perzinahan",
description: description: "Hubungan seksual antara orang yang salah satunya sudah terikat pernikahan dengan orang lain."
"Hubungan seksual di luar pernikahan yang melanggar norma hukum dan sosial.",
}, },
{ {
name: "PERKOSAAN", name: "Perkosaan",
description: "Tindak pemaksaan hubungan seksual tanpa persetujuan korban.", description: "Pemaksaan hubungan seksual tanpa persetujuan korban."
}, },
{ {
name: "PERJUDIAN", name: "Perjudian",
description: description: "Kegiatan taruhan yang dilarang oleh hukum."
"Segala bentuk kegiatan taruhan atau perjudian yang melanggar hukum.",
}, },
{ {
name: "PENGHINAAN", name: "Penghinaan",
description: "Ucapan atau tindakan yang merendahkan martabat seseorang.", description: "Tindakan menghina atau merendahkan martabat orang lain secara lisan atau tulisan."
}, },
{ {
name: "PENCULIKAN", name: "Penculikan",
description: description: "Pengambilan seseorang secara paksa atau tanpa izin untuk tujuan tertentu."
"Mengambil atau menahan seseorang secara paksa dengan tujuan tertentu.",
}, },
{ {
name: "PERBUATAN TIDAK MENYENANGKAN", name: "Perbuatan Tidak Menyenangkan",
description: description: "Tindakan yang menyebabkan ketidaknyamanan atau ketakutan pada orang lain."
"Tindakan yang mengakibatkan ketidaknyamanan atau kerugian emosional pada orang lain.",
}, },
{ {
name: "PEMBUNUHAN", name: "Pembunuhan",
description: "Menghilangkan nyawa seseorang secara sengaja.", description: "Tindakan menghilangkan nyawa orang lain secara sengaja."
}, },
{ {
name: "PENGANIAYAAN RINGAN", name: "Penganiayaan Ringan",
description: description: "Tindakan kekerasan fisik ringan yang tidak menyebabkan luka berat."
"Tindakan kekerasan yang mengakibatkan luka ringan pada korban.",
}, },
{ {
name: "PENGANIAYAAN BERAT", name: "Penganiayaan Berat",
description: "Kekerasan yang mengakibatkan luka serius atau kematian.", description: "Kekerasan fisik yang menyebabkan luka berat pada korban."
}, },
{ {
name: "KELALAIAN AKIBATKAN ORANG MATI", name: "Kelalaian Akibatkan Orang Mati",
description: "Kelalaian yang menyebabkan kematian seseorang.", description: "Kelalaian yang menyebabkan kematian seseorang."
}, },
{ {
name: "KELALAIAN AKIBATKAN ORANG LUKA", name: "Kelalaian Akibatkan Orang Luka",
description: "Kelalaian yang mengakibatkan luka pada orang lain.", description: "Kelalaian yang menyebabkan seseorang terluka."
}, },
{ {
name: "PENCURIAN BIASA", name: "Pencurian Biasa",
description: "Mengambil barang milik orang lain tanpa izin.", description: "Pencurian yang dilakukan tanpa kekerasan atau perencanaan khusus."
}, },
{ {
name: "CURAT", name: "Curat",
description: description: "Pencurian dengan pemberatan seperti membobol rumah atau bangunan."
"Pencurian dengan pemberatan, seperti pembobolan rumah atau kendaraan.",
}, },
{ {
name: "CURINGAN", name: "Curingan",
description: "Pencurian ringan dengan nilai kerugian yang kecil.", description: "Pencurian ringan terhadap barang-barang bernilai 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: "PREMANISME", name: "Curas",
description: description: "Pencurian dengan kekerasan atau ancaman kekerasan."
"Tindakan kekerasan, pemerasan, atau ancaman oleh kelompok tertentu untuk menguasai wilayah.",
}, },
{ {
name: "PEMERASAN DAN PENGANCAMAN", name: "Curanmor",
description: description: "Pencurian kendaraan bermotor."
"Tindakan meminta sesuatu dengan ancaman kekerasan atau pengungkapan informasi merugikan.",
}, },
{ {
name: "PENGGELAPAN", name: "Pengeroyokan",
description: description: "Tindakan kekerasan oleh beberapa orang terhadap satu atau lebih korban."
"Mengambil barang atau uang yang dipercayakan untuk kepentingan pribadi.",
}, },
{ {
name: "PENIPUAN", name: "Premanisme",
description: description: "Tindakan intimidasi atau kekerasan oleh kelompok preman."
"Menipu orang lain dengan tujuan mendapatkan keuntungan secara melawan hukum.",
}, },
{ {
name: "PENGRUSAKAN", name: "Pemerasan Dan Pengancaman",
description: "Merusak properti orang lain secara sengaja.", description: "Memaksa orang lain menyerahkan sesuatu melalui ancaman."
}, },
{ {
name: "KENAKALAN REMAJA", name: "Penggelapan",
description: description: "Penguasaan barang milik orang lain yang dipercayakan, namun tidak dikembalikan."
"Perilaku menyimpang oleh remaja yang dapat meresahkan masyarakat.",
}, },
{ {
name: "MENERIMA SUAP", name: "Penipuan",
description: description: "Tindakan menipu untuk mendapatkan keuntungan pribadi."
"Menerima sesuatu dengan imbalan pengaruh keputusan atau tindakan tertentu.",
}, },
{ {
name: "PENADAHAN", name: "Pengrusakan",
description: "Menyimpan atau menjual barang hasil kejahatan.", description: "Merusak barang milik orang lain secara sengaja."
}, },
{ {
name: "PEKERJAKAN ANAK", name: "Kenakalan Remaja",
description: description: "Perilaku menyimpang dari norma oleh anak remaja seperti tawuran atau balap liar."
"Mempekerjakan anak di bawah umur dalam pekerjaan yang melanggar hukum.",
}, },
{ {
name: "AGRARIA", name: "Menerima Suap",
description: "Kejahatan terkait sengketa tanah dan sumber daya agraria.", description: "Menerima imbalan untuk mempengaruhi keputusan atau tindakan."
}, },
{ {
name: "PERADILAN ANAK", name: "Penadahan",
description: "Tindak pidana yang melibatkan anak dalam proses peradilan.", description: "Membeli, menyimpan, atau menjual barang hasil kejahatan."
}, },
{ {
name: "PERLINDUNGAN ANAK", name: "Pekerjakan Anak",
description: description: "Mempekerjakan anak di bawah umur dalam pekerjaan yang dilarang oleh hukum."
"Kejahatan yang melanggar hak-hak anak dan kesejahteraan mereka.", },
{
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", name: "PKDRT",
description: description: "Tindak kekerasan dalam rumah tangga baik fisik maupun psikis."
"Tindak kekerasan dalam rumah tangga yang merugikan anggota keluarga.",
}, },
{ {
name: "PERLINDUNGAN TKI", name: "Perlindungan TKI",
description: description: "Perlindungan hukum terhadap Tenaga Kerja Indonesia di luar negeri."
"Kejahatan yang melibatkan pelanggaran terhadap hak Tenaga Kerja Indonesia di luar negeri.",
}, },
{ {
name: "PERLINDUNGAN SAKSI KORBAN", name: "Perlindungan Saksi Korban",
description: description: "Perlindungan bagi saksi atau korban kejahatan dalam proses hukum."
"Tindakan yang mengancam keselamatan saksi atau korban dalam proses hukum.",
}, },
{ {
name: "PTPPO", name: "PTPPO",
description: description: "Perdagangan orang, termasuk eksploitasi tenaga kerja dan seksual."
"Perdagangan orang, termasuk eksploitasi tenaga kerja dan seksual.",
}, },
{ {
name: "PORNOGRAFI", name: "Pornografi",
description: description: "Produksi, distribusi, atau kepemilikan materi pornografi yang melanggar hukum."
"Produksi, distribusi, atau konsumsi materi pornografi yang melanggar hukum.",
}, },
{ {
name: "SISTEM PERADILAN ANAK", name: "Sistem Peradilan Anak",
description: description: "Kerangka hukum dan institusi yang menangani kejahatan oleh anak."
"Pelaksanaan hukum dan keadilan yang berkaitan dengan anak sebagai pelaku kejahatan.",
}, },
{ {
name: "PENYELENGGARAN PEMILU", name: "Penyelenggaraan Pemilu",
description: description: "Kejahatan yang berkaitan dengan pelaksanaan pemilihan umum."
"Kejahatan yang mengganggu proses pemilihan umum, seperti kecurangan suara.",
}, },
{ {
name: "PEMERINTAH DAERAH", name: "Pemerintah Daerah",
description: description: "Tindak pidana yang dilakukan atau melibatkan pejabat pemerintah daerah."
"Tindak pidana yang dilakukan oleh atau melibatkan aparat pemerintah daerah.",
}, },
{ {
name: "KEIMIGRASIAN", name: "Keimigrasian",
description: description: "Kejahatan yang berkaitan dengan dokumen atau proses imigrasi."
"Pelanggaran hukum yang terkait dengan masuk dan keluarnya orang dari suatu negara.",
}, },
{ {
name: "EKSTRADISI", name: "Ekstradisi",
description: description: "Permintaan penyerahan pelaku kejahatan antar negara."
"Proses penyerahan tersangka atau terpidana ke negara lain untuk diadili.",
}, },
{ {
name: "LAHGUN SENPI/HANDAK/SAJAM", name: "Lahgun Senpi/Handak/Sajam",
description: description: "Penyalahgunaan senjata api, bahan peledak, atau senjata tajam."
"Penyalahgunaan senjata api, bahan peledak, atau senjata tajam.",
}, },
{ {
name: "PIDUM LAINNYA", name: "Pidum Lainnya",
description: description: "Tindak pidana umum lainnya yang tidak termasuk dalam kategori tertentu."
"Tindak pidana umum lainnya yang tidak tercakup dalam kategori di atas.",
}, },
]; {
name: "Money Loudering",
export default crimeCategories; 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."
}
]

View File

@ -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.',
};

View File

@ -106,47 +106,79 @@ export const navData = {
orderSeq: 1, orderSeq: 1,
isActive: true, isActive: true,
}, },
{
title: "Crime Categories",
url: "/dashboard/crime-management/crime-categories",
slug: "crime-categories",
icon: IconSlice,
orderSeq: 2,
isActive: true,
},
{ {
title: "Cases", title: "Cases",
url: "/dashboard/crime-management/crime-incident", url: "/dashboard/crime-management/crime-incident",
slug: "crime-incident", slug: "crime-incident",
icon: IconAlertTriangle, 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, orderSeq: 2,
isActive: true, 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", title: "Evidence Management",
url: "/dashboard/crime-management/crime-incident/case-closed", url: "/dashboard/crime-management/evidence",
slug: "resolved-cases", slug: "evidence-management",
icon: IconAlertTriangle, icon: IconPin,
orderSeq: 3, orderSeq: 3,
isActive: true, 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,
}, },
], ],
}, },

View File

@ -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']
}
}
];

View File

@ -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.',
},
];

View File

@ -11,12 +11,11 @@ datasource db {
} }
model cities { model cities {
id String @id id String @id @db.Char(20)
geographic_id String? @db.Uuid
name String @db.VarChar(100) name String @db.VarChar(100)
created_at DateTime @default(now()) @db.Timestamptz(6) created_at DateTime @default(now()) @db.Timestamptz(6)
updated_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[] crimes crimes[]
demographics demographics[] demographics demographics[]
districts districts[] districts districts[]
@ -38,9 +37,9 @@ model contact_messages {
} }
model crime_incidents { model crime_incidents {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid id String @id @db.Char(20)
crime_id String? @db.Uuid crime_id String?
crime_category_id String? @db.Uuid crime_category_id String?
date DateTime @db.Timestamptz(6) date DateTime @db.Timestamptz(6)
time DateTime @db.Timestamptz(6) time DateTime @db.Timestamptz(6)
location String @db.VarChar(255) location String @db.VarChar(255)
@ -48,7 +47,7 @@ model crime_incidents {
longitude Float longitude Float
description String description String
victim_count Int victim_count Int
status crime_status @default(new) status crime_status @default(open)
created_at DateTime @default(now()) @db.Timestamptz(6) created_at DateTime @default(now()) @db.Timestamptz(6)
updated_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]) crime_categories crime_categories? @relation(fields: [crime_category_id], references: [id])
@ -56,7 +55,7 @@ model crime_incidents {
} }
model crime_categories { model crime_categories {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid id String @id @db.Char(20)
name String @db.VarChar(255) name String @db.VarChar(255)
description String description String
created_at DateTime @default(now()) @db.Timestamptz(6) created_at DateTime @default(now()) @db.Timestamptz(6)
@ -65,7 +64,7 @@ model crime_categories {
} }
model crimes { model crimes {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid id String @id @db.Char(20)
district_id String? district_id String?
city_id String? city_id String?
year Int year Int
@ -78,30 +77,29 @@ model crimes {
cities cities? @relation(fields: [city_id], references: [id]) cities cities? @relation(fields: [city_id], references: [id])
districts districts? @relation(fields: [district_id], references: [id]) districts districts? @relation(fields: [district_id], references: [id])
@@unique([city_id, year]) @@unique([city_id, year, created_at])
@@unique([district_id, year]) @@unique([district_id, year, created_at])
} }
model demographics { model demographics {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
district_id String? @unique district_id String?
city_id String? city_id String?
province_id String?
year Int year Int
population Int population Int
population_density Float population_density Float
poverty_rate Float number_of_unemployed Int
created_at DateTime @default(now()) @db.Timestamptz(6) created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6) updated_at DateTime @default(now()) @db.Timestamptz(6)
cities cities? @relation(fields: [city_id], references: [id]) cities cities? @relation(fields: [city_id], references: [id])
districts districts? @relation(fields: [district_id], references: [id]) districts districts? @relation(fields: [district_id], references: [id])
@@unique([city_id, year]) @@unique([city_id, year, created_at])
@@unique([district_id, year]) @@unique([district_id, year, created_at])
} }
model districts { model districts {
id String @id id String @id @db.Char(20)
city_id String city_id String
name String @db.VarChar(100) name String @db.VarChar(100)
created_at DateTime @default(now()) @db.Timestamptz(6) created_at DateTime @default(now()) @db.Timestamptz(6)
@ -116,6 +114,7 @@ model districts {
model geographics { model geographics {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
city_id String
district_id String? @unique district_id String? @unique
latitude Float? latitude Float?
longitude Float? longitude Float?
@ -124,7 +123,7 @@ model geographics {
geometry Json? geometry Json?
created_at DateTime @default(now()) @db.Timestamptz(6) created_at DateTime @default(now()) @db.Timestamptz(6)
updated_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]) districts districts? @relation(fields: [district_id], references: [id])
} }
@ -210,9 +209,10 @@ enum crime_rates {
} }
enum crime_status { enum crime_status {
new open
in_progress closed
resolved resolved
unresolved
} }
// enum roles { // enum roles {

View File

@ -1,275 +1,80 @@
// prisma/seeder.ts
import { PrismaClient } from '@prisma/client'; 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(); const prisma = new PrismaClient();
async function main() { // Interface untuk standarisasi struktur seeder
console.log('Starting seeding...'); interface Seeder {
run: () => Promise<void>;
}
// Create roles // Class utama untuk menjalankan semua seeders
const adminRole = await prisma.roles.upsert({ class DatabaseSeeder {
where: { name: 'admin' }, private seeders: Seeder[] = [];
update: {}, private shouldReset: boolean = false; // Set true jika ingin mereset database sebelum seeding
create: {
name: 'admin', constructor(shouldReset: boolean = true) {
description: 'Administrator with full access to all features', this.shouldReset = shouldReset;
},
});
const viewerRole = await prisma.roles.upsert({ // Daftar semua seeders di sini
where: { name: 'viewer' }, this.seeders = [
update: {}, new RoleSeeder(prisma),
create: { new ResourceSeeder(prisma),
new PermissionSeeder(prisma),
name: 'viewer', new GeoJSONSeeder(prisma),
description: 'Read-only access to data', new DemographicsSeeder(prisma),
}, new CrimeCategoriesSeeder(prisma),
}); new CrimeIncidentsSeeder(prisma),
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']
}
}
]; ];
// 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 async run() {
const allResources = await prisma.resources.findMany(); // Jalankan migrate reset jika diperlukan
if (this.shouldReset) {
// Admin permissions - full access to all resources console.log('🔄 Menjalankan prisma migrate reset...');
for (const resource of allResources) { try {
await createPermissions(adminRole.id, resource.id, ['create', 'read', 'update', 'delete']); // Jalankan perintah dengan --force untuk melewati konfirmasi
} execSync('npx prisma migrate reset --force', { stdio: 'inherit' });
console.log('✅ Database telah direset');
// Viewer permissions - read-only access to all resources } catch (error) {
for (const resource of allResources) { console.error('❌ Gagal mereset database:', error);
await createPermissions(viewerRole.id, resource.id, ['read']); throw error;
}
// Staff permissions - mixed permissions based on resource
for (const resource of allResources) {
if (['roles', 'permissions', 'resources', 'users'].includes(resource.name)) {
// Staff can only read roles, permissions, resources and users
await createPermissions(staffRole.id, resource.id, ['read']);
} else {
// Staff can create, read, update but not delete other resources
await createPermissions(staffRole.id, resource.id, ['create', 'read', 'update']);
} }
} }
console.log('Seeding completed!'); console.log('🌱 Mulai seeding database...');
for (const seeder of this.seeders) {
await seeder.run();
} }
async function createPermissions(roleId: string, resourceId: string, actions: string[]) { console.log('✅ Seeding selesai!');
for (const action of actions) {
await prisma.permissions.createMany({
data: {
action: action,
resource_id: resourceId,
role_id: roleId,
},
skipDuplicates: true // Skip if the permission already exists
}).catch((error) => {
console.error(`Error creating permission for role ${roleId} on resource ${resourceId}:`, error);
});
} }
} }
// File untuk menjalankan seeder
async function main() {
try {
// Parameter pertama mengontrol apakah akan melakukan reset database
// Default: true (akan melakukan reset)
const shouldReset = process.argv.includes('--no-reset') ? false : true;
main() const seeder = new DatabaseSeeder(shouldReset);
.then(async () => { await seeder.run();
await prisma.$disconnect(); } catch (error) {
}) console.error('Error saat seeding:', error);
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1); process.exit(1);
}); } finally {
await prisma.$disconnect();
}
}
// import { PrismaClient } from '@prisma/client'; main();
// 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();
// });

View File

@ -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`);
}
}

View File

@ -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,
};
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}