Compare commits
2 Commits
tasks/fix-
...
tasks/impl
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e822f0b05 | |||
| 34a37dc63b |
13
.qwen/settings.json
Normal file
13
.qwen/settings.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright-mcp": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"playwright-mcp@latest"
|
||||
],
|
||||
"timeout": 60000
|
||||
}
|
||||
},
|
||||
"$version": 3
|
||||
}
|
||||
9
.qwen/settings.json.orig
Normal file
9
.qwen/settings.json.orig
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright-mcp": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "playwright-mcp@latest"],
|
||||
"timeout": 60000
|
||||
}
|
||||
}
|
||||
}
|
||||
49
biome.json
Normal file
49
biome.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"experimentalScannerIgnores": [
|
||||
"node_modules",
|
||||
".next",
|
||||
"out",
|
||||
"public"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"noUnusedVariables": "warn",
|
||||
"noUnusedImports": "warn"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "warn"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "warn"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "always"
|
||||
}
|
||||
}
|
||||
}
|
||||
133
bun.lock
133
bun.lock
@@ -111,10 +111,11 @@
|
||||
"@types/react-dom": "^19",
|
||||
"@vitest/ui": "^4.0.18",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"eslint-config-next": "15.5.12",
|
||||
"jsdom": "^28.0.0",
|
||||
"msw": "^2.12.9",
|
||||
"parcel": "^2.6.2",
|
||||
"playwright-mcp": "^0.0.19",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
@@ -246,6 +247,8 @@
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.13", "", { "peerDependencies": { "hono": "^4" } }, "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
||||
@@ -360,6 +363,8 @@
|
||||
|
||||
"@mischnic/json-sourcemap": ["@mischnic/json-sourcemap@0.1.1", "", { "dependencies": { "@lezer/common": "^1.0.0", "@lezer/lr": "^1.0.0", "json5": "^2.2.1" } }, "sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="],
|
||||
@@ -378,7 +383,7 @@
|
||||
|
||||
"@next/env": ["@next/env@15.5.14", "", {}, "sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA=="],
|
||||
|
||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.1.6", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw=="],
|
||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.12", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-+ZRSDFTv4aC96aMb5E41rMjysx8ApkryevnvEYZvPZO52KvkqP5rNExLUXJFr9P4s0f3oqNQR6vopCZsPWKDcQ=="],
|
||||
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y9K6SPzobnZvrRDPO2s0grgzC+Egf0CqfbdvYmQVaztV890zicw8Z8+4Vqw8oPck8r1TjUHxVh8299Cg4TrxXg=="],
|
||||
|
||||
@@ -600,6 +605,8 @@
|
||||
|
||||
"@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
|
||||
|
||||
"@posthog/core": ["@posthog/core@1.25.1", "", {}, "sha512-76enEGwLVtcCTbfAr1wJ6zi4tYzVkGsqJx+H9JiX0K8/VxuKdbOtVShsOJhTD9uvFfTeW3DX+PSCelcqWrRRkw=="],
|
||||
|
||||
"@prisma/client": ["@prisma/client@6.3.1", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-ARAJaPs+eBkemdky/XU3cvGRl+mIPHCN2lCXsl5Vlb0E2gV+R6IN7aCI8CisRGszEZondwIsW9Iz8EJkTdykyA=="],
|
||||
|
||||
"@prisma/debug": ["@prisma/debug@6.3.1", "", {}, "sha512-RrEBkd+HLZx+ydfmYT0jUj7wjLiS95wfTOSQ+8FQbvb6vHh5AeKfEPt/XUQ5+Buljj8hltEfOslEW57/wQIVeA=="],
|
||||
@@ -662,6 +669,8 @@
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
|
||||
|
||||
"@skorotkiewicz/snowflake-id": ["@skorotkiewicz/snowflake-id@1.0.1", "", {}, "sha512-leZkiZoBgg6zpVfg+cMkehr/bdKXGa6pOoX0rNc1C2jVdGTWa87+/BBgIy8C4uVtI404cuOMBu2IzIXI1BE3TQ=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
@@ -926,6 +935,8 @@
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.1.2", "", { "dependencies": { "@vitest/pretty-format": "4.1.2", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
@@ -938,6 +949,8 @@
|
||||
|
||||
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"animate.css": ["animate.css@4.1.1", "", {}, "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
@@ -994,6 +1007,8 @@
|
||||
|
||||
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
@@ -1008,6 +1023,8 @@
|
||||
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
@@ -1054,6 +1071,10 @@
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
@@ -1062,6 +1083,8 @@
|
||||
|
||||
"core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
|
||||
|
||||
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
|
||||
@@ -1132,6 +1155,8 @@
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
@@ -1154,6 +1179,8 @@
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.331", "", {}, "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q=="],
|
||||
|
||||
"elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="],
|
||||
@@ -1168,6 +1195,8 @@
|
||||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
@@ -1196,11 +1225,13 @@
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
|
||||
|
||||
"eslint-config-next": ["eslint-config-next@15.1.6", "", { "dependencies": { "@next/eslint-plugin-next": "15.1.6", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-Wd1uy6y7nBbXUSg9QAuQ+xYEKli5CgUhLjz1QHW11jLDis5vK5XB3PemL6jEmy7HrdhaRFDz+GTZ/3FoH+EUjg=="],
|
||||
"eslint-config-next": ["eslint-config-next@15.5.12", "", { "dependencies": { "@next/eslint-plugin-next": "15.5.12", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-ktW3XLfd+ztEltY5scJNjxjHwtKWk6vU2iwzZqSN09UsbBmMeE/cVlJ1yESg6Yx5LW7p/Z8WzUAgYXGLEmGIpg=="],
|
||||
|
||||
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="],
|
||||
|
||||
@@ -1232,8 +1263,14 @@
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
|
||||
|
||||
"exif-js": ["exif-js@2.3.0", "", {}, "sha512-1Og9pAzG2FZRVlaavH8bB8BTeHcjMdJhKmeQITkX+uLRCD0xPtKAdZ2clZmQdJ56p9adXtJ8+jwrGp/4505lYg=="],
|
||||
@@ -1242,6 +1279,10 @@
|
||||
|
||||
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="],
|
||||
|
||||
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
@@ -1254,6 +1295,8 @@
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||
|
||||
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
||||
@@ -1272,6 +1315,8 @@
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
@@ -1286,8 +1331,12 @@
|
||||
|
||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
@@ -1324,6 +1373,8 @@
|
||||
|
||||
"graphql": ["graphql@16.13.2", "", {}, "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig=="],
|
||||
|
||||
"happy-dom": ["happy-dom@17.6.3", "", { "dependencies": { "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0" } }, "sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
@@ -1342,14 +1393,20 @@
|
||||
|
||||
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
|
||||
|
||||
"hono": ["hono@4.12.12", "", {}, "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
@@ -1362,10 +1419,16 @@
|
||||
|
||||
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"iron-session": ["iron-session@8.0.4", "", { "dependencies": { "cookie": "^0.7.2", "iron-webcrypto": "^1.2.1", "uncrypto": "^0.1.3" } }, "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA=="],
|
||||
|
||||
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
|
||||
@@ -1412,6 +1475,8 @@
|
||||
|
||||
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||
|
||||
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
||||
@@ -1454,6 +1519,8 @@
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
@@ -1546,8 +1613,12 @@
|
||||
|
||||
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
@@ -1586,6 +1657,8 @@
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"next": ["next@15.5.14", "", { "dependencies": { "@next/env": "15.5.14", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.14", "@next/swc-darwin-x64": "15.5.14", "@next/swc-linux-arm64-gnu": "15.5.14", "@next/swc-linux-arm64-musl": "15.5.14", "@next/swc-linux-x64-gnu": "15.5.14", "@next/swc-linux-x64-musl": "15.5.14", "@next/swc-win32-arm64-msvc": "15.5.14", "@next/swc-win32-x64-msvc": "15.5.14", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ=="],
|
||||
|
||||
"next-view-transitions": ["next-view-transitions@0.3.5", "", { "peerDependencies": { "next": ">=14.0.0", "react": ">=18.2.0 || ^19.0.0", "react-dom": ">=18.2.0 || ^19.0.0" } }, "sha512-yP8OPNydLmKpmE94hLurLGEzPsUy1uyl9iSv8oxaC2JwhSXTD86SVwk1NMMQT7Ado4kMENDJ7fNBIXHy3GU/Lg=="],
|
||||
@@ -1624,6 +1697,8 @@
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
@@ -1650,6 +1725,8 @@
|
||||
|
||||
"parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
@@ -1668,10 +1745,14 @@
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
|
||||
|
||||
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
|
||||
|
||||
"playwright-mcp": ["playwright-mcp@0.0.19", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.25.1", "@skorotkiewicz/snowflake-id": "^1.0.1", "happy-dom": "^17.4.4", "lodash": "^4.17.21", "playwright": "^1.51.0", "posthog-node": "^5.11.1", "zod": "^3.24.2" }, "bin": { "playwright-mcp": "dist/server.js" } }, "sha512-KzqO66bShWvXQU7pGrb/NkQ3pQKRAxOK5H+tHVUCBv/0X+2fcV7AJrsgr97BOAo5dv+/7ZrNK6o7aMkMJ/MMmA=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
@@ -1690,6 +1771,8 @@
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"posthog-node": ["posthog-node@5.29.1", "", { "dependencies": { "@posthog/core": "1.25.1" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-XA1OGrE8SAmO3JkfWjeVg7XxEN/6M6ZXzYmBJKl3uXv/xxk7ru0oeoqi/rcWJ6z5+nFP9wUFVoS37//qaPxwFg=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"primeicons": ["primeicons@7.0.0", "", {}, "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="],
|
||||
@@ -1736,6 +1819,8 @@
|
||||
|
||||
"prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"proxy-compare": ["proxy-compare@3.0.1", "", {}, "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="],
|
||||
|
||||
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
|
||||
@@ -1744,10 +1829,16 @@
|
||||
|
||||
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
|
||||
|
||||
"qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"ramda": ["ramda@0.27.2", "", {}, "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||
@@ -1820,6 +1911,8 @@
|
||||
|
||||
"rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
@@ -1830,18 +1923,26 @@
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||
|
||||
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
@@ -1940,6 +2041,8 @@
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
@@ -1958,6 +2061,8 @@
|
||||
|
||||
"type-fest": ["type-fest@5.5.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
|
||||
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
|
||||
@@ -1980,6 +2085,8 @@
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
|
||||
|
||||
"until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="],
|
||||
@@ -2010,6 +2117,8 @@
|
||||
|
||||
"valtio": ["valtio@2.3.1", "", { "dependencies": { "proxy-compare": "^3.0.1" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-ZsOaOEn0U9IJ96cAj3CZ3GjwpN3EJdjsi1PT4PREuB+Pcqfsczu16isT5DT1UrmHbk4PtLjk8kwNEHuR2CX56w=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"victory-vendor": ["victory-vendor@37.3.6", "", { "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" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
||||
|
||||
"vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
|
||||
@@ -2072,6 +2181,8 @@
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||
|
||||
"@cubejs-client/core/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"@elysiajs/cookie/cookie": ["cookie@0.5.0", "", {}, "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="],
|
||||
@@ -2084,6 +2195,8 @@
|
||||
|
||||
"@mantine/core/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"@parcel/core/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||
|
||||
"@parcel/packager-js/globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
|
||||
@@ -2102,6 +2215,8 @@
|
||||
|
||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"babel-plugin-macros/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||
|
||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
@@ -2120,10 +2235,16 @@
|
||||
|
||||
"eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"happy-dom/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
|
||||
|
||||
"happy-dom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
||||
|
||||
"iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"lmdb/node-addon-api": ["node-addon-api@6.1.0", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="],
|
||||
@@ -2146,10 +2267,14 @@
|
||||
|
||||
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||
|
||||
"string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@parcel/packager-js/globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
|
||||
|
||||
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
|
||||
@@ -2158,6 +2283,8 @@
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||
|
||||
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"cross-fetch/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
"jsdom": "^28.0.0",
|
||||
"msw": "^2.12.9",
|
||||
"parcel": "^2.6.2",
|
||||
"playwright-mcp": "^0.0.19",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
|
||||
27
src/app/admin/(dashboard)/_state/kependudukan/dashboard.ts
Normal file
27
src/app/admin/(dashboard)/_state/kependudukan/dashboard.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { proxy } from "valtio";
|
||||
|
||||
const kependudukanDashboard = proxy({
|
||||
summary: {
|
||||
data: null as any,
|
||||
loading: false,
|
||||
async load() {
|
||||
kependudukanDashboard.summary.loading = true;
|
||||
try {
|
||||
const res = await ApiFetch.api.kependudukan.dashboard.summary.get();
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
kependudukanDashboard.summary.data = res.data.data;
|
||||
} else {
|
||||
kependudukanDashboard.summary.data = null;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch dashboard summary:", err);
|
||||
kependudukanDashboard.summary.data = null;
|
||||
} finally {
|
||||
kependudukanDashboard.summary.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default kependudukanDashboard;
|
||||
205
src/app/admin/(dashboard)/_state/kependudukan/data-banjar.ts
Normal file
205
src/app/admin/(dashboard)/_state/kependudukan/data-banjar.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { proxy } from "valtio";
|
||||
import { toast } from "react-toastify";
|
||||
import { z } from "zod";
|
||||
|
||||
const templateDataBanjar = z.object({
|
||||
nama: z.string().min(1, "Nama banjar harus diisi"),
|
||||
penduduk: z.number().min(0, "Jumlah penduduk harus diisi"),
|
||||
kk: z.number().min(0, "Jumlah KK harus diisi"),
|
||||
miskin: z.number().min(0, "Jumlah penduduk miskin harus diisi"),
|
||||
tahun: z.number().min(2000, "Tahun harus diisi"),
|
||||
});
|
||||
|
||||
const dataBanjar = proxy({
|
||||
create: {
|
||||
form: {
|
||||
nama: "",
|
||||
penduduk: 0,
|
||||
kk: 0,
|
||||
miskin: 0,
|
||||
tahun: new Date().getFullYear(),
|
||||
},
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateDataBanjar.safeParse(dataBanjar.create.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
dataBanjar.create.loading = true;
|
||||
const res = await ApiFetch.api.kependudukan.databanjar["create"].post(dataBanjar.create.form);
|
||||
|
||||
if (res.status === 200) {
|
||||
const id = res.data?.data?.id;
|
||||
if (id) {
|
||||
toast.success("Sukses menambahkan data banjar");
|
||||
dataBanjar.create.form = { nama: "", penduduk: 0, kk: 0, miskin: 0, tahun: new Date().getFullYear() };
|
||||
dataBanjar.findMany.load();
|
||||
return id;
|
||||
}
|
||||
}
|
||||
toast.error("Gagal menambahkan data");
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
return null;
|
||||
} finally {
|
||||
dataBanjar.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
findMany: {
|
||||
data: null as any[] | null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "", tahun = new Date().getFullYear()) => {
|
||||
dataBanjar.findMany.loading = true;
|
||||
dataBanjar.findMany.page = page;
|
||||
dataBanjar.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit, tahun };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.kependudukan.databanjar["find-many"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
dataBanjar.findMany.data = res.data.data ?? [];
|
||||
dataBanjar.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
dataBanjar.findMany.data = [];
|
||||
dataBanjar.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch data banjar paginated:", err);
|
||||
dataBanjar.findMany.data = [];
|
||||
dataBanjar.findMany.totalPages = 1;
|
||||
} finally {
|
||||
dataBanjar.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
findUnique: {
|
||||
data: null as any | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/kependudukan/databanjar/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
dataBanjar.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch data banjar:", res.statusText);
|
||||
dataBanjar.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data banjar:", error);
|
||||
dataBanjar.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
update: {
|
||||
id: "",
|
||||
form: {
|
||||
nama: "",
|
||||
penduduk: 0,
|
||||
kk: 0,
|
||||
miskin: 0,
|
||||
tahun: new Date().getFullYear(),
|
||||
},
|
||||
loading: false,
|
||||
async submit() {
|
||||
const id = this.id;
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
nama: this.form.nama,
|
||||
penduduk: this.form.penduduk,
|
||||
kk: this.form.kk,
|
||||
miskin: this.form.miskin,
|
||||
tahun: this.form.tahun,
|
||||
};
|
||||
|
||||
const cek = templateDataBanjar.safeParse(formData);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
const res = await fetch(`/api/kependudukan/databanjar/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!res.ok || !result?.success) {
|
||||
throw new Error(result?.message || "Gagal update data");
|
||||
}
|
||||
|
||||
toast.success("Berhasil update data!");
|
||||
await dataBanjar.findMany.load();
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error("Update error:", error);
|
||||
toast.error("Gagal update data banjar");
|
||||
throw error;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
dataBanjar.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/kependudukan/databanjar/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "Data banjar berhasil dihapus");
|
||||
await dataBanjar.findMany.load();
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus data banjar");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus data banjar");
|
||||
} finally {
|
||||
dataBanjar.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default dataBanjar;
|
||||
@@ -0,0 +1,197 @@
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { proxy } from "valtio";
|
||||
import { toast } from "react-toastify";
|
||||
import { z } from "zod";
|
||||
|
||||
const templateDistribusiAgama = z.object({
|
||||
agama: z.string().min(1, "Agama harus diisi"),
|
||||
jumlah: z.number().min(0, "Jumlah harus diisi"),
|
||||
tahun: z.number().min(2000, "Tahun harus diisi"),
|
||||
});
|
||||
|
||||
const distribusiAgama = proxy({
|
||||
create: {
|
||||
form: {
|
||||
agama: "",
|
||||
jumlah: 0,
|
||||
tahun: new Date().getFullYear(),
|
||||
},
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateDistribusiAgama.safeParse(distribusiAgama.create.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
distribusiAgama.create.loading = true;
|
||||
const res = await ApiFetch.api.kependudukan.distribusiagama["create"].post(distribusiAgama.create.form);
|
||||
|
||||
if (res.status === 200) {
|
||||
const id = res.data?.data?.id;
|
||||
if (id) {
|
||||
toast.success("Sukses menambahkan distribusi agama");
|
||||
distribusiAgama.create.form = { agama: "", jumlah: 0, tahun: new Date().getFullYear() };
|
||||
distribusiAgama.findMany.load();
|
||||
return id;
|
||||
}
|
||||
}
|
||||
toast.error("Gagal menambahkan data");
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
return null;
|
||||
} finally {
|
||||
distribusiAgama.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
findMany: {
|
||||
data: null as any[] | null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "", tahun = new Date().getFullYear()) => {
|
||||
distribusiAgama.findMany.loading = true;
|
||||
distribusiAgama.findMany.page = page;
|
||||
distribusiAgama.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit, tahun };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.kependudukan.distribusiagama["find-many"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
distribusiAgama.findMany.data = res.data.data ?? [];
|
||||
distribusiAgama.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
distribusiAgama.findMany.data = [];
|
||||
distribusiAgama.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch distribusi agama paginated:", err);
|
||||
distribusiAgama.findMany.data = [];
|
||||
distribusiAgama.findMany.totalPages = 1;
|
||||
} finally {
|
||||
distribusiAgama.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
findUnique: {
|
||||
data: null as any | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/kependudukan/distribusiagama/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
distribusiAgama.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch distribusiAgama:", res.statusText);
|
||||
distribusiAgama.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching distribusiAgama:", error);
|
||||
distribusiAgama.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
update: {
|
||||
id: "",
|
||||
form: {
|
||||
agama: "",
|
||||
jumlah: 0,
|
||||
tahun: new Date().getFullYear(),
|
||||
},
|
||||
loading: false,
|
||||
async submit() {
|
||||
const id = this.id;
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
agama: this.form.agama,
|
||||
jumlah: this.form.jumlah,
|
||||
tahun: this.form.tahun,
|
||||
};
|
||||
|
||||
const cek = templateDistribusiAgama.safeParse(formData);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
const res = await fetch(`/api/kependudukan/distribusiagama/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!res.ok || !result?.success) {
|
||||
throw new Error(result?.message || "Gagal update data");
|
||||
}
|
||||
|
||||
toast.success("Berhasil update data!");
|
||||
await distribusiAgama.findMany.load();
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error("Update error:", error);
|
||||
toast.error("Gagal update data distribusi agama");
|
||||
throw error;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
distribusiAgama.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/kependudukan/distribusiagama/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "Distribusi agama berhasil dihapus");
|
||||
await distribusiAgama.findMany.load();
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus distribusi agama");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus distribusi agama");
|
||||
} finally {
|
||||
distribusiAgama.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default distribusiAgama;
|
||||
197
src/app/admin/(dashboard)/_state/kependudukan/distribusi-umur.ts
Normal file
197
src/app/admin/(dashboard)/_state/kependudukan/distribusi-umur.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { proxy } from "valtio";
|
||||
import { toast } from "react-toastify";
|
||||
import { z } from "zod";
|
||||
|
||||
const templateDistribusiUmur = z.object({
|
||||
rentangUmur: z.string().min(1, "Rentang umur harus diisi"),
|
||||
jumlah: z.number().min(0, "Jumlah harus diisi"),
|
||||
tahun: z.number().min(2000, "Tahun harus diisi"),
|
||||
});
|
||||
|
||||
const distribusiUmur = proxy({
|
||||
create: {
|
||||
form: {
|
||||
rentangUmur: "",
|
||||
jumlah: 0,
|
||||
tahun: new Date().getFullYear(),
|
||||
},
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateDistribusiUmur.safeParse(distribusiUmur.create.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
distribusiUmur.create.loading = true;
|
||||
const res = await ApiFetch.api.kependudukan.distribusiumur["create"].post(distribusiUmur.create.form);
|
||||
|
||||
if (res.status === 200) {
|
||||
const id = res.data?.data?.id;
|
||||
if (id) {
|
||||
toast.success("Sukses menambahkan distribusi umur");
|
||||
distribusiUmur.create.form = { rentangUmur: "", jumlah: 0, tahun: new Date().getFullYear() };
|
||||
distribusiUmur.findMany.load();
|
||||
return id;
|
||||
}
|
||||
}
|
||||
toast.error("Gagal menambahkan data");
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
return null;
|
||||
} finally {
|
||||
distribusiUmur.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
findMany: {
|
||||
data: null as any[] | null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "", tahun = new Date().getFullYear()) => {
|
||||
distribusiUmur.findMany.loading = true;
|
||||
distribusiUmur.findMany.page = page;
|
||||
distribusiUmur.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit, tahun };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.kependudukan.distribusiumur["find-many"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
distribusiUmur.findMany.data = res.data.data ?? [];
|
||||
distribusiUmur.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
distribusiUmur.findMany.data = [];
|
||||
distribusiUmur.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch distribusi umur paginated:", err);
|
||||
distribusiUmur.findMany.data = [];
|
||||
distribusiUmur.findMany.totalPages = 1;
|
||||
} finally {
|
||||
distribusiUmur.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
findUnique: {
|
||||
data: null as any | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/kependudukan/distribusiumur/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
distribusiUmur.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch distribusi umur:", res.statusText);
|
||||
distribusiUmur.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching distribusi umur:", error);
|
||||
distribusiUmur.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
update: {
|
||||
id: "",
|
||||
form: {
|
||||
rentangUmur: "",
|
||||
jumlah: 0,
|
||||
tahun: new Date().getFullYear(),
|
||||
},
|
||||
loading: false,
|
||||
async submit() {
|
||||
const id = this.id;
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
rentangUmur: this.form.rentangUmur,
|
||||
jumlah: this.form.jumlah,
|
||||
tahun: this.form.tahun,
|
||||
};
|
||||
|
||||
const cek = templateDistribusiUmur.safeParse(formData);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
const res = await fetch(`/api/kependudukan/distribusiumur/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!res.ok || !result?.success) {
|
||||
throw new Error(result?.message || "Gagal update data");
|
||||
}
|
||||
|
||||
toast.success("Berhasil update data!");
|
||||
await distribusiUmur.findMany.load();
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error("Update error:", error);
|
||||
toast.error("Gagal update data distribusi umur");
|
||||
throw error;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
distribusiUmur.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/kependudukan/distribusiumur/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "Distribusi umur berhasil dihapus");
|
||||
await distribusiUmur.findMany.load();
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus distribusi umur");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus distribusi umur");
|
||||
} finally {
|
||||
distribusiUmur.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default distribusiUmur;
|
||||
@@ -0,0 +1,209 @@
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { proxy } from "valtio";
|
||||
import { toast } from "react-toastify";
|
||||
import { z } from "zod";
|
||||
|
||||
const templateMigrasiPenduduk = z.object({
|
||||
jenis: z.string().min(1, "Jenis migrasi harus diisi"),
|
||||
nama: z.string().min(1, "Nama harus diisi"),
|
||||
tanggal: z.string().min(1, "Tanggal harus diisi"),
|
||||
asalTujuan: z.string().min(1, "Asal/Tujuan harus diisi"),
|
||||
alasan: z.string().optional(),
|
||||
jenisKelamin: z.string().optional(),
|
||||
});
|
||||
|
||||
const migrasiPenduduk = proxy({
|
||||
create: {
|
||||
form: {
|
||||
jenis: "",
|
||||
nama: "",
|
||||
tanggal: "",
|
||||
asalTujuan: "",
|
||||
alasan: "",
|
||||
jenisKelamin: "",
|
||||
},
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateMigrasiPenduduk.safeParse(migrasiPenduduk.create.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
migrasiPenduduk.create.loading = true;
|
||||
const res = await ApiFetch.api.kependudukan.migrasipenduduk["create"].post(migrasiPenduduk.create.form);
|
||||
|
||||
if (res.status === 200) {
|
||||
const id = res.data?.data?.id;
|
||||
if (id) {
|
||||
toast.success("Sukses menambahkan data migrasi penduduk");
|
||||
migrasiPenduduk.create.form = { jenis: "", nama: "", tanggal: "", asalTujuan: "", alasan: "", jenisKelamin: "" };
|
||||
migrasiPenduduk.findMany.load();
|
||||
return id;
|
||||
}
|
||||
}
|
||||
toast.error("Gagal menambahkan data");
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
return null;
|
||||
} finally {
|
||||
migrasiPenduduk.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
findMany: {
|
||||
data: null as any[] | null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "", tahun = new Date().getFullYear()) => {
|
||||
migrasiPenduduk.findMany.loading = true;
|
||||
migrasiPenduduk.findMany.page = page;
|
||||
migrasiPenduduk.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit, tahun };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.kependudukan.migrasipenduduk["find-many"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
migrasiPenduduk.findMany.data = res.data.data ?? [];
|
||||
migrasiPenduduk.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
migrasiPenduduk.findMany.data = [];
|
||||
migrasiPenduduk.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch migrasi penduduk paginated:", err);
|
||||
migrasiPenduduk.findMany.data = [];
|
||||
migrasiPenduduk.findMany.totalPages = 1;
|
||||
} finally {
|
||||
migrasiPenduduk.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
findUnique: {
|
||||
data: null as any | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/kependudukan/migrasipenduduk/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
migrasiPenduduk.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch migrasi penduduk:", res.statusText);
|
||||
migrasiPenduduk.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching migrasi penduduk:", error);
|
||||
migrasiPenduduk.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
update: {
|
||||
id: "",
|
||||
form: {
|
||||
jenis: "",
|
||||
nama: "",
|
||||
tanggal: "",
|
||||
asalTujuan: "",
|
||||
alasan: "",
|
||||
jenisKelamin: "",
|
||||
},
|
||||
loading: false,
|
||||
async submit() {
|
||||
const id = this.id;
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
jenis: this.form.jenis,
|
||||
nama: this.form.nama,
|
||||
tanggal: this.form.tanggal,
|
||||
asalTujuan: this.form.asalTujuan,
|
||||
alasan: this.form.alasan,
|
||||
jenisKelamin: this.form.jenisKelamin,
|
||||
};
|
||||
|
||||
const cek = templateMigrasiPenduduk.safeParse(formData);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
const res = await fetch(`/api/kependudukan/migrasipenduduk/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!res.ok || !result?.success) {
|
||||
throw new Error(result?.message || "Gagal update data");
|
||||
}
|
||||
|
||||
toast.success("Berhasil update data!");
|
||||
await migrasiPenduduk.findMany.load();
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error("Update error:", error);
|
||||
toast.error("Gagal update data migrasi penduduk");
|
||||
throw error;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
migrasiPenduduk.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/kependudukan/migrasipenduduk/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "Data migrasi penduduk berhasil dihapus");
|
||||
await migrasiPenduduk.findMany.load();
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus data migrasi penduduk");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus data migrasi penduduk");
|
||||
} finally {
|
||||
migrasiPenduduk.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default migrasiPenduduk;
|
||||
249
src/app/admin/(dashboard)/kependudukan/data-banjar/[id]/page.tsx
Normal file
249
src/app/admin/(dashboard)/kependudukan/data-banjar/[id]/page.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Title,
|
||||
NumberInput,
|
||||
TextInput
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import dataBanjar from '../../../_state/kependudukan/data-banjar';
|
||||
|
||||
interface FormData {
|
||||
nama: string;
|
||||
penduduk: number;
|
||||
kk: number;
|
||||
miskin: number;
|
||||
tahun: number;
|
||||
}
|
||||
|
||||
export default function EditDataBanjar() {
|
||||
const router = useRouter();
|
||||
const { id } = useParams() as { id: string };
|
||||
const stateDataBanjar = useProxy(dataBanjar);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
nama: '',
|
||||
penduduk: 0,
|
||||
kk: 0,
|
||||
miskin: 0,
|
||||
tahun: new Date().getFullYear(),
|
||||
});
|
||||
const [originalData, setOriginalData] = useState<FormData>({
|
||||
nama: '',
|
||||
penduduk: 0,
|
||||
kk: 0,
|
||||
miskin: 0,
|
||||
tahun: new Date().getFullYear(),
|
||||
});
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const isFormValid = () => {
|
||||
return (
|
||||
formData.nama?.trim() !== '' &&
|
||||
formData.penduduk !== null &&
|
||||
formData.penduduk >= 0 &&
|
||||
formData.kk !== null &&
|
||||
formData.kk >= 0 &&
|
||||
formData.miskin !== null &&
|
||||
formData.miskin >= 0 &&
|
||||
formData.tahun !== null
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
stateDataBanjar.update.id = id;
|
||||
await stateDataBanjar.findUnique.load(id);
|
||||
|
||||
const data = stateDataBanjar.findUnique.data;
|
||||
if (data) {
|
||||
setFormData({
|
||||
nama: data.nama ?? '',
|
||||
penduduk: Number(data.penduduk ?? 0),
|
||||
kk: Number(data.kk ?? 0),
|
||||
miskin: Number(data.miskin ?? 0),
|
||||
tahun: Number(data.tahun ?? currentYear),
|
||||
});
|
||||
setOriginalData({
|
||||
nama: data.nama ?? '',
|
||||
penduduk: Number(data.penduduk ?? 0),
|
||||
kk: Number(data.kk ?? 0),
|
||||
miskin: Number(data.miskin ?? 0),
|
||||
tahun: Number(data.tahun ?? currentYear),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
toast.error('Gagal memuat data');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [id]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(field: keyof FormData) =>
|
||||
(value: any) => {
|
||||
const val =
|
||||
field === 'penduduk' || field === 'kk' || field === 'miskin' || field === 'tahun'
|
||||
? Number(value || 0)
|
||||
: value;
|
||||
|
||||
setFormData((prev) => ({ ...prev, [field]: val }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleResetForm = () => {
|
||||
setFormData({
|
||||
nama: originalData.nama,
|
||||
penduduk: Number(originalData.penduduk),
|
||||
kk: Number(originalData.kk),
|
||||
miskin: Number(originalData.miskin),
|
||||
tahun: Number(originalData.tahun),
|
||||
});
|
||||
toast.info("Form dikembalikan ke data awal");
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
stateDataBanjar.update.id = id;
|
||||
stateDataBanjar.update.form = { ...formData };
|
||||
|
||||
await stateDataBanjar.update.submit();
|
||||
|
||||
toast.success('Data berhasil diperbarui');
|
||||
router.push('/admin/kependudukan/data-banjar');
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
toast.error('Gagal memperbarui data');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Data Banjar
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form Card */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Banjar"
|
||||
placeholder="Masukkan nama banjar"
|
||||
value={formData.nama}
|
||||
onChange={handleChange('nama')}
|
||||
required
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Jumlah Penduduk"
|
||||
placeholder="Masukkan jumlah penduduk"
|
||||
value={formData.penduduk}
|
||||
onChange={handleChange('penduduk')}
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Jumlah KK"
|
||||
placeholder="Masukkan jumlah KK"
|
||||
value={formData.kk}
|
||||
onChange={handleChange('kk')}
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Jumlah Penduduk Miskin"
|
||||
placeholder="Masukkan jumlah penduduk miskin"
|
||||
value={formData.miskin}
|
||||
onChange={handleChange('miskin')}
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Tahun"
|
||||
placeholder="Masukkan tahun"
|
||||
value={formData.tahun}
|
||||
onChange={handleChange('tahun')}
|
||||
min={2000}
|
||||
max={currentYear + 1}
|
||||
required
|
||||
/>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="md"
|
||||
onClick={handleResetForm}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
|
||||
{/* Tombol Simpan */}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
style={{
|
||||
background: !isFormValid() || isSubmitting
|
||||
? `linear-gradient(135deg, #cccccc, #eeeeee)`
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Title,
|
||||
NumberInput,
|
||||
TextInput
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import dataBanjar from '../../../_state/kependudukan/data-banjar';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
function CreateDataBanjar() {
|
||||
const stateDataBanjar = useProxy(dataBanjar);
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const yearOptions = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: String(currentYear - i),
|
||||
label: String(currentYear - i),
|
||||
}));
|
||||
|
||||
const isFormValid = () => {
|
||||
return (
|
||||
stateDataBanjar.create.form.nama?.trim() !== '' &&
|
||||
stateDataBanjar.create.form.penduduk !== null &&
|
||||
stateDataBanjar.create.form.penduduk >= 0 &&
|
||||
stateDataBanjar.create.form.kk !== null &&
|
||||
stateDataBanjar.create.form.kk >= 0 &&
|
||||
stateDataBanjar.create.form.miskin !== null &&
|
||||
stateDataBanjar.create.form.miskin >= 0 &&
|
||||
stateDataBanjar.create.form.tahun !== null
|
||||
);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
stateDataBanjar.create.form = {
|
||||
nama: '',
|
||||
penduduk: 0,
|
||||
kk: 0,
|
||||
miskin: 0,
|
||||
tahun: currentYear,
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const id = await stateDataBanjar.create.create();
|
||||
if (id) {
|
||||
resetForm();
|
||||
router.push('/admin/kependudukan/data-banjar');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating data banjar:', error);
|
||||
toast.error('Terjadi kesalahan saat menambah data banjar');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Data Banjar
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Banjar"
|
||||
placeholder="Masukkan nama banjar"
|
||||
value={stateDataBanjar.create.form.nama}
|
||||
onChange={(e) => {
|
||||
stateDataBanjar.create.form.nama = e.currentTarget.value;
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Jumlah Penduduk"
|
||||
placeholder="Masukkan jumlah penduduk"
|
||||
value={stateDataBanjar.create.form.penduduk}
|
||||
onChange={(val) => {
|
||||
stateDataBanjar.create.form.penduduk = Number(val || 0);
|
||||
}}
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Jumlah KK"
|
||||
placeholder="Masukkan jumlah KK"
|
||||
value={stateDataBanjar.create.form.kk}
|
||||
onChange={(val) => {
|
||||
stateDataBanjar.create.form.kk = Number(val || 0);
|
||||
}}
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Jumlah Penduduk Miskin"
|
||||
placeholder="Masukkan jumlah penduduk miskin"
|
||||
value={stateDataBanjar.create.form.miskin}
|
||||
onChange={(val) => {
|
||||
stateDataBanjar.create.form.miskin = Number(val || 0);
|
||||
}}
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Tahun"
|
||||
placeholder="Masukkan tahun"
|
||||
value={stateDataBanjar.create.form.tahun}
|
||||
onChange={(val) => {
|
||||
stateDataBanjar.create.form.tahun = Number(val || currentYear);
|
||||
}}
|
||||
min={2000}
|
||||
max={currentYear + 1}
|
||||
required
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="md"
|
||||
onClick={resetForm}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
{/* Tombol Simpan */}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
style={{
|
||||
background: !isFormValid() || isSubmitting
|
||||
? `linear-gradient(135deg, #cccccc, #eeeeee)`
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDataBanjar;
|
||||
304
src/app/admin/(dashboard)/kependudukan/data-banjar/page.tsx
Normal file
304
src/app/admin/(dashboard)/kependudukan/data-banjar/page.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
|
||||
import dataBanjar from '../../_state/kependudukan/data-banjar';
|
||||
|
||||
function DataBanjarAdmin() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Data Banjar'
|
||||
placeholder='Cari nama banjar...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListDataBanjar search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListDataBanjar({ search }: { search: string }) {
|
||||
type DataBanjarType = {
|
||||
id: string;
|
||||
nama: string;
|
||||
penduduk: number;
|
||||
kk: number;
|
||||
miskin: number;
|
||||
tahun: number;
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const stateDataBanjar = useProxy(dataBanjar);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateDataBanjar.findMany;
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
stateDataBanjar.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
}
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={{ base: 1.2, md: 1.15 }}>
|
||||
List Data Banjar
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/kependudukan/data-banjar/create')}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||
<Table
|
||||
highlightOnHover
|
||||
miw={0}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '25%' }}>Nama Banjar</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Penduduk</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>KK</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Miskin</TableTh>
|
||||
<TableTh style={{ width: '10%' }}>Tahun</TableTh>
|
||||
<TableTh style={{ width: '10%' }}>Edit</TableTh>
|
||||
<TableTh style={{ width: '10%' }}>Hapus</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item: DataBanjarType) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.nama}</TableTd>
|
||||
<TableTd>{item.penduduk.toLocaleString('id-ID')}</TableTd>
|
||||
<TableTd>{item.kk.toLocaleString('id-ID')}</TableTd>
|
||||
<TableTd>{item.miskin.toLocaleString('id-ID')}</TableTd>
|
||||
<TableTd>{item.tahun}</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(`/admin/kependudukan/data-banjar/${item.id}`)
|
||||
}
|
||||
fz="sm"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={stateDataBanjar.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
fz="sm"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={7}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data banjar yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card */}
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="xs">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item: DataBanjarType) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="sm">
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nama Banjar
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Jumlah Penduduk
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.penduduk.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
KK
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.kk.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Penduduk Miskin
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.miskin.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Tahun
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.tahun}
|
||||
</Text>
|
||||
</Box>
|
||||
<Group justify="flex-end" gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(`/admin/kependudukan/data-banjar/${item.id}`)
|
||||
}
|
||||
fz="xs"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconEdit size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={stateDataBanjar.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
fz="xs"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data banjar yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleDelete}
|
||||
text="Apakah anda yakin ingin menghapus data banjar ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataBanjarAdmin;
|
||||
@@ -0,0 +1,232 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title,
|
||||
NumberInput,
|
||||
Select
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import distribusiAgama from '../../../_state/kependudukan/distribusi-agama';
|
||||
|
||||
interface FormData {
|
||||
agama: string;
|
||||
jumlah: number;
|
||||
tahun: number;
|
||||
}
|
||||
|
||||
export default function EditDistribusiAgama() {
|
||||
const router = useRouter();
|
||||
const { id } = useParams() as { id: string };
|
||||
const stateDistribusiAgama = useProxy(distribusiAgama);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
agama: '',
|
||||
jumlah: 0,
|
||||
tahun: new Date().getFullYear(),
|
||||
});
|
||||
const [originalData, setOriginalData] = useState<FormData>({
|
||||
agama: '',
|
||||
jumlah: 0,
|
||||
tahun: new Date().getFullYear(),
|
||||
});
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const yearOptions = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: String(currentYear - i),
|
||||
label: String(currentYear - i),
|
||||
}));
|
||||
|
||||
const agamaOptions = [
|
||||
{ value: 'HINDU', label: 'Hindu' },
|
||||
{ value: 'ISLAM', label: 'Islam' },
|
||||
{ value: 'KRISTEN', label: 'Kristen' },
|
||||
{ value: 'KRISTEN_PROTESTAN', label: 'Kristen Protestan' },
|
||||
{ value: 'KRISTEN_KATOLIK', label: 'Kristen Katolik' },
|
||||
{ value: 'BUDDHA', label: 'Buddha' },
|
||||
{ value: 'KONGHUCU', label: 'Konghucu' },
|
||||
{ value: 'LAINNYA', label: 'Lainnya' },
|
||||
];
|
||||
|
||||
const isFormValid = () => {
|
||||
return (
|
||||
formData.agama?.trim() !== '' &&
|
||||
formData.jumlah !== null &&
|
||||
formData.jumlah >= 0 &&
|
||||
formData.tahun !== null
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
stateDistribusiAgama.update.id = id;
|
||||
await stateDistribusiAgama.findUnique.load(id);
|
||||
|
||||
const data = stateDistribusiAgama.findUnique.data;
|
||||
if (data) {
|
||||
setFormData({
|
||||
agama: data.agama ?? '',
|
||||
jumlah: Number(data.jumlah ?? 0),
|
||||
tahun: Number(data.tahun ?? currentYear),
|
||||
});
|
||||
setOriginalData({
|
||||
agama: data.agama ?? '',
|
||||
jumlah: Number(data.jumlah ?? 0),
|
||||
tahun: Number(data.tahun ?? currentYear),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
toast.error('Gagal memuat data');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [id]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(field: keyof FormData) =>
|
||||
(value: any) => {
|
||||
const val =
|
||||
field === 'jumlah' || field === 'tahun'
|
||||
? Number(value || 0)
|
||||
: value;
|
||||
|
||||
setFormData((prev) => ({ ...prev, [field]: val }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleResetForm = () => {
|
||||
setFormData({
|
||||
agama: originalData.agama,
|
||||
jumlah: Number(originalData.jumlah),
|
||||
tahun: Number(originalData.tahun),
|
||||
});
|
||||
toast.info("Form dikembalikan ke data awal");
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
stateDistribusiAgama.update.id = id;
|
||||
stateDistribusiAgama.update.form = { ...formData };
|
||||
|
||||
await stateDistribusiAgama.update.submit();
|
||||
|
||||
toast.success('Data berhasil diperbarui');
|
||||
router.push('/admin/kependudukan/distribusi-agama');
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
toast.error('Gagal memperbarui data');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Distribusi Agama
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form Card */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Agama"
|
||||
placeholder="Pilih agama"
|
||||
data={agamaOptions}
|
||||
value={formData.agama}
|
||||
onChange={handleChange('agama')}
|
||||
required
|
||||
searchable
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Jumlah"
|
||||
placeholder="Masukkan jumlah"
|
||||
value={formData.jumlah}
|
||||
onChange={handleChange('jumlah')}
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Tahun"
|
||||
placeholder="Pilih tahun"
|
||||
data={yearOptions}
|
||||
value={String(formData.tahun)}
|
||||
onChange={handleChange('tahun')}
|
||||
required
|
||||
/>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="md"
|
||||
onClick={handleResetForm}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
|
||||
{/* Tombol Simpan */}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
style={{
|
||||
background: !isFormValid() || isSubmitting
|
||||
? `linear-gradient(135deg, #cccccc, #eeeeee)`
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title,
|
||||
NumberInput,
|
||||
Select
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import distribusiAgama from '../../../_state/kependudukan/distribusi-agama';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
function CreateDistribusiAgama() {
|
||||
const stateDistribusiAgama = useProxy(distribusiAgama);
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const agamaOptions = [
|
||||
{ value: 'HINDU', label: 'Hindu' },
|
||||
{ value: 'ISLAM', label: 'Islam' },
|
||||
{ value: 'KRISTEN', label: 'Kristen' },
|
||||
{ value: 'KRISTEN_PROTESTAN', label: 'Kristen Protestan' },
|
||||
{ value: 'KRISTEN_KATOLIK', label: 'Kristen Katolik' },
|
||||
{ value: 'BUDDHA', label: 'Buddha' },
|
||||
{ value: 'KONGHUCU', label: 'Konghucu' },
|
||||
{ value: 'LAINNYA', label: 'Lainnya' },
|
||||
];
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const yearOptions = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: String(currentYear - i),
|
||||
label: String(currentYear - i),
|
||||
}));
|
||||
|
||||
const isFormValid = () => {
|
||||
return (
|
||||
stateDistribusiAgama.create.form.agama?.trim() !== '' &&
|
||||
stateDistribusiAgama.create.form.jumlah !== null &&
|
||||
stateDistribusiAgama.create.form.jumlah >= 0 &&
|
||||
stateDistribusiAgama.create.form.tahun !== null
|
||||
);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
stateDistribusiAgama.create.form = {
|
||||
agama: '',
|
||||
jumlah: 0,
|
||||
tahun: currentYear,
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const id = await stateDistribusiAgama.create.create();
|
||||
if (id) {
|
||||
resetForm();
|
||||
router.push('/admin/kependudukan/distribusi-agama');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating distribusi agama:', error);
|
||||
toast.error('Terjadi kesalahan saat menambah distribusi agama');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Distribusi Agama
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Agama"
|
||||
placeholder="Pilih agama"
|
||||
data={agamaOptions}
|
||||
value={stateDistribusiAgama.create.form.agama}
|
||||
onChange={(val) => {
|
||||
stateDistribusiAgama.create.form.agama = val || '';
|
||||
}}
|
||||
required
|
||||
searchable
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Jumlah"
|
||||
placeholder="Masukkan jumlah"
|
||||
value={stateDistribusiAgama.create.form.jumlah}
|
||||
onChange={(val) => {
|
||||
stateDistribusiAgama.create.form.jumlah = Number(val || 0);
|
||||
}}
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Tahun"
|
||||
placeholder="Pilih tahun"
|
||||
data={yearOptions}
|
||||
value={String(stateDistribusiAgama.create.form.tahun)}
|
||||
onChange={(val) => {
|
||||
stateDistribusiAgama.create.form.tahun = Number(val || currentYear);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="md"
|
||||
onClick={resetForm}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
{/* Tombol Simpan */}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
style={{
|
||||
background: !isFormValid() || isSubmitting
|
||||
? `linear-gradient(135deg, #cccccc, #eeeeee)`
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDistribusiAgama;
|
||||
283
src/app/admin/(dashboard)/kependudukan/distribusi-agama/page.tsx
Normal file
283
src/app/admin/(dashboard)/kependudukan/distribusi-agama/page.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Flex,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
|
||||
import distribusiAgama from '../../_state/kependudukan/distribusi-agama';
|
||||
|
||||
function DistribusiAgamaAdmin() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Distribusi Agama'
|
||||
placeholder='Cari agama...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListDistribusiAgama search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListDistribusiAgama({ search }: { search: string }) {
|
||||
type DistribusiAgamaType = {
|
||||
id: string;
|
||||
agama: string;
|
||||
jumlah: number;
|
||||
tahun: number;
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const stateDistribusiAgama = useProxy(distribusiAgama);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateDistribusiAgama.findMany;
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
stateDistribusiAgama.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
}
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={{ base: 1.2, md: 1.15 }}>
|
||||
List Distribusi Agama
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/kependudukan/distribusi-agama/create')}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||
<Table
|
||||
highlightOnHover
|
||||
miw={0}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '40%' }}>Agama</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Jumlah</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Tahun</TableTh>
|
||||
<TableTh style={{ width: '10%' }}>Edit</TableTh>
|
||||
<TableTh style={{ width: '10%' }}>Hapus</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item: DistribusiAgamaType) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.agama}</TableTd>
|
||||
<TableTd>{item.jumlah.toLocaleString('id-ID')}</TableTd>
|
||||
<TableTd>{item.tahun}</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(`/admin/kependudukan/distribusi-agama/${item.id}`)
|
||||
}
|
||||
fz="sm"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={stateDistribusiAgama.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
fz="sm"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={5}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data distribusi agama yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card */}
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="xs">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item: DistribusiAgamaType) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="sm">
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Agama
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.agama}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Jumlah
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.jumlah.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Tahun
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.tahun}
|
||||
</Text>
|
||||
</Box>
|
||||
<Group justify="flex-end" gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(`/admin/kependudukan/distribusi-agama/${item.id}`)
|
||||
}
|
||||
fz="xs"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconEdit size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={stateDistribusiAgama.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
fz="xs"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data distribusi agama yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleDelete}
|
||||
text="Apakah anda yakin ingin menghapus data distribusi agama ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DistribusiAgamaAdmin;
|
||||
@@ -0,0 +1,232 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Title,
|
||||
NumberInput,
|
||||
Select
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import distribusiUmur from '../../../_state/kependudukan/distribusi-umur';
|
||||
|
||||
interface FormData {
|
||||
rentangUmur: string;
|
||||
jumlah: number;
|
||||
tahun: number;
|
||||
}
|
||||
|
||||
export default function EditDistribusiUmur() {
|
||||
const router = useRouter();
|
||||
const { id } = useParams() as { id: string };
|
||||
const stateDistribusiUmur = useProxy(distribusiUmur);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
rentangUmur: '',
|
||||
jumlah: 0,
|
||||
tahun: new Date().getFullYear(),
|
||||
});
|
||||
const [originalData, setOriginalData] = useState<FormData>({
|
||||
rentangUmur: '',
|
||||
jumlah: 0,
|
||||
tahun: new Date().getFullYear(),
|
||||
});
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const yearOptions = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: String(currentYear - i),
|
||||
label: String(currentYear - i),
|
||||
}));
|
||||
|
||||
const rentangUmurOptions = [
|
||||
{ value: '0-5', label: '0-5 Tahun' },
|
||||
{ value: '6-12', label: '6-12 Tahun' },
|
||||
{ value: '13-17', label: '13-17 Tahun' },
|
||||
{ value: '18-25', label: '18-25 Tahun' },
|
||||
{ value: '26-35', label: '26-35 Tahun' },
|
||||
{ value: '36-45', label: '36-45 Tahun' },
|
||||
{ value: '46-55', label: '46-55 Tahun' },
|
||||
{ value: '56-65', label: '56-65 Tahun' },
|
||||
{ value: '65+', label: '65+ Tahun' },
|
||||
];
|
||||
|
||||
const isFormValid = () => {
|
||||
return (
|
||||
formData.rentangUmur?.trim() !== '' &&
|
||||
formData.jumlah !== null &&
|
||||
formData.jumlah >= 0 &&
|
||||
formData.tahun !== null
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
stateDistribusiUmur.update.id = id;
|
||||
await stateDistribusiUmur.findUnique.load(id);
|
||||
|
||||
const data = stateDistribusiUmur.findUnique.data;
|
||||
if (data) {
|
||||
setFormData({
|
||||
rentangUmur: data.rentangUmur ?? '',
|
||||
jumlah: Number(data.jumlah ?? 0),
|
||||
tahun: Number(data.tahun ?? currentYear),
|
||||
});
|
||||
setOriginalData({
|
||||
rentangUmur: data.rentangUmur ?? '',
|
||||
jumlah: Number(data.jumlah ?? 0),
|
||||
tahun: Number(data.tahun ?? currentYear),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
toast.error('Gagal memuat data');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [id]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(field: keyof FormData) =>
|
||||
(value: any) => {
|
||||
const val =
|
||||
field === 'jumlah' || field === 'tahun'
|
||||
? Number(value || 0)
|
||||
: value;
|
||||
|
||||
setFormData((prev) => ({ ...prev, [field]: val }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleResetForm = () => {
|
||||
setFormData({
|
||||
rentangUmur: originalData.rentangUmur,
|
||||
jumlah: Number(originalData.jumlah),
|
||||
tahun: Number(originalData.tahun),
|
||||
});
|
||||
toast.info("Form dikembalikan ke data awal");
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
stateDistribusiUmur.update.id = id;
|
||||
stateDistribusiUmur.update.form = { ...formData };
|
||||
|
||||
await stateDistribusiUmur.update.submit();
|
||||
|
||||
toast.success('Data berhasil diperbarui');
|
||||
router.push('/admin/kependudukan/distribusi-umur');
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
toast.error('Gagal memperbarui data');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Distribusi Umur
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form Card */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Rentang Umur"
|
||||
placeholder="Pilih rentang umur"
|
||||
data={rentangUmurOptions}
|
||||
value={formData.rentangUmur}
|
||||
onChange={handleChange('rentangUmur')}
|
||||
required
|
||||
searchable
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Jumlah"
|
||||
placeholder="Masukkan jumlah"
|
||||
value={formData.jumlah}
|
||||
onChange={handleChange('jumlah')}
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Tahun"
|
||||
placeholder="Pilih tahun"
|
||||
data={yearOptions}
|
||||
value={String(formData.tahun)}
|
||||
onChange={handleChange('tahun')}
|
||||
required
|
||||
/>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="md"
|
||||
onClick={handleResetForm}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
|
||||
{/* Tombol Simpan */}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
style={{
|
||||
background: !isFormValid() || isSubmitting
|
||||
? `linear-gradient(135deg, #cccccc, #eeeeee)`
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Title,
|
||||
NumberInput,
|
||||
Select
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import distribusiUmur from '../../../_state/kependudukan/distribusi-umur';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
function CreateDistribusiUmur() {
|
||||
const stateDistribusiUmur = useProxy(distribusiUmur);
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const rentangUmurOptions = [
|
||||
{ value: '0-5', label: '0-5 Tahun' },
|
||||
{ value: '6-12', label: '6-12 Tahun' },
|
||||
{ value: '13-17', label: '13-17 Tahun' },
|
||||
{ value: '18-25', label: '18-25 Tahun' },
|
||||
{ value: '26-35', label: '26-35 Tahun' },
|
||||
{ value: '36-45', label: '36-45 Tahun' },
|
||||
{ value: '46-55', label: '46-55 Tahun' },
|
||||
{ value: '56-65', label: '56-65 Tahun' },
|
||||
{ value: '65+', label: '65+ Tahun' },
|
||||
];
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const yearOptions = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: String(currentYear - i),
|
||||
label: String(currentYear - i),
|
||||
}));
|
||||
|
||||
const isFormValid = () => {
|
||||
return (
|
||||
stateDistribusiUmur.create.form.rentangUmur?.trim() !== '' &&
|
||||
stateDistribusiUmur.create.form.jumlah !== null &&
|
||||
stateDistribusiUmur.create.form.jumlah >= 0 &&
|
||||
stateDistribusiUmur.create.form.tahun !== null
|
||||
);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
stateDistribusiUmur.create.form = {
|
||||
rentangUmur: '',
|
||||
jumlah: 0,
|
||||
tahun: currentYear,
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const id = await stateDistribusiUmur.create.create();
|
||||
if (id) {
|
||||
resetForm();
|
||||
router.push('/admin/kependudukan/distribusi-umur');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating distribusi umur:', error);
|
||||
toast.error('Terjadi kesalahan saat menambah distribusi umur');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Distribusi Umur
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Rentang Umur"
|
||||
placeholder="Pilih rentang umur"
|
||||
data={rentangUmurOptions}
|
||||
value={stateDistribusiUmur.create.form.rentangUmur}
|
||||
onChange={(val) => {
|
||||
stateDistribusiUmur.create.form.rentangUmur = val || '';
|
||||
}}
|
||||
required
|
||||
searchable
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Jumlah"
|
||||
placeholder="Masukkan jumlah"
|
||||
value={stateDistribusiUmur.create.form.jumlah}
|
||||
onChange={(val) => {
|
||||
stateDistribusiUmur.create.form.jumlah = Number(val || 0);
|
||||
}}
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Tahun"
|
||||
placeholder="Pilih tahun"
|
||||
data={yearOptions}
|
||||
value={String(stateDistribusiUmur.create.form.tahun)}
|
||||
onChange={(val) => {
|
||||
stateDistribusiUmur.create.form.tahun = Number(val || currentYear);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="md"
|
||||
onClick={resetForm}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
{/* Tombol Simpan */}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
style={{
|
||||
background: !isFormValid() || isSubmitting
|
||||
? `linear-gradient(135deg, #cccccc, #eeeeee)`
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDistribusiUmur;
|
||||
284
src/app/admin/(dashboard)/kependudukan/distribusi-umur/page.tsx
Normal file
284
src/app/admin/(dashboard)/kependudukan/distribusi-umur/page.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Flex,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
|
||||
import distribusiUmur from '../../_state/kependudukan/distribusi-umur';
|
||||
|
||||
function DistribusiUmurAdmin() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Distribusi Umur'
|
||||
placeholder='Cari rentang umur...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListDistribusiUmur search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListDistribusiUmur({ search }: { search: string }) {
|
||||
type DistribusiUmurType = {
|
||||
id: string;
|
||||
rentangUmur: string;
|
||||
jumlah: number;
|
||||
tahun: number;
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const stateDistribusiUmur = useProxy(distribusiUmur);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateDistribusiUmur.findMany;
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
stateDistribusiUmur.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
}
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={{ base: 1.2, md: 1.15 }}>
|
||||
List Distribusi Umur
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/kependudukan/distribusi-umur/create')}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||
<Table
|
||||
highlightOnHover
|
||||
miw={0}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '40%' }}>Rentang Umur</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Jumlah</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Tahun</TableTh>
|
||||
<TableTh style={{ width: '10%' }}>Edit</TableTh>
|
||||
<TableTh style={{ width: '10%' }}>Hapus</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item: DistribusiUmurType) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.rentangUmur}</TableTd>
|
||||
<TableTd>{item.jumlah.toLocaleString('id-ID')}</TableTd>
|
||||
<TableTd>{item.tahun}</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(`/admin/kependudukan/distribusi-umur/${item.id}`)
|
||||
}
|
||||
fz="sm"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={stateDistribusiUmur.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
fz="sm"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={5}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data distribusi umur yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card */}
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="xs">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item: DistribusiUmurType) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="sm">
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Rentang Umur
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.rentangUmur}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Jumlah
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.jumlah.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Tahun
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.tahun}
|
||||
</Text>
|
||||
</Box>
|
||||
<Group justify="flex-end" gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(`/admin/kependudukan/distribusi-umur/${item.id}`)
|
||||
}
|
||||
fz="xs"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconEdit size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={stateDistribusiUmur.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
fz="xs"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data distribusi umur yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleDelete}
|
||||
text="Apakah anda yakin ingin menghapus data distribusi umur ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DistribusiUmurAdmin;
|
||||
@@ -0,0 +1,267 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Title,
|
||||
TextInput,
|
||||
Select,
|
||||
Textarea
|
||||
} from '@mantine/core';
|
||||
import { DatePickerInput } from '@mantine/dates';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import migrasiPenduduk from '../../../_state/kependudukan/migrasi-penduduk';
|
||||
|
||||
interface FormData {
|
||||
jenis: string;
|
||||
nama: string;
|
||||
tanggal: string;
|
||||
asalTujuan: string;
|
||||
alasan: string;
|
||||
jenisKelamin: string;
|
||||
}
|
||||
|
||||
export default function EditMigrasiPenduduk() {
|
||||
const router = useRouter();
|
||||
const { id } = useParams() as { id: string };
|
||||
const stateMigrasiPenduduk = useProxy(migrasiPenduduk);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
jenis: '',
|
||||
nama: '',
|
||||
tanggal: '',
|
||||
asalTujuan: '',
|
||||
alasan: '',
|
||||
jenisKelamin: '',
|
||||
});
|
||||
const [originalData, setOriginalData] = useState<FormData>({
|
||||
jenis: '',
|
||||
nama: '',
|
||||
tanggal: '',
|
||||
asalTujuan: '',
|
||||
alasan: '',
|
||||
jenisKelamin: '',
|
||||
});
|
||||
|
||||
const jenisOptions = [
|
||||
{ value: 'MASUK', label: 'Masuk' },
|
||||
{ value: 'KELUAR', label: 'Keluar' },
|
||||
];
|
||||
|
||||
const jenisKelaminOptions = [
|
||||
{ value: 'L', label: 'Laki-laki' },
|
||||
{ value: 'P', label: 'Perempuan' },
|
||||
];
|
||||
|
||||
const isFormValid = () => {
|
||||
return (
|
||||
formData.jenis?.trim() !== '' &&
|
||||
formData.nama?.trim() !== '' &&
|
||||
formData.tanggal?.trim() !== '' &&
|
||||
formData.asalTujuan?.trim() !== ''
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
stateMigrasiPenduduk.update.id = id;
|
||||
await stateMigrasiPenduduk.findUnique.load(id);
|
||||
|
||||
const data = stateMigrasiPenduduk.findUnique.data;
|
||||
if (data) {
|
||||
setFormData({
|
||||
jenis: data.jenis ?? '',
|
||||
nama: data.nama ?? '',
|
||||
tanggal: data.tanggal ?? '',
|
||||
asalTujuan: data.asalTujuan ?? '',
|
||||
alasan: data.alasan ?? '',
|
||||
jenisKelamin: data.jenisKelamin ?? '',
|
||||
});
|
||||
setOriginalData({
|
||||
jenis: data.jenis ?? '',
|
||||
nama: data.nama ?? '',
|
||||
tanggal: data.tanggal ?? '',
|
||||
asalTujuan: data.asalTujuan ?? '',
|
||||
alasan: data.alasan ?? '',
|
||||
jenisKelamin: data.jenisKelamin ?? '',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
toast.error('Gagal memuat data');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [id]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(field: keyof FormData) =>
|
||||
(value: any) => {
|
||||
const val = value || '';
|
||||
setFormData((prev) => ({ ...prev, [field]: val }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleResetForm = () => {
|
||||
setFormData({
|
||||
jenis: originalData.jenis,
|
||||
nama: originalData.nama,
|
||||
tanggal: originalData.tanggal,
|
||||
asalTujuan: originalData.asalTujuan,
|
||||
alasan: originalData.alasan,
|
||||
jenisKelamin: originalData.jenisKelamin,
|
||||
});
|
||||
toast.info("Form dikembalikan ke data awal");
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
stateMigrasiPenduduk.update.id = id;
|
||||
stateMigrasiPenduduk.update.form = { ...formData };
|
||||
|
||||
await stateMigrasiPenduduk.update.submit();
|
||||
|
||||
toast.success('Data berhasil diperbarui');
|
||||
router.push('/admin/kependudukan/migrasi-penduduk');
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
toast.error('Gagal memperbarui data');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Migrasi Penduduk
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form Card */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Jenis Migrasi"
|
||||
placeholder="Pilih jenis migrasi"
|
||||
data={jenisOptions}
|
||||
value={formData.jenis}
|
||||
onChange={handleChange('jenis')}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Nama"
|
||||
placeholder="Masukkan nama lengkap"
|
||||
value={formData.nama}
|
||||
onChange={handleChange('nama')}
|
||||
required
|
||||
/>
|
||||
|
||||
<DatePickerInput
|
||||
label="Tanggal"
|
||||
placeholder="Pilih tanggal"
|
||||
value={formData.tanggal ? new Date(formData.tanggal) : null}
|
||||
onChange={(val: string | null) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
tanggal: val || '',
|
||||
}));
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label={formData.jenis === 'MASUK' ? 'Asal' : 'Tujuan'}
|
||||
placeholder={formData.jenis === 'MASUK' ? 'Masukkan asal' : 'Masukkan tujuan'}
|
||||
value={formData.asalTujuan}
|
||||
onChange={handleChange('asalTujuan')}
|
||||
required
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Alasan"
|
||||
placeholder="Masukkan alasan (opsional)"
|
||||
value={formData.alasan}
|
||||
onChange={handleChange('alasan')}
|
||||
autosize
|
||||
minRows={2}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Jenis Kelamin"
|
||||
placeholder="Pilih jenis kelamin"
|
||||
data={jenisKelaminOptions}
|
||||
value={formData.jenisKelamin}
|
||||
onChange={handleChange('jenisKelamin')}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="md"
|
||||
onClick={handleResetForm}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
|
||||
{/* Tombol Simpan */}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
style={{
|
||||
background: !isFormValid() || isSubmitting
|
||||
? `linear-gradient(135deg, #cccccc, #eeeeee)`
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Title,
|
||||
TextInput,
|
||||
Select,
|
||||
Textarea
|
||||
} from '@mantine/core';
|
||||
import { DatePickerInput } from '@mantine/dates';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import migrasiPenduduk from '../../../_state/kependudukan/migrasi-penduduk';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
function CreateMigrasiPenduduk() {
|
||||
const stateMigrasiPenduduk = useProxy(migrasiPenduduk);
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const jenisOptions = [
|
||||
{ value: 'MASUK', label: 'Masuk' },
|
||||
{ value: 'KELUAR', label: 'Keluar' },
|
||||
];
|
||||
|
||||
const jenisKelaminOptions = [
|
||||
{ value: 'L', label: 'Laki-laki' },
|
||||
{ value: 'P', label: 'Perempuan' },
|
||||
];
|
||||
|
||||
const isFormValid = () => {
|
||||
return (
|
||||
stateMigrasiPenduduk.create.form.jenis?.trim() !== '' &&
|
||||
stateMigrasiPenduduk.create.form.nama?.trim() !== '' &&
|
||||
stateMigrasiPenduduk.create.form.tanggal?.trim() !== '' &&
|
||||
stateMigrasiPenduduk.create.form.asalTujuan?.trim() !== ''
|
||||
);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
stateMigrasiPenduduk.create.form = {
|
||||
jenis: '',
|
||||
nama: '',
|
||||
tanggal: '',
|
||||
asalTujuan: '',
|
||||
alasan: '',
|
||||
jenisKelamin: '',
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const id = await stateMigrasiPenduduk.create.create();
|
||||
if (id) {
|
||||
resetForm();
|
||||
router.push('/admin/kependudukan/migrasi-penduduk');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating migrasi penduduk:', error);
|
||||
toast.error('Terjadi kesalahan saat menambah data migrasi penduduk');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Migrasi Penduduk
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Jenis Migrasi"
|
||||
placeholder="Pilih jenis migrasi"
|
||||
data={jenisOptions}
|
||||
value={stateMigrasiPenduduk.create.form.jenis}
|
||||
onChange={(val) => {
|
||||
stateMigrasiPenduduk.create.form.jenis = val || '';
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Nama"
|
||||
placeholder="Masukkan nama lengkap"
|
||||
value={stateMigrasiPenduduk.create.form.nama}
|
||||
onChange={(e) => {
|
||||
stateMigrasiPenduduk.create.form.nama = e.currentTarget.value;
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<DatePickerInput
|
||||
label="Tanggal"
|
||||
placeholder="Pilih tanggal"
|
||||
value={stateMigrasiPenduduk.create.form.tanggal ? new Date(stateMigrasiPenduduk.create.form.tanggal) : null}
|
||||
onChange={(val: string | null) => {
|
||||
stateMigrasiPenduduk.create.form.tanggal = val || '';
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label={stateMigrasiPenduduk.create.form.jenis === 'MASUK' ? 'Asal' : 'Tujuan'}
|
||||
placeholder={stateMigrasiPenduduk.create.form.jenis === 'MASUK' ? 'Masukkan asal' : 'Masukkan tujuan'}
|
||||
value={stateMigrasiPenduduk.create.form.asalTujuan}
|
||||
onChange={(e) => {
|
||||
stateMigrasiPenduduk.create.form.asalTujuan = e.currentTarget.value;
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Alasan"
|
||||
placeholder="Masukkan alasan (opsional)"
|
||||
value={stateMigrasiPenduduk.create.form.alasan}
|
||||
onChange={(e) => {
|
||||
stateMigrasiPenduduk.create.form.alasan = e.currentTarget.value;
|
||||
}}
|
||||
autosize
|
||||
minRows={2}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Jenis Kelamin"
|
||||
placeholder="Pilih jenis kelamin"
|
||||
data={jenisKelaminOptions}
|
||||
value={stateMigrasiPenduduk.create.form.jenisKelamin}
|
||||
onChange={(val) => {
|
||||
stateMigrasiPenduduk.create.form.jenisKelamin = val || '';
|
||||
}}
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="md"
|
||||
onClick={resetForm}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
{/* Tombol Simpan */}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
style={{
|
||||
background: !isFormValid() || isSubmitting
|
||||
? `linear-gradient(135deg, #cccccc, #eeeeee)`
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateMigrasiPenduduk;
|
||||
339
src/app/admin/(dashboard)/kependudukan/migrasi-penduduk/page.tsx
Normal file
339
src/app/admin/(dashboard)/kependudukan/migrasi-penduduk/page.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
|
||||
import migrasiPenduduk from '../../_state/kependudukan/migrasi-penduduk';
|
||||
|
||||
function MigrasiPendudukAdmin() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Migrasi Penduduk'
|
||||
placeholder='Cari nama...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListMigrasiPenduduk search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListMigrasiPenduduk({ search }: { search: string }) {
|
||||
type MigrasiPendudukType = {
|
||||
id: string;
|
||||
jenis: string;
|
||||
nama: string;
|
||||
tanggal: string;
|
||||
asalTujuan: string;
|
||||
alasan: string | null;
|
||||
jenisKelamin: string | null;
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const stateMigrasiPenduduk = useProxy(migrasiPenduduk);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateMigrasiPenduduk.findMany;
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
stateMigrasiPenduduk.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
}
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const formatTanggal = (tanggal: string) => {
|
||||
try {
|
||||
return new Date(tanggal).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
} catch {
|
||||
return tanggal;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box py={{ base: 'sm', md: 'md' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||
<Title order={4} lh={{ base: 1.2, md: 1.15 }}>
|
||||
List Migrasi Penduduk
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/kependudukan/migrasi-penduduk/create')}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||
<Table
|
||||
highlightOnHover
|
||||
miw={0}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '10%' }}>Jenis</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '12%' }}>Tanggal</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Asal/Tujuan</TableTh>
|
||||
<TableTh style={{ width: '10%' }}>L/P</TableTh>
|
||||
<TableTh style={{ width: '10%' }}>Edit</TableTh>
|
||||
<TableTh style={{ width: '10%' }}>Hapus</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item: MigrasiPendudukType) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text
|
||||
fz="sm"
|
||||
fw={500}
|
||||
c={item.jenis === 'MASUK' ? 'green' : 'red'}
|
||||
>
|
||||
{item.jenis}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>{item.nama}</TableTd>
|
||||
<TableTd>{formatTanggal(item.tanggal)}</TableTd>
|
||||
<TableTd>{item.asalTujuan}</TableTd>
|
||||
<TableTd>{item.jenisKelamin || '-'}</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(`/admin/kependudukan/migrasi-penduduk/${item.id}`)
|
||||
}
|
||||
fz="sm"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={stateMigrasiPenduduk.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
fz="sm"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={7}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data migrasi penduduk yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Card */}
|
||||
<Box hiddenFrom="md">
|
||||
<Stack gap="xs">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item: MigrasiPendudukType) => (
|
||||
<Paper key={item.id} withBorder p="sm" radius="sm">
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Jenis Migrasi
|
||||
</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
fw={500}
|
||||
c={item.jenis === 'MASUK' ? 'green' : 'red'}
|
||||
>
|
||||
{item.jenis}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nama
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Tanggal
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{formatTanggal(item.tanggal)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Asal/Tujuan
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.asalTujuan}
|
||||
</Text>
|
||||
</Box>
|
||||
{item.alasan && (
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Alasan
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.alasan}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Jenis Kelamin
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.4}>
|
||||
{item.jenisKelamin || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Group justify="flex-end" gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(`/admin/kependudukan/migrasi-penduduk/${item.id}`)
|
||||
}
|
||||
fz="xs"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconEdit size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={stateMigrasiPenduduk.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
fz="xs"
|
||||
px="xs"
|
||||
py="xs"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data migrasi penduduk yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleDelete}
|
||||
text="Apakah anda yakin ingin menghapus data migrasi penduduk ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default MigrasiPendudukAdmin;
|
||||
@@ -373,6 +373,33 @@ export const devBar = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "Kependudukan",
|
||||
name: "Kependudukan",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "Kependudukan_1",
|
||||
name: "Distribusi Agama",
|
||||
path: "/admin/kependudukan/distribusi-agama"
|
||||
},
|
||||
{
|
||||
id: "Kependudukan_2",
|
||||
name: "Distribusi Umur",
|
||||
path: "/admin/kependudukan/distribusi-umur"
|
||||
},
|
||||
{
|
||||
id: "Kependudukan_3",
|
||||
name: "Data Banjar",
|
||||
path: "/admin/kependudukan/data-banjar"
|
||||
},
|
||||
{
|
||||
id: "Kependudukan_4",
|
||||
name: "Migrasi Penduduk",
|
||||
path: "/admin/kependudukan/migrasi-penduduk"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "Musik",
|
||||
name: "Musik",
|
||||
@@ -777,6 +804,33 @@ export const navBar = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "Kependudukan",
|
||||
name: "Kependudukan",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "Kependudukan_1",
|
||||
name: "Distribusi Agama",
|
||||
path: "/admin/kependudukan/distribusi-agama"
|
||||
},
|
||||
{
|
||||
id: "Kependudukan_2",
|
||||
name: "Distribusi Umur",
|
||||
path: "/admin/kependudukan/distribusi-umur"
|
||||
},
|
||||
{
|
||||
id: "Kependudukan_3",
|
||||
name: "Data Banjar",
|
||||
path: "/admin/kependudukan/data-banjar"
|
||||
},
|
||||
{
|
||||
id: "Kependudukan_4",
|
||||
name: "Migrasi Penduduk",
|
||||
path: "/admin/kependudukan/migrasi-penduduk"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "Musik",
|
||||
name: "Musik",
|
||||
@@ -1098,6 +1152,33 @@ export const role1 = [
|
||||
path: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "Kependudukan",
|
||||
name: "Kependudukan",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "Kependudukan_1",
|
||||
name: "Distribusi Agama",
|
||||
path: "/admin/kependudukan/distribusi-agama"
|
||||
},
|
||||
{
|
||||
id: "Kependudukan_2",
|
||||
name: "Distribusi Umur",
|
||||
path: "/admin/kependudukan/distribusi-umur"
|
||||
},
|
||||
{
|
||||
id: "Kependudukan_3",
|
||||
name: "Data Banjar",
|
||||
path: "/admin/kependudukan/data-banjar"
|
||||
},
|
||||
{
|
||||
id: "Kependudukan_4",
|
||||
name: "Migrasi Penduduk",
|
||||
path: "/admin/kependudukan/migrasi-penduduk"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "Musik",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import Elysia from "elysia";
|
||||
import dashboardSummary from "./summary";
|
||||
|
||||
const DashboardKependudukan = new Elysia({
|
||||
prefix: "/dashboard",
|
||||
tags: ["Kependudukan/Dashboard"],
|
||||
})
|
||||
.get("/summary", dashboardSummary)
|
||||
|
||||
export default DashboardKependudukan;
|
||||
147
src/app/api/[[...slugs]]/_lib/kependudukan/dashboard/summary.ts
Normal file
147
src/app/api/[[...slugs]]/_lib/kependudukan/dashboard/summary.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function dashboardSummary() {
|
||||
try {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
// Get dashboard summary
|
||||
const [
|
||||
totalPenduduk,
|
||||
totalKK,
|
||||
totalKelahiran,
|
||||
totalKemiskinan,
|
||||
kelahiranData,
|
||||
kematianData,
|
||||
pindahMasukData,
|
||||
pindahKeluarData,
|
||||
agamaData,
|
||||
umurData,
|
||||
banjarData
|
||||
] = await Promise.all([
|
||||
// Total penduduk - hitung dari data banjar
|
||||
prisma.dataBanjar.aggregate({
|
||||
where: { isActive: true, tahun: currentYear },
|
||||
_sum: { penduduk: true }
|
||||
}),
|
||||
|
||||
// Total KK
|
||||
prisma.dataBanjar.aggregate({
|
||||
where: { isActive: true, tahun: currentYear },
|
||||
_sum: { kk: true }
|
||||
}),
|
||||
|
||||
// Total kelahiran tahun ini
|
||||
prisma.kelahiran.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
tanggal: {
|
||||
gte: new Date(`${currentYear}-01-01`),
|
||||
lte: new Date(`${currentYear}-12-31`),
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
// Total penduduk miskin
|
||||
prisma.dataBanjar.aggregate({
|
||||
where: { isActive: true, tahun: currentYear },
|
||||
_sum: { miskin: true }
|
||||
}),
|
||||
|
||||
// Kelahiran data
|
||||
prisma.kelahiran.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
tanggal: {
|
||||
gte: new Date(`${currentYear}-01-01`),
|
||||
lte: new Date(`${currentYear}-12-31`),
|
||||
}
|
||||
},
|
||||
orderBy: { tanggal: 'asc' }
|
||||
}),
|
||||
|
||||
// Kematian data
|
||||
prisma.kematian.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
tanggal: {
|
||||
gte: new Date(`${currentYear}-01-01`),
|
||||
lte: new Date(`${currentYear}-12-31`),
|
||||
}
|
||||
},
|
||||
orderBy: { tanggal: 'asc' }
|
||||
}),
|
||||
|
||||
// Pindah masuk
|
||||
prisma.migrasiPenduduk.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
jenis: 'MASUK',
|
||||
tanggal: {
|
||||
gte: new Date(`${currentYear}-01-01`),
|
||||
lte: new Date(`${currentYear}-12-31`),
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
// Pindah keluar
|
||||
prisma.migrasiPenduduk.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
jenis: 'KELUAR',
|
||||
tanggal: {
|
||||
gte: new Date(`${currentYear}-01-01`),
|
||||
lte: new Date(`${currentYear}-12-31`),
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
// Data agama
|
||||
prisma.distribusiAgama.findMany({
|
||||
where: { isActive: true, tahun: currentYear },
|
||||
orderBy: { jumlah: 'desc' }
|
||||
}),
|
||||
|
||||
// Data umur
|
||||
prisma.distribusiUmur.findMany({
|
||||
where: { isActive: true, tahun: currentYear },
|
||||
orderBy: { createdAt: 'asc' }
|
||||
}),
|
||||
|
||||
// Data banjar
|
||||
prisma.dataBanjar.findMany({
|
||||
where: { isActive: true, tahun: currentYear },
|
||||
orderBy: { nama: 'asc' }
|
||||
})
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Dashboard summary berhasil diambil",
|
||||
data: {
|
||||
tahun: currentYear,
|
||||
summary: {
|
||||
totalPenduduk: totalPenduduk._sum.penduduk || 0,
|
||||
totalKK: totalKK._sum.kk || 0,
|
||||
totalKelahiran: totalKelahiran,
|
||||
totalKemiskinan: totalKemiskinan._sum.miskin || 0,
|
||||
},
|
||||
dinamika: {
|
||||
kelahiran: totalKelahiran,
|
||||
kematian: kematianData.length,
|
||||
pindahMasuk: pindahMasukData,
|
||||
pindahKeluar: pindahKeluarData,
|
||||
},
|
||||
agama: agamaData,
|
||||
umur: umurData,
|
||||
banjar: banjarData,
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching dashboard summary:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Terjadi kesalahan saat mengambil data dashboard",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = Prisma.DataBanjarGetPayload<{
|
||||
select: {
|
||||
nama: true;
|
||||
penduduk: true;
|
||||
kk: true;
|
||||
miskin: true;
|
||||
tahun: true;
|
||||
}
|
||||
}>
|
||||
|
||||
export default async function dataBanjarCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
const created = await prisma.dataBanjar.create({
|
||||
data: {
|
||||
nama: body.nama,
|
||||
penduduk: body.penduduk,
|
||||
kk: body.kk,
|
||||
miskin: body.miskin,
|
||||
tahun: body.tahun,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
nama: true,
|
||||
penduduk: true,
|
||||
kk: true,
|
||||
miskin: true,
|
||||
tahun: true,
|
||||
}
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Sukses menambahkan data banjar",
|
||||
data: created,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function dataBanjarDelete(context: Context) {
|
||||
const id = context.params?.id;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await prisma.dataBanjar.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const deleted = await prisma.dataBanjar.delete({
|
||||
where: { id },
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Data berhasil dihapus",
|
||||
data: deleted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function dataBanjarFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 100;
|
||||
const search = (context.query.search as string) || '';
|
||||
const tahun = Number(context.query.tahun) || new Date().getFullYear();
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = { isActive: true, tahun };
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.dataBanjar.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { nama: "asc" },
|
||||
}),
|
||||
prisma.dataBanjar.count({
|
||||
where,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch data banjar with pagination",
|
||||
data,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to fetch data banjar with pagination",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function dataBanjarFindUnique(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const pathSegments = url.pathname.split('/');
|
||||
const id = pathSegments[pathSegments.length - 1];
|
||||
|
||||
if (!id) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "ID tidak boleh kosong",
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof id !== 'string') {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "ID tidak valid",
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const data = await prisma.dataBanjar.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: "Data ditemukan",
|
||||
data: data,
|
||||
}, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "Terjadi kesalahan saat mengambil data",
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import dataBanjarFindUnique from "./findUnique";
|
||||
import dataBanjarUpdate from "./updt";
|
||||
import dataBanjarFindMany from "./findMany";
|
||||
import dataBanjarCreate from "./create";
|
||||
import dataBanjarDelete from "./del";
|
||||
|
||||
const DataBanjar = new Elysia({
|
||||
prefix: "/databanjar",
|
||||
tags: ["Kependudukan/Data Banjar"],
|
||||
})
|
||||
.get("/:id", async (context) => {
|
||||
const response = await dataBanjarFindUnique(new Request(context.request))
|
||||
return response
|
||||
})
|
||||
.get("/find-many", dataBanjarFindMany)
|
||||
.post("/create", dataBanjarCreate, {
|
||||
body: t.Object({
|
||||
nama: t.String(),
|
||||
penduduk: t.Number(),
|
||||
kk: t.Number(),
|
||||
miskin: t.Number(),
|
||||
tahun: t.Number(),
|
||||
}),
|
||||
})
|
||||
.put("/:id", dataBanjarUpdate, {
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
body: t.Object({
|
||||
nama: t.String(),
|
||||
penduduk: t.Number(),
|
||||
kk: t.Number(),
|
||||
miskin: t.Number(),
|
||||
tahun: t.Number(),
|
||||
}),
|
||||
})
|
||||
.delete("/del/:id", dataBanjarDelete, {
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
})
|
||||
export default DataBanjar;
|
||||
@@ -0,0 +1,51 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function dataBanjarUpdate(context: Context) {
|
||||
const id = context.params?.id;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const {nama, penduduk, kk, miskin, tahun} = context.body as {
|
||||
nama: string;
|
||||
penduduk: number;
|
||||
kk: number;
|
||||
miskin: number;
|
||||
tahun: number;
|
||||
}
|
||||
|
||||
const existing = await prisma.dataBanjar.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await prisma.dataBanjar.update({
|
||||
where: { id },
|
||||
data: {
|
||||
nama,
|
||||
penduduk,
|
||||
kk,
|
||||
miskin,
|
||||
tahun,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Data berhasil diupdate",
|
||||
data: updated,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = Prisma.DistribusiAgamaGetPayload<{
|
||||
select: {
|
||||
agama: true;
|
||||
jumlah: true;
|
||||
tahun: true;
|
||||
}
|
||||
}>
|
||||
|
||||
export default async function distribusiAgamaCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
const created = await prisma.distribusiAgama.create({
|
||||
data: {
|
||||
agama: body.agama,
|
||||
jumlah: body.jumlah,
|
||||
tahun: body.tahun,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
agama: true,
|
||||
jumlah: true,
|
||||
tahun: true,
|
||||
}
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Sukses menambahkan distribusi agama",
|
||||
data: created,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function distribusiAgamaDelete(context: Context) {
|
||||
const id = context.params?.id;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await prisma.distribusiAgama.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const deleted = await prisma.distribusiAgama.delete({
|
||||
where: { id },
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Data berhasil dihapus",
|
||||
data: deleted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function distribusiAgamaFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 100;
|
||||
const search = (context.query.search as string) || '';
|
||||
const tahun = Number(context.query.tahun) || new Date().getFullYear();
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = { isActive: true, tahun };
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ agama: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.distribusiAgama.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { jumlah: "desc" },
|
||||
}),
|
||||
prisma.distribusiAgama.count({
|
||||
where,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch distribusi agama with pagination",
|
||||
data,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to fetch distribusi agama with pagination",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function distribusiAgamaFindUnique(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const pathSegments = url.pathname.split('/');
|
||||
const id = pathSegments[pathSegments.length - 1];
|
||||
|
||||
if (!id) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "ID tidak boleh kosong",
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof id !== 'string') {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "ID tidak valid",
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const data = await prisma.distribusiAgama.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: "Data ditemukan",
|
||||
data: data,
|
||||
}, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "Terjadi kesalahan saat mengambil data",
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import distribusiAgamaFindUnique from "./findUnique";
|
||||
import distribusiAgamaUpdate from "./updt";
|
||||
import distribusiAgamaFindMany from "./findMany";
|
||||
import distribusiAgamaCreate from "./create";
|
||||
import distribusiAgamaDelete from "./del";
|
||||
|
||||
const DistribusiAgama = new Elysia({
|
||||
prefix: "/distribusiagama",
|
||||
tags: ["Kependudukan/Distribusi Agama"],
|
||||
})
|
||||
.get("/:id", async (context) => {
|
||||
const response = await distribusiAgamaFindUnique(new Request(context.request))
|
||||
return response
|
||||
})
|
||||
.get("/find-many", distribusiAgamaFindMany)
|
||||
.post("/create", distribusiAgamaCreate, {
|
||||
body: t.Object({
|
||||
agama: t.String(),
|
||||
jumlah: t.Number(),
|
||||
tahun: t.Number(),
|
||||
}),
|
||||
})
|
||||
.put("/:id", distribusiAgamaUpdate, {
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
body: t.Object({
|
||||
agama: t.String(),
|
||||
jumlah: t.Number(),
|
||||
tahun: t.Number(),
|
||||
}),
|
||||
})
|
||||
.delete("/del/:id", distribusiAgamaDelete, {
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
})
|
||||
export default DistribusiAgama;
|
||||
@@ -0,0 +1,47 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function distribusiAgamaUpdate(context: Context) {
|
||||
const id = context.params?.id;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const {agama, jumlah, tahun} = context.body as {
|
||||
agama: string;
|
||||
jumlah: number;
|
||||
tahun: number;
|
||||
}
|
||||
|
||||
const existing = await prisma.distribusiAgama.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await prisma.distribusiAgama.update({
|
||||
where: { id },
|
||||
data: {
|
||||
agama,
|
||||
jumlah,
|
||||
tahun,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Data berhasil diupdate",
|
||||
data: updated,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = Prisma.DistribusiUmurGetPayload<{
|
||||
select: {
|
||||
rentangUmur: true;
|
||||
jumlah: true;
|
||||
tahun: true;
|
||||
}
|
||||
}>
|
||||
|
||||
export default async function distribusiUmurCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
const created = await prisma.distribusiUmur.create({
|
||||
data: {
|
||||
rentangUmur: body.rentangUmur,
|
||||
jumlah: body.jumlah,
|
||||
tahun: body.tahun,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
rentangUmur: true,
|
||||
jumlah: true,
|
||||
tahun: true,
|
||||
}
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Sukses menambahkan distribusi umur",
|
||||
data: created,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function distribusiUmurDelete(context: Context) {
|
||||
const id = context.params?.id;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await prisma.distribusiUmur.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const deleted = await prisma.distribusiUmur.delete({
|
||||
where: { id },
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Data berhasil dihapus",
|
||||
data: deleted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function distribusiUmurFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 100;
|
||||
const tahun = Number(context.query.tahun) || new Date().getFullYear();
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = { isActive: true, tahun };
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.distribusiUmur.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: "desc" },
|
||||
}),
|
||||
prisma.distribusiUmur.count({
|
||||
where,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch distribusi umur with pagination",
|
||||
data,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to fetch distribusi umur with pagination",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function distribusiUmurFindUnique(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const pathSegments = url.pathname.split('/');
|
||||
const id = pathSegments[pathSegments.length - 1];
|
||||
|
||||
if (!id) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "ID tidak boleh kosong",
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof id !== 'string') {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "ID tidak valid",
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const data = await prisma.distribusiUmur.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: "Data ditemukan",
|
||||
data: data,
|
||||
}, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "Terjadi kesalahan saat mengambil data",
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import distribusiUmurFindUnique from "./findUnique";
|
||||
import distribusiUmurUpdate from "./updt";
|
||||
import distribusiUmurFindMany from "./findMany";
|
||||
import distribusiUmurCreate from "./create";
|
||||
import distribusiUmurDelete from "./del";
|
||||
|
||||
const DistribusiUmur = new Elysia({
|
||||
prefix: "/distribusiumur",
|
||||
tags: ["Kependudukan/Distribusi Umur"],
|
||||
})
|
||||
.get("/:id", async (context) => {
|
||||
const response = await distribusiUmurFindUnique(new Request(context.request))
|
||||
return response
|
||||
})
|
||||
.get("/find-many", distribusiUmurFindMany)
|
||||
.post("/create", distribusiUmurCreate, {
|
||||
body: t.Object({
|
||||
rentangUmur: t.String(),
|
||||
jumlah: t.Number(),
|
||||
tahun: t.Number(),
|
||||
}),
|
||||
})
|
||||
.put("/:id", distribusiUmurUpdate, {
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
body: t.Object({
|
||||
rentangUmur: t.String(),
|
||||
jumlah: t.Number(),
|
||||
tahun: t.Number(),
|
||||
}),
|
||||
})
|
||||
.delete("/del/:id", distribusiUmurDelete, {
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
})
|
||||
export default DistribusiUmur;
|
||||
@@ -0,0 +1,47 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function distribusiUmurUpdate(context: Context) {
|
||||
const id = context.params?.id;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const {rentangUmur, jumlah, tahun} = context.body as {
|
||||
rentangUmur: string;
|
||||
jumlah: number;
|
||||
tahun: number;
|
||||
}
|
||||
|
||||
const existing = await prisma.distribusiUmur.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await prisma.distribusiUmur.update({
|
||||
where: { id },
|
||||
data: {
|
||||
rentangUmur,
|
||||
jumlah,
|
||||
tahun,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Data berhasil diupdate",
|
||||
data: updated,
|
||||
}
|
||||
}
|
||||
18
src/app/api/[[...slugs]]/_lib/kependudukan/index.ts
Normal file
18
src/app/api/[[...slugs]]/_lib/kependudukan/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import Elysia from "elysia";
|
||||
import DistribusiAgama from "./distribusi-agama";
|
||||
import DistribusiUmur from "./distribusi-umur";
|
||||
import DataBanjar from "./data-banjar";
|
||||
import MigrasiPenduduk from "./migrasi-penduduk";
|
||||
import DashboardKependudukan from "./dashboard";
|
||||
|
||||
const Kependudukan = new Elysia({
|
||||
prefix: "/kependudukan",
|
||||
tags: ["Kependudukan"],
|
||||
})
|
||||
.use(DashboardKependudukan)
|
||||
.use(DistribusiAgama)
|
||||
.use(DistribusiUmur)
|
||||
.use(DataBanjar)
|
||||
.use(MigrasiPenduduk)
|
||||
|
||||
export default Kependudukan;
|
||||
@@ -0,0 +1,43 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = Prisma.MigrasiPendudukGetPayload<{
|
||||
select: {
|
||||
jenis: true;
|
||||
nama: true;
|
||||
tanggal: true;
|
||||
asalTujuan: true;
|
||||
alasan: true;
|
||||
jenisKelamin: true;
|
||||
}
|
||||
}>
|
||||
|
||||
export default async function migrasiPendudukCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
const created = await prisma.migrasiPenduduk.create({
|
||||
data: {
|
||||
jenis: body.jenis,
|
||||
nama: body.nama,
|
||||
tanggal: new Date(body.tanggal),
|
||||
asalTujuan: body.asalTujuan,
|
||||
alasan: body.alasan,
|
||||
jenisKelamin: body.jenisKelamin,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
jenis: true,
|
||||
nama: true,
|
||||
tanggal: true,
|
||||
asalTujuan: true,
|
||||
alasan: true,
|
||||
jenisKelamin: true,
|
||||
}
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Sukses menambahkan migrasi penduduk",
|
||||
data: created,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function migrasiPendudukDelete(context: Context) {
|
||||
const id = context.params?.id;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await prisma.migrasiPenduduk.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const deleted = await prisma.migrasiPenduduk.delete({
|
||||
where: { id },
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Data berhasil dihapus",
|
||||
data: deleted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function migrasiPendudukFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || '';
|
||||
const jenis = (context.query.jenis as string) || '';
|
||||
const tahun = Number(context.query.tahun) || new Date().getFullYear();
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = { isActive: true };
|
||||
|
||||
if (jenis) {
|
||||
where.jenis = jenis;
|
||||
}
|
||||
|
||||
if (tahun) {
|
||||
where.tanggal = {
|
||||
gte: new Date(`${tahun}-01-01`),
|
||||
lte: new Date(`${tahun}-12-31`),
|
||||
};
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: "insensitive" } },
|
||||
{ asalTujuan: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.migrasiPenduduk.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { tanggal: "desc" },
|
||||
}),
|
||||
prisma.migrasiPenduduk.count({
|
||||
where,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch migrasi penduduk with pagination",
|
||||
data,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to fetch migrasi penduduk with pagination",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function migrasiPendudukFindUnique(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const pathSegments = url.pathname.split('/');
|
||||
const id = pathSegments[pathSegments.length - 1];
|
||||
|
||||
if (!id) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "ID tidak boleh kosong",
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof id !== 'string') {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "ID tidak valid",
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const data = await prisma.migrasiPenduduk.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: "Data ditemukan",
|
||||
data: data,
|
||||
}, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "Terjadi kesalahan saat mengambil data",
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import migrasiPendudukFindUnique from "./findUnique";
|
||||
import migrasiPendudukUpdate from "./updt";
|
||||
import migrasiPendudukFindMany from "./findMany";
|
||||
import migrasiPendudukCreate from "./create";
|
||||
import migrasiPendudukDelete from "./del";
|
||||
|
||||
const MigrasiPenduduk = new Elysia({
|
||||
prefix: "/migrasipenduduk",
|
||||
tags: ["Kependudukan/Migrasi Penduduk"],
|
||||
})
|
||||
.get("/:id", async (context) => {
|
||||
const response = await migrasiPendudukFindUnique(new Request(context.request))
|
||||
return response
|
||||
})
|
||||
.get("/find-many", migrasiPendudukFindMany)
|
||||
.post("/create", migrasiPendudukCreate, {
|
||||
body: t.Object({
|
||||
jenis: t.String(),
|
||||
nama: t.String(),
|
||||
tanggal: t.String(),
|
||||
asalTujuan: t.String(),
|
||||
alasan: t.Optional(t.String()),
|
||||
jenisKelamin: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
.put("/:id", migrasiPendudukUpdate, {
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
body: t.Object({
|
||||
jenis: t.String(),
|
||||
nama: t.String(),
|
||||
tanggal: t.String(),
|
||||
asalTujuan: t.String(),
|
||||
alasan: t.Optional(t.String()),
|
||||
jenisKelamin: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
.delete("/del/:id", migrasiPendudukDelete, {
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
})
|
||||
export default MigrasiPenduduk;
|
||||
@@ -0,0 +1,53 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function migrasiPendudukUpdate(context: Context) {
|
||||
const id = context.params?.id;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const {jenis, nama, tanggal, asalTujuan, alasan, jenisKelamin} = context.body as {
|
||||
jenis: string;
|
||||
nama: string;
|
||||
tanggal: string;
|
||||
asalTujuan: string;
|
||||
alasan?: string;
|
||||
jenisKelamin?: string;
|
||||
}
|
||||
|
||||
const existing = await prisma.migrasiPenduduk.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Data tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await prisma.migrasiPenduduk.update({
|
||||
where: { id },
|
||||
data: {
|
||||
jenis,
|
||||
nama,
|
||||
tanggal: new Date(tanggal),
|
||||
asalTujuan,
|
||||
alasan,
|
||||
jenisKelamin,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Data berhasil diupdate",
|
||||
data: updated,
|
||||
}
|
||||
}
|
||||
196
src/app/darmasaba/(pages)/kependudukan/dashboard/page.tsx
Normal file
196
src/app/darmasaba/(pages)/kependudukan/dashboard/page.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Box, Paper, Text, Title, SimpleGrid, Skeleton, Group, Badge, Center, Image } from '@mantine/core';
|
||||
import { IconUsers, IconHome, IconBasket, IconCoin, IconDatabaseOff } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import kependudukanDashboard from '@/app/admin/(dashboard)/_state/kependudukan/dashboard';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
|
||||
function Page() {
|
||||
const state = useProxy(kependudukanDashboard)
|
||||
|
||||
useShallowEffect(() => {
|
||||
state.summary.load()
|
||||
}, [])
|
||||
|
||||
const summary = state.summary.data?.summary;
|
||||
|
||||
if (state.summary.loading) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={200} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
if (!summary) {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
>
|
||||
Dashboard Kependudukan
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
c="black"
|
||||
>
|
||||
Ringkasan data kependudukan Desa Darmasaba
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" withBorder>
|
||||
<Center py="xl">
|
||||
<Stack align="center" gap="md">
|
||||
<IconDatabaseOff size={80} color={colors.grey['2']} />
|
||||
<Text ta="center" fz="lg" fw={500} c="dimmed">
|
||||
Data Belum Tersedia
|
||||
</Text>
|
||||
<Text ta="center" fz="sm" c="dimmed" maw={400}>
|
||||
Data kependudukan untuk tahun ini belum diperbarui.
|
||||
Silakan hubungi administrator desa untuk informasi lebih lanjut.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const stats = [
|
||||
{
|
||||
title: 'Total Penduduk',
|
||||
value: summary.totalPenduduk,
|
||||
icon: IconUsers,
|
||||
color: colors['blue-button'],
|
||||
},
|
||||
{
|
||||
title: 'Kepala Keluarga',
|
||||
value: summary.totalKK,
|
||||
icon: IconHome,
|
||||
color: '#6EDF9C',
|
||||
},
|
||||
{
|
||||
title: 'Kelahiran Tahun Ini',
|
||||
value: summary.totalKelahiran,
|
||||
icon: IconBasket,
|
||||
color: '#FF9F43',
|
||||
},
|
||||
{
|
||||
title: 'Penduduk Miskin',
|
||||
value: summary.totalKemiskinan,
|
||||
icon: IconCoin,
|
||||
color: '#EE5050',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
>
|
||||
Dashboard Kependudukan
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
c="black"
|
||||
>
|
||||
Ringkasan data kependudukan Desa Darmasaba
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="md">
|
||||
{stats.map((stat) => {
|
||||
const Icon = stat.icon;
|
||||
return (
|
||||
<Paper key={stat.title} p="xl" withBorder>
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text c="dimmed" fz="sm" fw={500}>
|
||||
{stat.title}
|
||||
</Text>
|
||||
<Text fz={28} fw={700} mt={5}>
|
||||
{stat.value.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
</div>
|
||||
<Badge
|
||||
size="xl"
|
||||
color={stat.color}
|
||||
variant="light"
|
||||
>
|
||||
<Icon size={32} />
|
||||
</Badge>
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
{state.summary.data?.dinamika && (
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" withBorder>
|
||||
<Title order={3} mb="md" c={colors["blue-button"]}>
|
||||
Dinamika Penduduk
|
||||
</Title>
|
||||
<SimpleGrid cols={{ base: 2, md: 4 }} spacing="md">
|
||||
<Paper p="md" bg="#6EDF9C">
|
||||
<Text fz="sm" c="white">Kelahiran</Text>
|
||||
<Text fz={24} fw={700} c="white">
|
||||
{state.summary.data.dinamika.kelahiran}
|
||||
</Text>
|
||||
</Paper>
|
||||
<Paper p="md" bg="#EE5050">
|
||||
<Text fz="sm" c="white">Kematian</Text>
|
||||
<Text fz={24} fw={700} c="white">
|
||||
{state.summary.data.dinamika.kematian}
|
||||
</Text>
|
||||
</Paper>
|
||||
<Paper p="md" bg="#5082EE">
|
||||
<Text fz="sm" c="white">Pindah Masuk</Text>
|
||||
<Text fz={24} fw={700} c="white">
|
||||
{state.summary.data.dinamika.pindahMasuk}
|
||||
</Text>
|
||||
</Paper>
|
||||
<Paper p="md" bg="#FF9F43">
|
||||
<Text fz="sm" c="white">Pindah Keluar</Text>
|
||||
<Text fz={24} fw={700} c="white">
|
||||
{state.summary.data.dinamika.pindahKeluar}
|
||||
</Text>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
147
src/app/darmasaba/(pages)/kependudukan/data-per-banjar/page.tsx
Normal file
147
src/app/darmasaba/(pages)/kependudukan/data-per-banjar/page.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Box, Paper, Text, Title, Skeleton, Table, Center } from '@mantine/core';
|
||||
import { IconDatabaseOff } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import dataBanjar from '@/app/admin/(dashboard)/_state/kependudukan/data-banjar';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
|
||||
function Page() {
|
||||
const state = useProxy(dataBanjar)
|
||||
|
||||
useShallowEffect(() => {
|
||||
state.findMany.load()
|
||||
}, [])
|
||||
|
||||
const data = state.findMany.data || [];
|
||||
|
||||
if (state.findMany.loading) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
>
|
||||
Data per Banjar
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
c="black"
|
||||
>
|
||||
Statistik kependudukan per banjar di Desa Darmasaba
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" withBorder>
|
||||
<Center py="xl">
|
||||
<Stack align="center" gap="md">
|
||||
<IconDatabaseOff size={80} color={colors.grey['2']} />
|
||||
<Text ta="center" fz="lg" fw={500} c="dimmed">
|
||||
Data Belum Tersedia
|
||||
</Text>
|
||||
<Text ta="center" fz="sm" c="dimmed" maw={400}>
|
||||
Data kependudukan per banjar untuk tahun ini belum diperbarui.
|
||||
Silakan hubungi administrator desa untuk informasi lebih lanjut.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const rows = data.map((item) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td>{item.nama}</Table.Td>
|
||||
<Table.Td ta="right">{item.penduduk.toLocaleString('id-ID')}</Table.Td>
|
||||
<Table.Td ta="right">{item.kk.toLocaleString('id-ID')}</Table.Td>
|
||||
<Table.Td ta="right">{item.miskin.toLocaleString('id-ID')}</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
|
||||
const totalPenduduk = data.reduce((sum, item) => sum + item.penduduk, 0);
|
||||
const totalKK = data.reduce((sum, item) => sum + item.kk, 0);
|
||||
const totalMiskin = data.reduce((sum, item) => sum + item.miskin, 0);
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
>
|
||||
Data per Banjar
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
c="black"
|
||||
>
|
||||
Statistik kependudukan per banjar di Desa Darmasaba
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" withBorder>
|
||||
<Text fw={700} fz="lg" mb="md" c="black">
|
||||
Data Kependudukan per Banjar
|
||||
</Text>
|
||||
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Banjar</Table.Th>
|
||||
<Table.Th ta="right">Penduduk</Table.Th>
|
||||
<Table.Th ta="right">Kepala Keluarga</Table.Th>
|
||||
<Table.Th ta="right">Penduduk Miskin</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows}
|
||||
<Table.Tr fw={700} bg={colors.grey[1]}>
|
||||
<Table.Td>Total</Table.Td>
|
||||
<Table.Td ta="right">{totalPenduduk.toLocaleString('id-ID')}</Table.Td>
|
||||
<Table.Td ta="right">{totalKK.toLocaleString('id-ID')}</Table.Td>
|
||||
<Table.Td ta="right">{totalMiskin.toLocaleString('id-ID')}</Table.Td>
|
||||
</Table.Tr>
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,189 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Box, Paper, Text, Title, Skeleton, SimpleGrid, Center } from '@mantine/core';
|
||||
import { IconDatabaseOff } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import kependudukanDashboard from '@/app/admin/(dashboard)/_state/kependudukan/dashboard';
|
||||
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
|
||||
function Page() {
|
||||
const stateKependudukan = useProxy(kependudukanDashboard);
|
||||
const statePersentaseKelahiranKematian = useProxy(persentaseKelahiranKematian);
|
||||
|
||||
// Load migration data from kependudukan dashboard
|
||||
useShallowEffect(() => {
|
||||
stateKependudukan.summary.load();
|
||||
}, []);
|
||||
|
||||
// Load birth and death data
|
||||
useShallowEffect(() => {
|
||||
statePersentaseKelahiranKematian.kelahiran.findMany.load();
|
||||
statePersentaseKelahiranKematian.kematian.findMany.load();
|
||||
}, []);
|
||||
|
||||
const dinamika = stateKependudukan.summary.data?.dinamika;
|
||||
|
||||
// Calculate birth and death counts from detailed data
|
||||
const kelahiranCount = statePersentaseKelahiranKematian.kelahiran.findMany.data?.length || 0;
|
||||
const kematianCount = statePersentaseKelahiranKematian.kematian.findMany.data?.length || 0;
|
||||
|
||||
const isLoading = stateKependudukan.summary.loading ||
|
||||
statePersentaseKelahiranKematian.kelahiran.findMany.loading ||
|
||||
statePersentaseKelahiranKematian.kematian.findMany.loading;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
if (!dinamika || (kelahiranCount === 0 && kematianCount === 0 && (!dinamika.pindahMasuk || dinamika.pindahMasuk === 0))) {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
>
|
||||
Dinamika Penduduk
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
c="black"
|
||||
>
|
||||
Statistik kelahiran, kematian, dan migrasi penduduk
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" withBorder>
|
||||
<Center py="xl">
|
||||
<Stack align="center" gap="md">
|
||||
<IconDatabaseOff size={80} color={colors.grey['2']} />
|
||||
<Text ta="center" fz="lg" fw={500} c="dimmed">
|
||||
Data Belum Tersedia
|
||||
</Text>
|
||||
<Text ta="center" fz="sm" c="dimmed" maw={400}>
|
||||
Data dinamika penduduk untuk tahun ini belum diperbarui.
|
||||
Silakan hubungi administrator desa untuk informasi lebih lanjut.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
>
|
||||
Dinamika Penduduk
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
c="black"
|
||||
>
|
||||
Statistik kelahiran, kematian, dan migrasi penduduk
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" withBorder>
|
||||
<Title order={3} mb="md" c={colors["blue-button"]}>
|
||||
Statistik Dinamika Penduduk
|
||||
</Title>
|
||||
<SimpleGrid cols={{ base: 2, md: 4 }} spacing="md">
|
||||
<Paper p="lg" bg="#6EDF9C">
|
||||
<Text fz="sm" c="white" fw={500}>
|
||||
Kelahiran
|
||||
</Text>
|
||||
<Text fz={36} fw={700} c="white" mt={10}>
|
||||
{kelahiranCount}
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
<Paper p="lg" bg="#EE5050">
|
||||
<Text fz="sm" c="white" fw={500}>
|
||||
Kematian
|
||||
</Text>
|
||||
<Text fz={36} fw={700} c="white" mt={10}>
|
||||
{kematianCount}
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
<Paper p="lg" bg="#5082EE">
|
||||
<Text fz="sm" c="white" fw={500}>
|
||||
Pindah Masuk
|
||||
</Text>
|
||||
<Text fz={36} fw={700} c="white" mt={10}>
|
||||
{dinamika?.pindahMasuk || 0}
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
<Paper p="lg" bg="#FF9F43">
|
||||
<Text fz="sm" c="white" fw={500}>
|
||||
Pindah Keluar
|
||||
</Text>
|
||||
<Text fz={36} fw={700} c="white" mt={10}>
|
||||
{dinamika?.pindahKeluar || 0}
|
||||
</Text>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
|
||||
<Box mt="xl" pt="xl" style={{ borderTop: `1px solid ${colors.grey['2']}` }}>
|
||||
<Text fz="md" c="dimmed" fw={500} mb="md">
|
||||
Ringkasan:
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<Paper p="md" bg={colors.grey[1]}>
|
||||
<Text fz="sm" c="dimmed">Pertumbuhan Alami</Text>
|
||||
<Text fz={24} fw={700} c={kelahiranCount - kematianCount >= 0 ? '#6EDF9C' : '#EE5050'}>
|
||||
{kelahiranCount - kematianCount > 0 ? '+' : ''}{kelahiranCount - kematianCount}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">(Kelahiran - Kematian)</Text>
|
||||
</Paper>
|
||||
|
||||
<Paper p="md" bg={colors.grey[1]}>
|
||||
<Text fz="sm" c="dimmed">Migrasi Bersih</Text>
|
||||
<Text fz={24} fw={700} c={(dinamika?.pindahMasuk || 0) - (dinamika?.pindahKeluar || 0) >= 0 ? colors['blue-button'] : '#FF9F43'}>
|
||||
{(dinamika?.pindahMasuk || 0) - (dinamika?.pindahKeluar || 0) > 0 ? '+' : ''}{(dinamika?.pindahMasuk || 0) - (dinamika?.pindahKeluar || 0)}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">(Pindah Masuk - Pindah Keluar)</Text>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
170
src/app/darmasaba/(pages)/kependudukan/distribusi-agama/page.tsx
Normal file
170
src/app/darmasaba/(pages)/kependudukan/distribusi-agama/page.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Box, Paper, Text, Title, Skeleton, Flex, ColorSwatch, Center } from '@mantine/core';
|
||||
import { IconDatabaseOff } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import distribusiAgama from '@/app/admin/(dashboard)/_state/kependudukan/distribusi-agama';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { PieChart } from '@mantine/charts';
|
||||
|
||||
function Page() {
|
||||
const state = useProxy(distribusiAgama)
|
||||
|
||||
useShallowEffect(() => {
|
||||
state.findMany.load()
|
||||
}, [])
|
||||
|
||||
const data = state.findMany.data || [];
|
||||
|
||||
if (state.findMany.loading) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
>
|
||||
Distribusi Agama
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
c="black"
|
||||
>
|
||||
Komposisi agama penduduk Desa Darmasaba
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" withBorder>
|
||||
<Center py="xl">
|
||||
<Stack align="center" gap="md">
|
||||
<IconDatabaseOff size={80} color={colors.grey['2']} />
|
||||
<Text ta="center" fz="lg" fw={500} c="dimmed">
|
||||
Data Belum Tersedia
|
||||
</Text>
|
||||
<Text ta="center" fz="sm" c="dimmed" maw={400}>
|
||||
Data distribusi agama untuk tahun ini belum diperbarui.
|
||||
Silakan hubungi administrator desa untuk informasi lebih lanjut.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const chartData = data.map(item => ({
|
||||
name: item.agama,
|
||||
value: item.jumlah,
|
||||
color: getColorForAgama(item.agama),
|
||||
}));
|
||||
|
||||
const total = data.reduce((sum, item) => sum + item.jumlah, 0);
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
>
|
||||
Distribusi Agama
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
c="black"
|
||||
>
|
||||
Komposisi agama penduduk Desa Darmasaba
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" withBorder>
|
||||
<Text fw={700} fz="lg" mb="md" c="black">
|
||||
Statistik Distribusi Agama
|
||||
</Text>
|
||||
|
||||
<Flex direction={{ base: 'column', md: 'row' }} gap="xl" align="center">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<PieChart
|
||||
data={chartData}
|
||||
size={300}
|
||||
labelsPosition="inside"
|
||||
withLabels
|
||||
withLabelsLine
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Stack style={{ flex: 1 }} gap="md">
|
||||
{data.map((item) => (
|
||||
<Flex key={item.id} align="center" gap="sm">
|
||||
<ColorSwatch color={getColorForAgama(item.agama)} size={20} />
|
||||
<Text fz="sm" c="black" style={{ flex: 1 }}>
|
||||
{item.agama}
|
||||
</Text>
|
||||
<Text fz="sm" fw={700} c="black">
|
||||
{item.jumlah.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
({((item.jumlah / total) * 100).toFixed(1)}%)
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
<Box mt="md" pt="md" style={{ borderTop: `1px solid ${colors.grey['2']}` }}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text fw={700} c="black">Total</Text>
|
||||
<Text fw={700} c="black">{total.toLocaleString('id-ID')}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function getColorForAgama(agama: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
'HINDU': '#FF9F43',
|
||||
'ISLAM': '#6EDF9C',
|
||||
'KRISTEN': '#5082EE',
|
||||
'KRISTEN_PROTESTAN': '#5082EE',
|
||||
'KRISTEN_KATOLIK': '#4263D1',
|
||||
'BUDDHA': '#FFD43B',
|
||||
'KONGHUCU': '#EE5050',
|
||||
'LAINNYA': '#868E96',
|
||||
};
|
||||
return colors[agama] || '#868E96';
|
||||
}
|
||||
|
||||
export default Page;
|
||||
150
src/app/darmasaba/(pages)/kependudukan/distribusi-umur/page.tsx
Normal file
150
src/app/darmasaba/(pages)/kependudukan/distribusi-umur/page.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Box, Paper, Text, Title, Skeleton, Center } from '@mantine/core';
|
||||
import { IconDatabaseOff } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import distribusiUmur from '@/app/admin/(dashboard)/_state/kependudukan/distribusi-umur';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { BarChart } from '@mantine/charts';
|
||||
|
||||
function Page() {
|
||||
const state = useProxy(distribusiUmur)
|
||||
|
||||
useShallowEffect(() => {
|
||||
state.findMany.load()
|
||||
}, [])
|
||||
|
||||
const data = state.findMany.data || [];
|
||||
|
||||
if (state.findMany.loading) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
>
|
||||
Distribusi Umur
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
c="black"
|
||||
>
|
||||
Komposisi penduduk berdasarkan kelompok umur
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" withBorder>
|
||||
<Center py="xl">
|
||||
<Stack align="center" gap="md">
|
||||
<IconDatabaseOff size={80} color={colors.grey['2']} />
|
||||
<Text ta="center" fz="lg" fw={500} c="dimmed">
|
||||
Data Belum Tersedia
|
||||
</Text>
|
||||
<Text ta="center" fz="sm" c="dimmed" maw={400}>
|
||||
Data distribusi umur untuk tahun ini belum diperbarui.
|
||||
Silakan hubungi administrator desa untuk informasi lebih lanjut.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
// Sort data by age range (extract the first number from rentangUmur)
|
||||
const sortedData = [...data].sort((a, b) => {
|
||||
const extractMinAge = (range: string) => {
|
||||
const match = range.match(/^(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : 999;
|
||||
};
|
||||
return extractMinAge(a.rentangUmur) - extractMinAge(b.rentangUmur);
|
||||
});
|
||||
|
||||
const chartData = sortedData.map(item => ({
|
||||
umur: item.rentangUmur,
|
||||
jumlah: item.jumlah,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
>
|
||||
Distribusi Umur
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
c="black"
|
||||
>
|
||||
Komposisi penduduk berdasarkan kelompok umur
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" withBorder>
|
||||
<Text fw={700} fz="lg" mb="md" c="black">
|
||||
Statistik Distribusi Umur
|
||||
</Text>
|
||||
|
||||
<BarChart
|
||||
h={400}
|
||||
data={chartData}
|
||||
dataKey="umur"
|
||||
series={[{ name: 'jumlah', color: colors['blue-button'] }]}
|
||||
tickLine="y"
|
||||
yAxisProps={{
|
||||
width: 80,
|
||||
}}
|
||||
xAxisProps={{
|
||||
angle: -45,
|
||||
textAnchor: 'end',
|
||||
height: 100,
|
||||
interval: 0,
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
}
|
||||
}}
|
||||
gridAxis="y"
|
||||
withXAxis
|
||||
withYAxis
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
Reference in New Issue
Block a user