tambahan
This commit is contained in:
230
bun.lock
230
bun.lock
@@ -9,15 +9,19 @@
|
|||||||
"@elysiajs/jwt": "^1.4.0",
|
"@elysiajs/jwt": "^1.4.0",
|
||||||
"@elysiajs/swagger": "^1.3.1",
|
"@elysiajs/swagger": "^1.3.1",
|
||||||
"@mantine/core": "^8.3.3",
|
"@mantine/core": "^8.3.3",
|
||||||
|
"@mantine/dates": "^8.3.4",
|
||||||
|
"@mantine/form": "^8.3.4",
|
||||||
"@mantine/hooks": "^8.3.3",
|
"@mantine/hooks": "^8.3.3",
|
||||||
"@mantine/notifications": "^8.3.3",
|
"@mantine/notifications": "^8.3.3",
|
||||||
"@modelcontextprotocol/sdk": "^1.19.1",
|
"@modelcontextprotocol/sdk": "^1.19.1",
|
||||||
"@prisma/client": "^6.7.0",
|
"@prisma/client": "^6.7.0",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@types/jwt-decode": "^3.1.0",
|
"@types/jwt-decode": "^3.1.0",
|
||||||
|
"@types/lodash": "^4.17.20",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"elysia": "^1.4.9",
|
"elysia": "^1.4.9",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
"react-router-dom": "^7.9.3",
|
"react-router-dom": "^7.9.3",
|
||||||
@@ -28,6 +32,8 @@
|
|||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"biome": "^0.3.3",
|
||||||
|
"oxlint": "^1.22.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"postcss-preset-mantine": "^1.18.0",
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
@@ -58,6 +64,10 @@
|
|||||||
|
|
||||||
"@mantine/core": ["@mantine/core@8.3.3", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", "react-number-format": "^5.4.4", "react-remove-scroll": "^2.7.1", "react-textarea-autosize": "8.5.9", "type-fest": "^4.41.0" }, "peerDependencies": { "@mantine/hooks": "8.3.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-OdTAQ0lsXjEqfea0KyXJ1rV9cZb/Rtqv5l3luG2m8Sx5BTGMqXas6mKHtdj4LwIiUKeFkIkZYjNmH6ri1HXjSA=="],
|
"@mantine/core": ["@mantine/core@8.3.3", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", "react-number-format": "^5.4.4", "react-remove-scroll": "^2.7.1", "react-textarea-autosize": "8.5.9", "type-fest": "^4.41.0" }, "peerDependencies": { "@mantine/hooks": "8.3.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-OdTAQ0lsXjEqfea0KyXJ1rV9cZb/Rtqv5l3luG2m8Sx5BTGMqXas6mKHtdj4LwIiUKeFkIkZYjNmH6ri1HXjSA=="],
|
||||||
|
|
||||||
|
"@mantine/dates": ["@mantine/dates@8.3.4", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "@mantine/core": "8.3.4", "@mantine/hooks": "8.3.4", "dayjs": ">=1.0.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-Je7R86lIpU8gCMdSueCD5TZrIOEFPiRr3t+/uvgBGG4Om6KmU2FJw/8f8wbVDmj0KJAl2xzXy2UiKJGpnLdl1A=="],
|
||||||
|
|
||||||
|
"@mantine/form": ["@mantine/form@8.3.4", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-mfJ5tFTLP5IGBn1yY+ZpOMNF0/NPjjv+C8T1wL6Xcoik1FvfavdE/1viE1Dy7VlFZWxIb1mr0L0OrqRTL/8g0A=="],
|
||||||
|
|
||||||
"@mantine/hooks": ["@mantine/hooks@8.3.3", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-nmspxbFSjFkimRXYhgAujnyBwGeAWDSP1WKHFR+Yl5x3Q0IkmsiOTE9yJPjMjmjffZfunFXQFwQDl1OF3m42Pw=="],
|
"@mantine/hooks": ["@mantine/hooks@8.3.3", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-nmspxbFSjFkimRXYhgAujnyBwGeAWDSP1WKHFR+Yl5x3Q0IkmsiOTE9yJPjMjmjffZfunFXQFwQDl1OF3m42Pw=="],
|
||||||
|
|
||||||
"@mantine/notifications": ["@mantine/notifications@8.3.3", "", { "dependencies": { "@mantine/store": "8.3.3", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "8.3.3", "@mantine/hooks": "8.3.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-tEp2nGxx9gd8616V7T93l6D6XAXmEa+H2MERwxsBs6IGjGcswda8MUc10SLhLCJgDzB0RX0Pcod4r+tpGbXz/Q=="],
|
"@mantine/notifications": ["@mantine/notifications@8.3.3", "", { "dependencies": { "@mantine/store": "8.3.3", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "8.3.3", "@mantine/hooks": "8.3.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-tEp2nGxx9gd8616V7T93l6D6XAXmEa+H2MERwxsBs6IGjGcswda8MUc10SLhLCJgDzB0RX0Pcod4r+tpGbXz/Q=="],
|
||||||
@@ -66,6 +76,22 @@
|
|||||||
|
|
||||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.19.1", "", { "dependencies": { "ajv": "^6.12.6", "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.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-3Y2h3MZKjec1eAqSTBclATlX+AbC6n1LgfVzRMJLt3v6w0RCYgwLrjbxPDbhsYHt6Wdqc/aCceNJYgj448ELQQ=="],
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.19.1", "", { "dependencies": { "ajv": "^6.12.6", "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.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-3Y2h3MZKjec1eAqSTBclATlX+AbC6n1LgfVzRMJLt3v6w0RCYgwLrjbxPDbhsYHt6Wdqc/aCceNJYgj448ELQQ=="],
|
||||||
|
|
||||||
|
"@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.22.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vfgwTA1CowVaU3QXFBjfGjbPsHbdjAiJnWX5FBaq8uXS8tksGgl0ue14MK6fVnXncWK9j69LRnkteGTixxDAfA=="],
|
||||||
|
|
||||||
|
"@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.22.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-70x7Y+e0Ddb2Cf2IZsYGnXZrnB/MZgOTi/VkyXZucbnQcpi2VoaYS4Ve662DaNkzvTxdKOGmyJVMmD/digdJLQ=="],
|
||||||
|
|
||||||
|
"@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.22.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Rv94lOyEV8WEuzhjJSpCW3DbL/tlOVizPxth1v5XAFuQdM5rgpOMs3TsAf/YFUn52/qenwVglyvQZL8oAUYlpg=="],
|
||||||
|
|
||||||
|
"@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.22.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Aau6V6Osoyb3SFmRejP3rRhs1qhep4aJTdotFf1RVMVSLJkF7Ir0p+eGZSaIJyylFZuCCxHpud3hWasphmZnzw=="],
|
||||||
|
|
||||||
|
"@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.22.0", "", { "os": "linux", "cpu": "x64" }, "sha512-6eOtv+2gHrKw/hxUkV6hJdvYhzr0Dqzb4oc7sNlWxp64jU6I19tgMwSlmtn02r34YNSn+/NpZ/ECvQrycKUUFQ=="],
|
||||||
|
|
||||||
|
"@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.22.0", "", { "os": "linux", "cpu": "x64" }, "sha512-c4O7qD7TCEfPE/FFKYvakF2sQoIP0LFZB8F5AQK4K9VYlyT1oENNRCdIiMu6irvLelOzJzkUM0XrvUCL9Kkxrw=="],
|
||||||
|
|
||||||
|
"@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.22.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-6DJwF5A9VoIbSWNexLYubbuteAL23l3YN00wUL7Wt4ZfEZu2f/lWtGB9yC9BfKLXzudq8MvGkrS0szmV0bc1VQ=="],
|
||||||
|
|
||||||
|
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.22.0", "", { "os": "win32", "cpu": "x64" }, "sha512-nf8EZnIUgIrHlP9k26iOFMZZPoJG16KqZBXu5CG5YTAtVcu4CWlee9Q/cOS/rgQNGjLF+WPw8sVA5P3iGlYGQQ=="],
|
||||||
|
|
||||||
"@prisma/client": ["@prisma/client@6.16.3", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-JfNfAtXG+/lIopsvoZlZiH2k5yNx87mcTS4t9/S5oufM1nKdXYxOvpDC1XoTCFBa5cQh7uXnbMPsmZrwZY80xw=="],
|
"@prisma/client": ["@prisma/client@6.16.3", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-JfNfAtXG+/lIopsvoZlZiH2k5yNx87mcTS4t9/S5oufM1nKdXYxOvpDC1XoTCFBa5cQh7uXnbMPsmZrwZY80xw=="],
|
||||||
|
|
||||||
"@prisma/config": ["@prisma/config@6.16.3", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, "sha512-VlsLnG4oOuKGGMToEeVaRhoTBZu5H3q51jTQXb/diRags3WV0+BQK5MolJTtP6G7COlzoXmWeS11rNBtvg+qFQ=="],
|
"@prisma/config": ["@prisma/config@6.16.3", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, "sha512-VlsLnG4oOuKGGMToEeVaRhoTBZu5H3q51jTQXb/diRags3WV0+BQK5MolJTtP6G7COlzoXmWeS11rNBtvg+qFQ=="],
|
||||||
@@ -98,6 +124,8 @@
|
|||||||
|
|
||||||
"@types/jwt-decode": ["@types/jwt-decode@3.1.0", "", { "dependencies": { "jwt-decode": "*" } }, "sha512-tthwik7TKkou3mVnBnvVuHnHElbjtdbM63pdBCbZTirCt3WAdM73Y79mOri7+ljsS99ZVwUFZHLMxJuJnv/z1w=="],
|
"@types/jwt-decode": ["@types/jwt-decode@3.1.0", "", { "dependencies": { "jwt-decode": "*" } }, "sha512-tthwik7TKkou3mVnBnvVuHnHElbjtdbM63pdBCbZTirCt3WAdM73Y79mOri7+ljsS99ZVwUFZHLMxJuJnv/z1w=="],
|
||||||
|
|
||||||
|
"@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.7.0", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw=="],
|
"@types/node": ["@types/node@24.7.0", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="],
|
"@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="],
|
||||||
@@ -112,8 +140,36 @@
|
|||||||
|
|
||||||
"ajv": ["ajv@6.12.6", "", { "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-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
"ajv": ["ajv@6.12.6", "", { "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-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||||
|
|
||||||
|
"ansi-escapes": ["ansi-escapes@1.4.0", "", {}, "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@2.2.1", "", {}, "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA=="],
|
||||||
|
|
||||||
|
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
|
||||||
|
|
||||||
|
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
|
||||||
|
|
||||||
|
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
|
||||||
|
|
||||||
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
|
"aws-sign2": ["aws-sign2@0.7.0", "", {}, "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="],
|
||||||
|
|
||||||
|
"aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="],
|
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
|
||||||
|
|
||||||
|
"biome": ["biome@0.3.3", "", { "dependencies": { "bluebird": "^3.4.1", "chalk": "^1.1.3", "commander": "^2.9.0", "editor": "^1.0.0", "fs-promise": "^0.5.0", "inquirer-promise": "0.0.3", "request-promise": "^3.0.0", "untildify": "^3.0.2", "user-home": "^2.0.0" }, "bin": { "biome": "./dist/index.js" } }, "sha512-4LXjrQYbn9iTXu9Y4SKT7ABzTV0WnLDHCVSd2fPUOKsy1gQ+E4xPFmlY1zcWexoi0j7fGHItlL6OWA2CZ/yYAQ=="],
|
||||||
|
|
||||||
|
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
|
||||||
|
|
||||||
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
|
"bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
|
||||||
|
|
||||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
@@ -126,12 +182,28 @@
|
|||||||
|
|
||||||
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||||
|
|
||||||
|
"caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="],
|
||||||
|
|
||||||
|
"chalk": ["chalk@1.1.3", "", { "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } }, "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="],
|
||||||
|
|
||||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
||||||
|
|
||||||
|
"cli-cursor": ["cli-cursor@1.0.2", "", { "dependencies": { "restore-cursor": "^1.0.1" } }, "sha512-25tABq090YNKkF6JH7lcwO0zFJTRke4Jcq9iX2nr/Sz0Cjjv4gckmwlW6Ty/aoyFd6z3ysR2hMGC2GFugmBo6A=="],
|
||||||
|
|
||||||
|
"cli-width": ["cli-width@1.1.1", "", {}, "sha512-eMU2akIeEIkCxGXUNmDnJq1KzOIiPnJ+rKqRe6hcxE3vIOPvpMrBYOn/Bl7zNlYJj/zQxXquAnozHUCf9Whnsg=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"code-point-at": ["code-point-at@1.1.0", "", {}, "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA=="],
|
||||||
|
|
||||||
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
|
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
|
|
||||||
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||||
|
|
||||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||||
@@ -144,6 +216,10 @@
|
|||||||
|
|
||||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
|
|
||||||
|
"core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="],
|
||||||
|
|
||||||
|
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
|
||||||
|
|
||||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
@@ -152,12 +228,18 @@
|
|||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="],
|
||||||
|
|
||||||
|
"dayjs": ["dayjs@1.11.18", "", {}, "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="],
|
"deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="],
|
||||||
|
|
||||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
|
|
||||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||||
@@ -172,6 +254,12 @@
|
|||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"earlgrey-runtime": ["earlgrey-runtime@0.1.2", "", { "dependencies": { "core-js": "^2.4.0", "kaiser": ">=0.0.4", "lodash": "^4.17.2", "regenerator-runtime": "^0.9.5" } }, "sha512-T4qoScXi5TwALDv8nlGTvOuCT8jXcKcxtO8qVdqv46IA2GHJfQzwoBPbkOmORnyhu3A98cVVuhWLsM2CzPljJg=="],
|
||||||
|
|
||||||
|
"ecc-jsbn": ["ecc-jsbn@0.1.2", "", { "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw=="],
|
||||||
|
|
||||||
|
"editor": ["editor@1.0.0", "", {}, "sha512-SoRmbGStwNYHgKfjOrX2L0mUvp9bUVv0uPppZSOMAntEbcFtoC3MKF5b3T6HQPXKIV+QGY3xPO3JK5it5lVkuw=="],
|
||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
"effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="],
|
"effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="],
|
||||||
@@ -190,6 +278,8 @@
|
|||||||
|
|
||||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
|
|
||||||
|
"escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||||
|
|
||||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||||
|
|
||||||
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||||
@@ -198,12 +288,18 @@
|
|||||||
|
|
||||||
"exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="],
|
"exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="],
|
||||||
|
|
||||||
|
"exit-hook": ["exit-hook@1.1.1", "", {}, "sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg=="],
|
||||||
|
|
||||||
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.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-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.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-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
||||||
|
|
||||||
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
|
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
|
||||||
|
|
||||||
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
|
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
|
||||||
|
|
||||||
|
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||||
|
|
||||||
|
"extsprintf": ["extsprintf@1.3.0", "", {}, "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="],
|
||||||
|
|
||||||
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
||||||
|
|
||||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||||
@@ -214,12 +310,24 @@
|
|||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"figures": ["figures@1.7.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5", "object-assign": "^4.1.0" } }, "sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ=="],
|
||||||
|
|
||||||
"finalhandler": ["finalhandler@2.1.0", "", { "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-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
|
"finalhandler": ["finalhandler@2.1.0", "", { "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-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
|
||||||
|
|
||||||
|
"forever-agent": ["forever-agent@0.6.1", "", {}, "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@2.3.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } }, "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ=="],
|
||||||
|
|
||||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
|
|
||||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
|
"fs-extra": ["fs-extra@0.26.7", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^2.1.0", "klaw": "^1.0.0", "path-is-absolute": "^1.0.0", "rimraf": "^2.2.8" } }, "sha512-waKu+1KumRhYv8D8gMRCKJGAMI9pRnPuEb1mvgYD0f7wBscg+h6bW4FDTmEZhB9VKxvoTtxW+Y7bnIlB7zja6Q=="],
|
||||||
|
|
||||||
|
"fs-promise": ["fs-promise@0.5.0", "", { "dependencies": { "any-promise": "^1.0.0", "fs-extra": "^0.26.5", "mz": "^2.3.1", "thenify-all": "^1.6.0" } }, "sha512-Y+4F4ujhEcayCJt6JmzcOun9MYGQwz+bVUiuBmTkJImhBHKpBvmVPZR9wtfiF7k3ffwAOAuurygQe+cPLSFQhw=="],
|
||||||
|
|
||||||
|
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
@@ -228,10 +336,22 @@
|
|||||||
|
|
||||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
|
"getpass": ["getpass@0.1.7", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng=="],
|
||||||
|
|
||||||
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||||
|
|
||||||
|
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"har-schema": ["har-schema@2.0.0", "", {}, "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q=="],
|
||||||
|
|
||||||
|
"har-validator": ["har-validator@5.1.5", "", { "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w=="],
|
||||||
|
|
||||||
|
"has-ansi": ["has-ansi@2.0.0", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg=="],
|
||||||
|
|
||||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
@@ -240,26 +360,58 @@
|
|||||||
|
|
||||||
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||||
|
|
||||||
|
"http-signature": ["http-signature@1.2.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ=="],
|
||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
|
"iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
|
||||||
|
|
||||||
|
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||||
|
|
||||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"inquirer": ["inquirer@0.11.4", "", { "dependencies": { "ansi-escapes": "^1.1.0", "ansi-regex": "^2.0.0", "chalk": "^1.0.0", "cli-cursor": "^1.0.1", "cli-width": "^1.0.1", "figures": "^1.3.5", "lodash": "^3.3.1", "readline2": "^1.0.1", "run-async": "^0.1.0", "rx-lite": "^3.1.2", "string-width": "^1.0.1", "strip-ansi": "^3.0.0", "through": "^2.3.6" } }, "sha512-QR+2TW90jnKk9LUUtbcA3yQXKt2rDEKMh6+BAZQIeumtzHexnwVLdPakSslGijXYLJCzFv7GMXbFCn0pA00EUw=="],
|
||||||
|
|
||||||
|
"inquirer-promise": ["inquirer-promise@0.0.3", "", { "dependencies": { "earlgrey-runtime": ">=0.0.11", "inquirer": "^0.11.3" } }, "sha512-82CQX586JAV9GAgU9yXZsMDs+NorjA0nLhkfFx9+PReyOnuoHRbHrC1Z90sS95bFJI1Tm1gzMObuE0HabzkJpg=="],
|
||||||
|
|
||||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@1.0.0", "", { "dependencies": { "number-is-nan": "^1.0.0" } }, "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw=="],
|
||||||
|
|
||||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||||
|
|
||||||
|
"is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="],
|
||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="],
|
||||||
|
|
||||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
"jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
|
"jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="],
|
||||||
|
|
||||||
|
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
|
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||||
|
|
||||||
|
"jsonfile": ["jsonfile@2.4.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw=="],
|
||||||
|
|
||||||
|
"jsprim": ["jsprim@1.4.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw=="],
|
||||||
|
|
||||||
"jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="],
|
"jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="],
|
||||||
|
|
||||||
|
"kaiser": ["kaiser@0.0.4", "", { "dependencies": { "earlgrey-runtime": ">=0.0.10" } }, "sha512-m8ju+rmBqvclZmyrOXgGGhOYSjKJK6RN1NhqEltemY87UqZOxEkizg9TOy1vQSyJ01Wx6SAPuuN0iO2Mgislvw=="],
|
||||||
|
|
||||||
|
"klaw": ["klaw@1.3.1", "", { "optionalDependencies": { "graceful-fs": "^4.1.9" } }, "sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw=="],
|
||||||
|
|
||||||
|
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
|
||||||
|
|
||||||
|
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
@@ -272,16 +424,26 @@
|
|||||||
|
|
||||||
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||||
|
|
||||||
|
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"mute-stream": ["mute-stream@0.0.5", "", {}, "sha512-EbrziT4s8cWPmzr47eYVW3wimS4HsvlnV5ri1xw1aR6JQo/OrJX5rkl32K/QQHdxeabJETtfeaROGhd8W7uBgg=="],
|
||||||
|
|
||||||
|
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
|
|
||||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||||
|
|
||||||
|
"number-is-nan": ["number-is-nan@1.0.1", "", {}, "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ=="],
|
||||||
|
|
||||||
"nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
|
"nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
|
||||||
|
|
||||||
|
"oauth-sign": ["oauth-sign@0.9.0", "", {}, "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="],
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
@@ -292,10 +454,18 @@
|
|||||||
|
|
||||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
|
"onetime": ["onetime@1.1.0", "", {}, "sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A=="],
|
||||||
|
|
||||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||||
|
|
||||||
|
"os-homedir": ["os-homedir@1.0.2", "", {}, "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ=="],
|
||||||
|
|
||||||
|
"oxlint": ["oxlint@1.22.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.22.0", "@oxlint/darwin-x64": "1.22.0", "@oxlint/linux-arm64-gnu": "1.22.0", "@oxlint/linux-arm64-musl": "1.22.0", "@oxlint/linux-x64-gnu": "1.22.0", "@oxlint/linux-x64-musl": "1.22.0", "@oxlint/win32-arm64": "1.22.0", "@oxlint/win32-x64": "1.22.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.2.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-/HYT1Cfanveim9QUM6KlPKJe9y+WPnh3SxIB7z1InWnag9S0nzxLaWEUiW1P4UGzh/No3KvtNmBv2IOiwAl2/w=="],
|
||||||
|
|
||||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||||
|
|
||||||
|
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||||
@@ -304,6 +474,8 @@
|
|||||||
|
|
||||||
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||||
|
|
||||||
|
"performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
@@ -334,6 +506,8 @@
|
|||||||
|
|
||||||
"proxy-compare": ["proxy-compare@3.0.1", "", {}, "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="],
|
"proxy-compare": ["proxy-compare@3.0.1", "", {}, "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="],
|
||||||
|
|
||||||
|
"psl": ["psl@1.15.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="],
|
||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
||||||
@@ -370,8 +544,24 @@
|
|||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"readline2": ["readline2@1.0.1", "", { "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "mute-stream": "0.0.5" } }, "sha512-8/td4MmwUB6PkZUbV25uKz7dfrmjYWxsW8DVfibWdlHRk/l/DfHKn4pU+dfcoGLFgWOdyGCzINRQD7jn+Bv+/g=="],
|
||||||
|
|
||||||
|
"regenerator-runtime": ["regenerator-runtime@0.9.6", "", {}, "sha512-D0Y/JJ4VhusyMOd/o25a3jdUqN/bC85EFsaoL9Oqmy/O4efCh+xhp7yj2EEOsj974qvMkcW8AwUzJ1jB/MbxCw=="],
|
||||||
|
|
||||||
|
"request": ["request@2.88.2", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" } }, "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw=="],
|
||||||
|
|
||||||
|
"request-promise": ["request-promise@3.0.0", "", { "dependencies": { "bluebird": "^3.3", "lodash": "^4.6.1", "request": "^2.34" } }, "sha512-wVGUX+BoKxYsavTA72i6qHcyLbjzM4LR4y/AmDCqlbuMAursZdDWO7PmgbGAUvD2SeEJ5iB99VSq/U51i/DNbw=="],
|
||||||
|
|
||||||
|
"restore-cursor": ["restore-cursor@1.0.1", "", { "dependencies": { "exit-hook": "^1.0.0", "onetime": "^1.0.0" } }, "sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw=="],
|
||||||
|
|
||||||
|
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
|
||||||
|
|
||||||
"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=="],
|
"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-async": ["run-async@0.1.0", "", { "dependencies": { "once": "^1.3.0" } }, "sha512-qOX+w+IxFgpUpJfkv2oGN0+ExPs68F4sZHfaRRx4dDexAQkG83atugKVEylyT5ARees3HBbfmuvnjbrd8j9Wjw=="],
|
||||||
|
|
||||||
|
"rx-lite": ["rx-lite@3.1.2", "", {}, "sha512-1I1+G2gteLB8Tkt8YI1sJvSIfa0lWuRtC8GjvtyPBcLSF5jBCCJJqKrpER5JU5r6Bhe+i9/pK3VMuUcXu0kdwQ=="],
|
||||||
|
|
||||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
@@ -400,22 +590,42 @@
|
|||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="],
|
||||||
|
|
||||||
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@1.0.2", "", { "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } }, "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@3.0.1", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg=="],
|
||||||
|
|
||||||
"sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw=="],
|
"sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw=="],
|
||||||
|
|
||||||
|
"supports-color": ["supports-color@2.0.0", "", {}, "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g=="],
|
||||||
|
|
||||||
"swr": ["swr@2.3.6", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw=="],
|
"swr": ["swr@2.3.6", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw=="],
|
||||||
|
|
||||||
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
|
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
|
||||||
|
|
||||||
|
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||||
|
|
||||||
|
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||||
|
|
||||||
|
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
|
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
|
"tough-cookie": ["tough-cookie@2.5.0", "", { "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" } }, "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="],
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||||
|
|
||||||
|
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
@@ -424,6 +634,8 @@
|
|||||||
|
|
||||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||||
|
|
||||||
|
"untildify": ["untildify@3.0.3", "", {}, "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA=="],
|
||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||||
@@ -438,12 +650,18 @@
|
|||||||
|
|
||||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
|
"user-home": ["user-home@2.0.0", "", { "dependencies": { "os-homedir": "^1.0.0" } }, "sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ=="],
|
||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="],
|
||||||
|
|
||||||
"valtio": ["valtio@2.1.8", "", { "dependencies": { "proxy-compare": "^3.0.1" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-fjTPbJyKEmfVBZUOh3V0OtMHoFUGr4+4XpejjxhNJE/IS2l8rDbyJuzi3w/fZWBDyk7BJOpG+lmvTK5iiVhXuQ=="],
|
"valtio": ["valtio@2.1.8", "", { "dependencies": { "proxy-compare": "^3.0.1" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-fjTPbJyKEmfVBZUOh3V0OtMHoFUGr4+4XpejjxhNJE/IS2l8rDbyJuzi3w/fZWBDyk7BJOpG+lmvTK5iiVhXuQ=="],
|
||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
|
"verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="],
|
||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
@@ -462,14 +680,26 @@
|
|||||||
|
|
||||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
|
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"inquirer/lodash": ["lodash@3.10.1", "", {}, "sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ=="],
|
||||||
|
|
||||||
"nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"request/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"request/qs": ["qs@6.5.3", "", {}, "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA=="],
|
||||||
|
|
||||||
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
|
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
|
||||||
|
|
||||||
"@scalar/themes/@scalar/types/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
"@scalar/themes/@scalar/types/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
||||||
|
|
||||||
|
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"request/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1530
package-lock.json
generated
1530
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,8 @@
|
|||||||
"dev": "bun --hot src/index.tsx",
|
"dev": "bun --hot src/index.tsx",
|
||||||
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
|
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
|
||||||
"start": "NODE_ENV=production bun src/index.tsx",
|
"start": "NODE_ENV=production bun src/index.tsx",
|
||||||
"seed": "bun prisma/seed.ts"
|
"seed": "bun prisma/seed.ts",
|
||||||
|
"lint": "bunx oxlint src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/cors": "^1.4.0",
|
"@elysiajs/cors": "^1.4.0",
|
||||||
@@ -15,15 +16,19 @@
|
|||||||
"@elysiajs/jwt": "^1.4.0",
|
"@elysiajs/jwt": "^1.4.0",
|
||||||
"@elysiajs/swagger": "^1.3.1",
|
"@elysiajs/swagger": "^1.3.1",
|
||||||
"@mantine/core": "^8.3.3",
|
"@mantine/core": "^8.3.3",
|
||||||
|
"@mantine/dates": "^8.3.4",
|
||||||
|
"@mantine/form": "^8.3.4",
|
||||||
"@mantine/hooks": "^8.3.3",
|
"@mantine/hooks": "^8.3.3",
|
||||||
"@mantine/notifications": "^8.3.3",
|
"@mantine/notifications": "^8.3.3",
|
||||||
"@modelcontextprotocol/sdk": "^1.19.1",
|
"@modelcontextprotocol/sdk": "^1.19.1",
|
||||||
"@prisma/client": "^6.7.0",
|
"@prisma/client": "^6.7.0",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@types/jwt-decode": "^3.1.0",
|
"@types/jwt-decode": "^3.1.0",
|
||||||
|
"@types/lodash": "^4.17.20",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"elysia": "^1.4.9",
|
"elysia": "^1.4.9",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
"react-router-dom": "^7.9.3",
|
"react-router-dom": "^7.9.3",
|
||||||
@@ -34,6 +39,8 @@
|
|||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"biome": "^0.3.3",
|
||||||
|
"oxlint": "^1.22.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"postcss-preset-mantine": "^1.18.0",
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ model User {
|
|||||||
name String?
|
name String?
|
||||||
email String? @unique
|
email String? @unique
|
||||||
password String?
|
password String?
|
||||||
|
phone String? @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
ApiKey ApiKey[]
|
ApiKey ApiKey[]
|
||||||
|
|||||||
19
src/App.tsx
19
src/App.tsx
@@ -1,15 +1,16 @@
|
|||||||
|
import "@mantine/core/styles.css";
|
||||||
|
import "@mantine/notifications/styles.css";
|
||||||
|
import '@mantine/dates/styles.css'
|
||||||
|
import { Notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
import '@mantine/core/styles.css';
|
import { MantineProvider } from "@mantine/core";
|
||||||
import '@mantine/notifications/styles.css';
|
import AppRoutes from "./AppRoutes";
|
||||||
import { Notifications } from '@mantine/notifications';
|
|
||||||
|
|
||||||
import { MantineProvider } from '@mantine/core';
|
|
||||||
import AppRoutes from './AppRoutes';
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return <MantineProvider defaultColorScheme='dark'>
|
return (
|
||||||
|
<MantineProvider defaultColorScheme="dark">
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
</MantineProvider>;
|
</MantineProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,104 @@
|
|||||||
|
// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY
|
||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
import Home from "./pages/Home";
|
|
||||||
import NotFound from "./pages/NotFound";
|
|
||||||
import Login from "./pages/Login";
|
import Login from "./pages/Login";
|
||||||
import ProtectedRoute from "./components/ProtectedRoute";
|
import DarmasabaLayout from "./pages/darmasaba/darmasaba_layout";
|
||||||
import Dashboard from "./pages/dashboard/dashboard_page";
|
import FormSuratKeteranganUsaha from "./pages/darmasaba/form_surat_keterangan_usaha";
|
||||||
import DashboardLayout from "./pages/dashboard/dashboard_layout";
|
import FormSuratKeteranganTidakMampu from "./pages/darmasaba/form_surat_keterangan_tidak_mampu";
|
||||||
import ApiKeyPage from "./pages/dashboard/apikey/apikey_page";
|
import DarmasabaHome from "./pages/darmasaba/darmasaba_home";
|
||||||
import CredentialPage from "./pages/dashboard/credential/credential_page";
|
import FormKartuTandaPenduduk from "./pages/darmasaba/form_kartu_tanda_penduduk";
|
||||||
|
import FormKartuKeluarga from "./pages/darmasaba/form_kartu_keluarga";
|
||||||
|
import FormLaporanSampah from "./pages/darmasaba/form_laporan_sampah";
|
||||||
|
import FormSuratKeteranganPenghasilan from "./pages/darmasaba/form_surat_keterangan_penghasilan";
|
||||||
|
import FormSuratKeteranganDomisiliOrganisasi from "./pages/darmasaba/form_surat_keterangan_domisili_organisasi";
|
||||||
|
import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterangan_belum_kawin";
|
||||||
|
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
|
||||||
|
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
|
||||||
|
import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_keterangan_kelakuan_baik";
|
||||||
|
import Home from "./pages/Home";
|
||||||
|
import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
|
||||||
|
import DashboardHome from "./pages/scr/dashboard/dashboard_home";
|
||||||
|
import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page";
|
||||||
|
import DashboardLayout from "./pages/scr/dashboard/dashboard_layout";
|
||||||
|
import ScrLayout from "./pages/scr/scr_layout";
|
||||||
|
import NotFound from "./pages/NotFound";
|
||||||
|
|
||||||
export default function AppRoutes() {
|
export default function AppRoutes() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route element={<ProtectedRoute />}>
|
|
||||||
<Route path="/dashboard" element={<DashboardLayout />}>
|
|
||||||
<Route index element={<Dashboard />} />
|
|
||||||
<Route path="landing" element={<Dashboard />} />
|
|
||||||
<Route path="apikey" element={<ApiKeyPage />} />
|
|
||||||
<Route path="credential" element={<CredentialPage />} />
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="/darmasaba" element={<DarmasabaLayout />}>
|
||||||
|
<Route index element={<DarmasabaHome />} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/darmasaba/surat-keterangan-usaha"
|
||||||
|
element={<FormSuratKeteranganUsaha />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/darmasaba/surat-keterangan-tidak-mampu"
|
||||||
|
element={<FormSuratKeteranganTidakMampu />}
|
||||||
|
/>
|
||||||
|
<Route path="/darmasaba/darmasaba-home" element={<DarmasabaHome />} />
|
||||||
|
<Route
|
||||||
|
path="/darmasaba/kartu-tanda-penduduk"
|
||||||
|
element={<FormKartuTandaPenduduk />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/darmasaba/kartu-keluarga"
|
||||||
|
element={<FormKartuKeluarga />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/darmasaba/laporan-sampah"
|
||||||
|
element={<FormLaporanSampah />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/darmasaba/surat-keterangan-penghasilan"
|
||||||
|
element={<FormSuratKeteranganPenghasilan />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/darmasaba/surat-keterangan-domisili-organisasi"
|
||||||
|
element={<FormSuratKeteranganDomisiliOrganisasi />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/darmasaba/surat-keterangan-belum-kawin"
|
||||||
|
element={<FormSuratKeteranganBelumKawin />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/darmasaba/keterangan-kelahiran"
|
||||||
|
element={<FormKeteranganKelahiran />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/darmasaba/surat-keterangan-tempat-usaha"
|
||||||
|
element={<FormSuratKeteranganTempatUsaha />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/darmasaba/surat-keterangan-kelakuan-baik"
|
||||||
|
element={<FormSuratKeteranganKelakuanBaik />}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
|
||||||
|
<Route path="/scr" element={<ScrLayout />}>
|
||||||
|
<Route path="/scr/dashboard" element={<DashboardLayout />}>
|
||||||
|
<Route index element={<DashboardHome />} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/scr/dashboard/credential/credential"
|
||||||
|
element={<CredentialPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/scr/dashboard/dashboard-home"
|
||||||
|
element={<DashboardHome />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/scr/dashboard/apikey/apikey"
|
||||||
|
element={<ApikeyPage />}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
</Route>
|
||||||
|
<Route path="/*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
34
src/AppRoutes.txt
Normal file
34
src/AppRoutes.txt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
|
import Home from "./pages/Home";
|
||||||
|
import NotFound from "./pages/NotFound";
|
||||||
|
import Login from "./pages/Login";
|
||||||
|
import ProtectedRoute from "./components/ProtectedRoute";
|
||||||
|
import Dashboard from "./pages/dashboard/dashboard_page";
|
||||||
|
import DashboardLayout from "./pages/dashboard/dashboard_layout";
|
||||||
|
import ApiKeyPage from "./pages/dashboard/apikey/apikey_page";
|
||||||
|
import CredentialPage from "./pages/dashboard/credential/credential_page";
|
||||||
|
|
||||||
|
export default function AppRoutes() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route element={<ProtectedRoute />}>
|
||||||
|
<Route path="/dashboard" element={<DashboardLayout />}>
|
||||||
|
<Route index element={<Dashboard />} />
|
||||||
|
<Route path="landing" element={<Dashboard />} />
|
||||||
|
<Route path="apikey" element={<ApiKeyPage />} />
|
||||||
|
<Route path="credential" element={<CredentialPage />} />
|
||||||
|
</Route>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,11 +1,25 @@
|
|||||||
// AUTO-GENERATED FILE
|
// AUTO-GENERATED FILE
|
||||||
const clientRoutes = {
|
const clientRoutes = {
|
||||||
"/": "/",
|
|
||||||
"/login": "/login",
|
"/login": "/login",
|
||||||
"/dashboard": "/dashboard",
|
"/darmasaba": "/darmasaba",
|
||||||
"/dashboard/landing": "/dashboard/landing",
|
"/darmasaba/surat-keterangan-usaha": "/darmasaba/surat-keterangan-usaha",
|
||||||
"/dashboard/apikey": "/dashboard/apikey",
|
"/darmasaba/surat-keterangan-tidak-mampu": "/darmasaba/surat-keterangan-tidak-mampu",
|
||||||
"/dashboard/credential": "/dashboard/credential",
|
"/darmasaba/darmasaba-home": "/darmasaba/darmasaba-home",
|
||||||
|
"/darmasaba/kartu-tanda-penduduk": "/darmasaba/kartu-tanda-penduduk",
|
||||||
|
"/darmasaba/kartu-keluarga": "/darmasaba/kartu-keluarga",
|
||||||
|
"/darmasaba/laporan-sampah": "/darmasaba/laporan-sampah",
|
||||||
|
"/darmasaba/surat-keterangan-penghasilan": "/darmasaba/surat-keterangan-penghasilan",
|
||||||
|
"/darmasaba/surat-keterangan-domisili-organisasi": "/darmasaba/surat-keterangan-domisili-organisasi",
|
||||||
|
"/darmasaba/surat-keterangan-belum-kawin": "/darmasaba/surat-keterangan-belum-kawin",
|
||||||
|
"/darmasaba/keterangan-kelahiran": "/darmasaba/keterangan-kelahiran",
|
||||||
|
"/darmasaba/surat-keterangan-tempat-usaha": "/darmasaba/surat-keterangan-tempat-usaha",
|
||||||
|
"/darmasaba/surat-keterangan-kelakuan-baik": "/darmasaba/surat-keterangan-kelakuan-baik",
|
||||||
|
"/": "/",
|
||||||
|
"/scr": "/scr",
|
||||||
|
"/scr/dashboard": "/scr/dashboard",
|
||||||
|
"/scr/dashboard/credential/credential": "/scr/dashboard/credential/credential",
|
||||||
|
"/scr/dashboard/dashboard-home": "/scr/dashboard/dashboard-home",
|
||||||
|
"/scr/dashboard/apikey/apikey": "/scr/dashboard/apikey/apikey",
|
||||||
"/*": "/*"
|
"/*": "/*"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from "react";
|
||||||
import { Navigate, Outlet } from 'react-router-dom'
|
import { Navigate, Outlet } from "react-router-dom";
|
||||||
import clientRoutes from '@/clientRoutes'
|
import clientRoutes from "@/clientRoutes";
|
||||||
import apiFetch from '@/lib/apiFetch'
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
|
||||||
export default function ProtectedRoute() {
|
export default function ProtectedRoute() {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null)
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function checkSession() {
|
async function checkSession() {
|
||||||
try {
|
try {
|
||||||
// backend otomatis baca cookie JWT dari request
|
// backend otomatis baca cookie JWT dari request
|
||||||
const res = await apiFetch.api.user.find.get()
|
const res = await apiFetch.api.user.find.get();
|
||||||
setIsAuthenticated(res.status === 200)
|
setIsAuthenticated(res.status === 200);
|
||||||
} catch {
|
} catch {
|
||||||
setIsAuthenticated(false)
|
setIsAuthenticated(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkSession()
|
checkSession();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if (isAuthenticated === null) return null // or loading spinner
|
if (isAuthenticated === null) return null; // or loading spinner
|
||||||
if (!isAuthenticated) return <Navigate to={clientRoutes['/login']} replace />
|
if (!isAuthenticated) return <Navigate to={clientRoutes["/login"]} replace />;
|
||||||
return <Outlet />
|
return <Outlet />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
import Swagger from "@elysiajs/swagger";
|
import Swagger from "@elysiajs/swagger";
|
||||||
import Elysia from "elysia";
|
import Elysia from "elysia";
|
||||||
import type { User } from "generated/prisma";
|
|
||||||
import html from "./index.html";
|
import html from "./index.html";
|
||||||
import apiAuth from "./server/middlewares/apiAuth";
|
import apiAuth from "./server/middlewares/apiAuth";
|
||||||
import ApiKeyRoute from "./server/routes/apikey_route";
|
import ApiKeyRoute from "./server/routes/apikey_route";
|
||||||
@@ -9,55 +7,49 @@ import Auth from "./server/routes/auth_route";
|
|||||||
import CredentialRoute from "./server/routes/credential_route";
|
import CredentialRoute from "./server/routes/credential_route";
|
||||||
import DarmasabaRoute from "./server/routes/darmasaba_route";
|
import DarmasabaRoute from "./server/routes/darmasaba_route";
|
||||||
import { convertOpenApiToMcp } from "./server/lib/mcp-converter";
|
import { convertOpenApiToMcp } from "./server/lib/mcp-converter";
|
||||||
|
import UserRoute from "./server/routes/user_route";
|
||||||
|
import LayananRoute from "./server/routes/layanan_route";
|
||||||
|
|
||||||
const Docs = new Elysia()
|
const Docs = new Elysia({
|
||||||
.use(Swagger({
|
tags: ["docs"],
|
||||||
|
}).use(
|
||||||
|
Swagger({
|
||||||
path: "/docs",
|
path: "/docs",
|
||||||
}))
|
}),
|
||||||
|
);
|
||||||
const ApiUser = new Elysia({
|
|
||||||
prefix: "/user",
|
|
||||||
})
|
|
||||||
.get('/find', (ctx) => {
|
|
||||||
const { user } = ctx as any
|
|
||||||
return {
|
|
||||||
user: user as User
|
|
||||||
}
|
|
||||||
},{
|
|
||||||
detail: {
|
|
||||||
summary: "find",
|
|
||||||
description: "find user",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const Api = new Elysia({
|
const Api = new Elysia({
|
||||||
prefix: "/api",
|
prefix: "/api",
|
||||||
|
tags: ["api"],
|
||||||
})
|
})
|
||||||
.use(apiAuth)
|
.use(apiAuth)
|
||||||
.use(ApiKeyRoute)
|
.use(ApiKeyRoute)
|
||||||
.use(DarmasabaRoute)
|
.use(DarmasabaRoute)
|
||||||
.use(ApiUser)
|
|
||||||
.use(CredentialRoute)
|
.use(CredentialRoute)
|
||||||
|
.use(UserRoute)
|
||||||
|
.use(LayananRoute);
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
.use(Api)
|
.use(Api)
|
||||||
.use(Docs)
|
.use(Docs)
|
||||||
.use(Auth)
|
.use(Auth)
|
||||||
.get("/.well-known/mcp.json", async () => {
|
.get(
|
||||||
const baseUrl = process.env.BUN_PUBLIC_BASE_URL!
|
"/.well-known/mcp.json",
|
||||||
return await convertOpenApiToMcp(baseUrl)
|
async () => {
|
||||||
}, {
|
const baseUrl = process.env.BUN_PUBLIC_BASE_URL!;
|
||||||
|
return await convertOpenApiToMcp(baseUrl);
|
||||||
|
},
|
||||||
|
{
|
||||||
detail: {
|
detail: {
|
||||||
description: "MCP manifest",
|
description: "MCP manifest",
|
||||||
tags: ["MCP"],
|
tags: ["MCP"],
|
||||||
}
|
},
|
||||||
})
|
},
|
||||||
|
)
|
||||||
// .use(McpRoute)
|
// .use(McpRoute)
|
||||||
.get("*", html)
|
.get("*", html)
|
||||||
.listen(3000, () => {
|
.listen(3000, () => {
|
||||||
console.log("Server running at http://localhost:3000");
|
console.log("Server running at http://localhost:3000");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export type ServerApp = typeof app;
|
export type ServerApp = typeof app;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -6,4 +5,3 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +1,63 @@
|
|||||||
import { Button, Container, Group, Stack, Text, TextInput } from "@mantine/core";
|
import {
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
} from "@mantine/core";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import apiFetch from "../lib/apiFetch";
|
import apiFetch from "../lib/apiFetch";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState("");
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch.auth.login.post({
|
const response = await apiFetch.auth.login.post({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (response.data?.token) {
|
if (response.data?.token) {
|
||||||
localStorage.setItem('token', response.data.token)
|
localStorage.setItem("token", response.data.token);
|
||||||
window.location.href = '/dashboard'
|
window.location.href = "/dashboard";
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
alert(JSON.stringify(response.error))
|
alert(JSON.stringify(response.error));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>Login</Text>
|
<Text>Login</Text>
|
||||||
<TextInput placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
<TextInput
|
||||||
<TextInput placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
<Button onClick={handleSubmit} disabled={loading}>Login</Button>
|
<Button onClick={handleSubmit} disabled={loading}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -6,4 +5,3 @@ export default function NotFound() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
82
src/pages/darmasaba/darmasaba_home.tsx
Normal file
82
src/pages/darmasaba/darmasaba_home.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import clientRoutes from "@/clientRoutes";
|
||||||
|
import { Button, Container, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function DarmasabaPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<Container size={"md"} w={"100%"}>
|
||||||
|
<Stack>
|
||||||
|
<Text>Form Darmasaba</Text>
|
||||||
|
<SimpleGrid
|
||||||
|
cols={{
|
||||||
|
base: 1,
|
||||||
|
sm: 2,
|
||||||
|
md: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(clientRoutes["/darmasaba/kartu-keluarga"])}
|
||||||
|
>
|
||||||
|
Form KK
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(clientRoutes["/darmasaba/kartu-tanda-penduduk"])}
|
||||||
|
>
|
||||||
|
Form KTP
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(clientRoutes["/darmasaba/laporan-sampah"])}
|
||||||
|
>
|
||||||
|
Form Laporan Sampah
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(clientRoutes["/darmasaba/surat-keterangan-domisili-organisasi"])}
|
||||||
|
>
|
||||||
|
Form SKDO
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(clientRoutes["/darmasaba/surat-keterangan-penghasilan"])}
|
||||||
|
>
|
||||||
|
Form SKP
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(clientRoutes["/darmasaba/surat-keterangan-tidak-mampu"])}
|
||||||
|
>
|
||||||
|
Form SKTM
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(clientRoutes["/darmasaba/surat-keterangan-kelakuan-baik"])}
|
||||||
|
>
|
||||||
|
Form SKK
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(clientRoutes["/darmasaba/surat-keterangan-usaha"])}
|
||||||
|
>
|
||||||
|
Form SKU
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(clientRoutes["/darmasaba/surat-keterangan-tempat-usaha"])}
|
||||||
|
>
|
||||||
|
Form SKTU
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(clientRoutes["/darmasaba/surat-keterangan-belum-kawin"])}
|
||||||
|
>
|
||||||
|
Form Belum Kawin
|
||||||
|
</Button>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/pages/darmasaba/darmasaba_layout.tsx
Normal file
5
src/pages/darmasaba/darmasaba_layout.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function DarmasabaLayout() {
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
835
src/pages/darmasaba/form_kartu_keluarga.tsx
Normal file
835
src/pages/darmasaba/form_kartu_keluarga.tsx
Normal file
@@ -0,0 +1,835 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
ActionIcon,
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Modal,
|
||||||
|
Paper,
|
||||||
|
ScrollArea,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { DatePicker } from "@mantine/dates";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import {
|
||||||
|
IconCheck,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconPlus,
|
||||||
|
IconTrash,
|
||||||
|
IconX,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Types derived from provided JSON schema
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
|
type JenisPermohonan =
|
||||||
|
| "Baru"
|
||||||
|
| "Tambah Anggota Keluarga"
|
||||||
|
| "Pengurangan Anggota Keluarga"
|
||||||
|
| "Perubahan Data";
|
||||||
|
|
||||||
|
type JenisKelamin = "Laki-laki" | "Perempuan";
|
||||||
|
|
||||||
|
type Agama =
|
||||||
|
| "Islam"
|
||||||
|
| "Kristen"
|
||||||
|
| "Katolik"
|
||||||
|
| "Hindu"
|
||||||
|
| "Buddha"
|
||||||
|
| "Konghucu"
|
||||||
|
| "Lainnya";
|
||||||
|
|
||||||
|
type StatusHubungan =
|
||||||
|
| "Kepala Keluarga"
|
||||||
|
| "Istri"
|
||||||
|
| "Anak"
|
||||||
|
| "Orang Tua"
|
||||||
|
| "Famili Lain"
|
||||||
|
| "Lainnya";
|
||||||
|
|
||||||
|
type StatusPerkawinan = "Belum Kawin" | "Kawin" | "Cerai Hidup" | "Cerai Mati";
|
||||||
|
|
||||||
|
type Kewarganegaraan = "WNI" | "WNA";
|
||||||
|
|
||||||
|
interface AnggotaKeluargaItem {
|
||||||
|
no: number;
|
||||||
|
namaLengkap: string;
|
||||||
|
nik: string;
|
||||||
|
jenisKelamin: JenisKelamin | "";
|
||||||
|
tempatTanggalLahir: string;
|
||||||
|
agama: Agama | "";
|
||||||
|
pendidikan: string;
|
||||||
|
pekerjaan: string;
|
||||||
|
statusHubungan: StatusHubungan | "";
|
||||||
|
statusPerkawinan: StatusPerkawinan | "";
|
||||||
|
kewarganegaraan: Kewarganegaraan | "";
|
||||||
|
noPasporKitas?: string;
|
||||||
|
namaAyah?: string;
|
||||||
|
namaIbu?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KepalaKeluarga {
|
||||||
|
namaLengkap: string;
|
||||||
|
nik: string;
|
||||||
|
tempatTanggalLahir: string;
|
||||||
|
alamat: string;
|
||||||
|
rt: string;
|
||||||
|
rw: string;
|
||||||
|
desaKelurahan: string;
|
||||||
|
kecamatan: string;
|
||||||
|
kabupatenKota: string;
|
||||||
|
kodePos: string;
|
||||||
|
telepon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Pemohon {
|
||||||
|
nama: string;
|
||||||
|
tandaTangan: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Pengesahan {
|
||||||
|
kepalaDesaLurah: string;
|
||||||
|
camat: string;
|
||||||
|
petugasRegistrasi: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KKFormValues {
|
||||||
|
jenisPermohonan: JenisPermohonan | "";
|
||||||
|
kepalaKeluarga: KepalaKeluarga;
|
||||||
|
anggotaKeluarga: AnggotaKeluargaItem[];
|
||||||
|
pernyataanPemohon: string;
|
||||||
|
tanggalPengajuan: Date | null;
|
||||||
|
pemohon: Pemohon;
|
||||||
|
pengesahan: Pengesahan;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Reusable small components
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
|
function FieldLabel({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
label: React.ReactNode;
|
||||||
|
description?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Group justify="apart" style={{ width: "100%" }}>
|
||||||
|
<Group gap={6}>
|
||||||
|
<Text size="sm" fw={600}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
{description && (
|
||||||
|
<Tooltip label={description} withArrow>
|
||||||
|
<ActionIcon size="xs" variant="transparent">
|
||||||
|
<IconInfoCircle size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render a form field based on a simple schema mapping. This keeps the main component tidy.
|
||||||
|
function FormField(props: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
error?: string | null;
|
||||||
|
}) {
|
||||||
|
const { children, label, description, error } = props;
|
||||||
|
return (
|
||||||
|
<Stack gap={6} style={{ width: "100%" }}>
|
||||||
|
<FieldLabel label={label} description={description} />
|
||||||
|
{children}
|
||||||
|
{error ? (
|
||||||
|
<Text size="xs" color="red" aria-live="polite">
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Main Dynamic KK Form component
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
|
export default function DynamicKKForm() {
|
||||||
|
// Form initialization with sensible defaults — helps user with example values and keyboard navigation.
|
||||||
|
const form = useForm<KKFormValues>({
|
||||||
|
initialValues: {
|
||||||
|
jenisPermohonan: "",
|
||||||
|
kepalaKeluarga: {
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
tempatTanggalLahir: "",
|
||||||
|
alamat: "",
|
||||||
|
rt: "",
|
||||||
|
rw: "",
|
||||||
|
desaKelurahan: "",
|
||||||
|
kecamatan: "",
|
||||||
|
kabupatenKota: "",
|
||||||
|
kodePos: "",
|
||||||
|
telepon: "",
|
||||||
|
},
|
||||||
|
anggotaKeluarga: [
|
||||||
|
{
|
||||||
|
no: 1,
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
jenisKelamin: "",
|
||||||
|
tempatTanggalLahir: "",
|
||||||
|
agama: "",
|
||||||
|
pendidikan: "",
|
||||||
|
pekerjaan: "",
|
||||||
|
statusHubungan: "",
|
||||||
|
statusPerkawinan: "",
|
||||||
|
kewarganegaraan: "",
|
||||||
|
noPasporKitas: "",
|
||||||
|
namaAyah: "",
|
||||||
|
namaIbu: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pernyataanPemohon: "",
|
||||||
|
tanggalPengajuan: new Date(),
|
||||||
|
pemohon: { nama: "", tandaTangan: "" },
|
||||||
|
pengesahan: { kepalaDesaLurah: "", camat: "", petugasRegistrasi: "" },
|
||||||
|
},
|
||||||
|
|
||||||
|
validate: {
|
||||||
|
// Simple validation rules matching schema descriptions.
|
||||||
|
jenisPermohonan: (value) => (value ? null : "Pilih jenis permohonan"),
|
||||||
|
kepalaKeluarga: {
|
||||||
|
namaLengkap: (v) =>
|
||||||
|
v && v.length > 1 ? null : "Nama lengkap wajib diisi",
|
||||||
|
nik: (v) => (/^\d{16}$/.test(v) ? null : "NIK harus 16 digit angka"),
|
||||||
|
telepon: (v) =>
|
||||||
|
v && v.length >= 7 ? null : "Masukkan nomor telepon/HP yang valid",
|
||||||
|
},
|
||||||
|
pemohon: {
|
||||||
|
nama: (v) => (v ? null : "Nama pemohon harus diisi"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper: add a new anggota with next sequential no
|
||||||
|
function addAnggota() {
|
||||||
|
const nextNo = form.values.anggotaKeluarga.length + 1;
|
||||||
|
form.setFieldValue("anggotaKeluarga", [
|
||||||
|
...form.values.anggotaKeluarga,
|
||||||
|
{
|
||||||
|
no: nextNo,
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
jenisKelamin: "",
|
||||||
|
tempatTanggalLahir: "",
|
||||||
|
agama: "",
|
||||||
|
pendidikan: "",
|
||||||
|
pekerjaan: "",
|
||||||
|
statusHubungan: "",
|
||||||
|
statusPerkawinan: "",
|
||||||
|
kewarganegaraan: "",
|
||||||
|
noPasporKitas: "",
|
||||||
|
namaAyah: "",
|
||||||
|
namaIbu: "",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove anggota by index
|
||||||
|
function removeAnggota(index: number) {
|
||||||
|
const list = [...form.values.anggotaKeluarga];
|
||||||
|
list.splice(index, 1);
|
||||||
|
// re-number
|
||||||
|
const renumbered = list.map((a, i) => ({ ...a, no: i + 1 }));
|
||||||
|
form.setFieldValue("anggotaKeluarga", renumbered);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit handler — in production you'd call an API. Here we show console and a modal.
|
||||||
|
const [submitted, setSubmitted] = useState<any>(null);
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
|
||||||
|
function handleSubmit(values: KKFormValues) {
|
||||||
|
// sanitize & prepare payload
|
||||||
|
const payload = {
|
||||||
|
...values,
|
||||||
|
tanggalPengajuan:
|
||||||
|
values.tanggalPengajuan?.toISOString().slice(0, 10) ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("KK form submitted:", payload);
|
||||||
|
setSubmitted(payload);
|
||||||
|
setOpened(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="md" w="100%">
|
||||||
|
<Card shadow="md" radius="md" p="lg">
|
||||||
|
<Group justify="apart" align="flex-start">
|
||||||
|
<Group>
|
||||||
|
<Avatar color="blue" radius="xl">
|
||||||
|
KK
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<Title order={3}>Formulir Permohonan Kartu Keluarga (KK)</Title>
|
||||||
|
<Text size="sm" color="dimmed">
|
||||||
|
Blangko resmi untuk pengajuan Kartu Keluarga — pembuatan,
|
||||||
|
perubahan, atau penambahan/ pengurangan anggota keluarga.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Divider my="sm" />
|
||||||
|
|
||||||
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
|
<Stack gap="lg">
|
||||||
|
{/* Jenis Permohonan */}
|
||||||
|
<FormField
|
||||||
|
label="Jenis Permohonan"
|
||||||
|
description="Jenis permohonan pembuatan atau perubahan KK."
|
||||||
|
error={form.errors.jenisPermohonan as any}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
"Baru",
|
||||||
|
"Tambah Anggota Keluarga",
|
||||||
|
"Pengurangan Anggota Keluarga",
|
||||||
|
"Perubahan Data",
|
||||||
|
]}
|
||||||
|
placeholder="Pilih jenis permohonan"
|
||||||
|
{...form.getInputProps("jenisPermohonan")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
{/* Kepala Keluarga Section */}
|
||||||
|
<Accordion
|
||||||
|
variant="separated"
|
||||||
|
defaultValue="kepala"
|
||||||
|
chevronPosition="left"
|
||||||
|
>
|
||||||
|
<Accordion.Item value="kepala">
|
||||||
|
<Accordion.Control>
|
||||||
|
<Group justify="apart" style={{ width: "100%" }}>
|
||||||
|
<Group>
|
||||||
|
<Text fw={700}>Kepala Keluarga</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Data kepala keluarga sesuai KTP
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Accordion.Control>
|
||||||
|
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
label="Nama Lengkap"
|
||||||
|
description="Nama lengkap kepala keluarga sesuai KTP."
|
||||||
|
error={
|
||||||
|
(form.errors.kepalaKeluarga as any)
|
||||||
|
?.namaLengkap as any
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Contoh: Budi Santoso"
|
||||||
|
{...form.getInputProps("kepalaKeluarga.namaLengkap")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
label="NIK"
|
||||||
|
description="Nomor Induk Kependudukan (16 digit)."
|
||||||
|
error={(form.errors.kepalaKeluarga as any)?.nik as any}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="16 digit NIK"
|
||||||
|
{...form.getInputProps("kepalaKeluarga.nik")}
|
||||||
|
inputMode="numeric"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
label="Tempat & Tanggal Lahir"
|
||||||
|
description="Contoh: Denpasar, 1990-01-01"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Tempat, yyyy-mm-dd"
|
||||||
|
{...form.getInputProps(
|
||||||
|
"kepalaKeluarga.tempatTanggalLahir",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
label="Telepon"
|
||||||
|
description="Nomor HP yang bisa dihubungi"
|
||||||
|
error={
|
||||||
|
(form.errors.kepalaKeluarga as any)?.telepon as any
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="08xx-xxxx-xxxx"
|
||||||
|
{...form.getInputProps("kepalaKeluarga.telepon")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<FormField
|
||||||
|
label="Alamat Lengkap"
|
||||||
|
description="Sesuai domisili"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Alamat lengkap"
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("kepalaKeluarga.alamat")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<FormField label="RT" description="Nomor RT">
|
||||||
|
<TextInput
|
||||||
|
placeholder="001"
|
||||||
|
{...form.getInputProps("kepalaKeluarga.rt")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<FormField label="RW" description="Nomor RW">
|
||||||
|
<TextInput
|
||||||
|
placeholder="002"
|
||||||
|
{...form.getInputProps("kepalaKeluarga.rw")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<FormField
|
||||||
|
label="Kode Pos"
|
||||||
|
description="Kode pos wilayah"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="80361"
|
||||||
|
{...form.getInputProps("kepalaKeluarga.kodePos")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<FormField
|
||||||
|
label="Desa / Kelurahan"
|
||||||
|
description="Nama desa atau kelurahan"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Contoh: Kuta"
|
||||||
|
{...form.getInputProps(
|
||||||
|
"kepalaKeluarga.desaKelurahan",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField label="Kecamatan" description="Nama kecamatan">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Contoh: Kuta"
|
||||||
|
{...form.getInputProps("kepalaKeluarga.kecamatan")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
label="Kabupaten / Kota"
|
||||||
|
description="Nama kabupaten atau kota"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Contoh: Badung"
|
||||||
|
{...form.getInputProps(
|
||||||
|
"kepalaKeluarga.kabupatenKota",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
{/* Anggota Keluarga (array) */}
|
||||||
|
<Card withBorder radius="md" p="md">
|
||||||
|
<Group justify="apart" mb="sm">
|
||||||
|
<Group>
|
||||||
|
<Text fw={700}>Anggota Keluarga</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Daftar anggota keluarga dalam KK
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
leftSection={<IconPlus size={14} />}
|
||||||
|
onClick={addAnggota}
|
||||||
|
aria-label="Tambah anggota"
|
||||||
|
>
|
||||||
|
Tambah Anggota
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack gap="sm">
|
||||||
|
{form.values.anggotaKeluarga.map((anggota, idx) => (
|
||||||
|
<Paper key={idx} withBorder radius="md" p="md">
|
||||||
|
<Grid align="center">
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Group justify="apart">
|
||||||
|
<Group>
|
||||||
|
<Badge>{`#${anggota.no}`}</Badge>
|
||||||
|
<Text fw={600} size="sm">
|
||||||
|
{anggota.namaLengkap || "(Belum diisi)"}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<ActionIcon
|
||||||
|
color="red"
|
||||||
|
onClick={() => removeAnggota(idx)}
|
||||||
|
aria-label={`Hapus anggota ${idx + 1}`}
|
||||||
|
>
|
||||||
|
<IconTrash />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField
|
||||||
|
label="Nama Lengkap"
|
||||||
|
description="Nama lengkap anggota keluarga"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Contoh: Siti"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.namaLengkap`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="NIK" description="16 digit NIK">
|
||||||
|
<TextInput
|
||||||
|
placeholder="NIK"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.nik`,
|
||||||
|
)}
|
||||||
|
inputMode="numeric"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Jenis Kelamin">
|
||||||
|
<Select
|
||||||
|
data={["Laki-laki", "Perempuan"]}
|
||||||
|
placeholder="Pilih"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.jenisKelamin`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Tempat & Tanggal Lahir">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Contoh: Denpasar, 1995-03-12"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.tempatTanggalLahir`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Agama">
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
"Islam",
|
||||||
|
"Kristen",
|
||||||
|
"Katolik",
|
||||||
|
"Hindu",
|
||||||
|
"Buddha",
|
||||||
|
"Konghucu",
|
||||||
|
"Lainnya",
|
||||||
|
]}
|
||||||
|
placeholder="Pilih"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.agama`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Pendidikan">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Pendidikan terakhir"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.pendidikan`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Pekerjaan">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Pekerjaan"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.pekerjaan`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Status Hubungan">
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
"Kepala Keluarga",
|
||||||
|
"Istri",
|
||||||
|
"Anak",
|
||||||
|
"Orang Tua",
|
||||||
|
"Famili Lain",
|
||||||
|
"Lainnya",
|
||||||
|
]}
|
||||||
|
placeholder="Pilih"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.statusHubungan`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Status Perkawinan">
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
"Belum Kawin",
|
||||||
|
"Kawin",
|
||||||
|
"Cerai Hidup",
|
||||||
|
"Cerai Mati",
|
||||||
|
]}
|
||||||
|
placeholder="Pilih"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.statusPerkawinan`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Kewarganegaraan">
|
||||||
|
<Select
|
||||||
|
data={["WNI", "WNA"]}
|
||||||
|
placeholder="Pilih"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.kewarganegaraan`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="No Paspor / KITAS">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Jika ada"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.noPasporKitas`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Nama Ayah">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama ayah"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.namaAyah`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Nama Ibu">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama ibu"
|
||||||
|
{...form.getInputProps(
|
||||||
|
`anggotaKeluarga.${idx}.namaIbu`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Pernyataan Pemohon, Tanggal, Pemohon, Pengesahan */}
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<FormField
|
||||||
|
label="Pernyataan Pemohon"
|
||||||
|
description="Pernyataan kebenaran data oleh pemohon."
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Saya menyatakan bahwa data yang saya berikan adalah benar..."
|
||||||
|
minRows={3}
|
||||||
|
{...form.getInputProps("pernyataanPemohon")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField
|
||||||
|
label="Tanggal Pengajuan"
|
||||||
|
description="Tanggal pengajuan formulir"
|
||||||
|
>
|
||||||
|
<DatePicker {...form.getInputProps("tanggalPengajuan")} />
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<Card withBorder radius="md" p="sm">
|
||||||
|
<Text fw={700} size="sm" mb="xs">
|
||||||
|
Data Pemohon
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<FormField label="Nama Pemohon">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama lengkap"
|
||||||
|
{...form.getInputProps("pemohon.nama")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField
|
||||||
|
label="Tanda Tangan (scan)"
|
||||||
|
description="Unggah file scan tanda tangan jika ada"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama file / URL"
|
||||||
|
{...form.getInputProps("pemohon.tandaTangan")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Card withBorder radius="md" p="sm">
|
||||||
|
<Text fw={700} size="sm" mb="xs">
|
||||||
|
Pengesahan
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Kepala Desa / Lurah">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama"
|
||||||
|
{...form.getInputProps("pengesahan.kepalaDesaLurah")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Camat">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama"
|
||||||
|
{...form.getInputProps("pengesahan.camat")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField label="Petugas Registrasi">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama"
|
||||||
|
{...form.getInputProps(
|
||||||
|
"pengesahan.petugasRegistrasi",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Submit / Reset actions */}
|
||||||
|
<Group justify="flex-end" mt="sm">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={() => form.reset()}
|
||||||
|
leftSection={<IconX />}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" leftSection={<IconCheck />}>
|
||||||
|
Kirim Permohonan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={() => setOpened(false)}
|
||||||
|
title="Preview Payload"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<ScrollArea style={{ height: 400 }}>
|
||||||
|
<pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
|
||||||
|
{JSON.stringify(submitted, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</ScrollArea>
|
||||||
|
</Modal>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
641
src/pages/darmasaba/form_kartu_tanda_penduduk.tsx
Normal file
641
src/pages/darmasaba/form_kartu_tanda_penduduk.tsx
Normal file
@@ -0,0 +1,641 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
ActionIcon,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
FileButton,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
TextInput,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { DatePicker } from "@mantine/dates";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import {
|
||||||
|
IconBuildingBank,
|
||||||
|
IconCalendar,
|
||||||
|
IconCheck,
|
||||||
|
IconId,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconUpload,
|
||||||
|
IconUser,
|
||||||
|
IconX,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Types - strong typing for schema-driven form
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
type EnumField = {
|
||||||
|
type: "enum";
|
||||||
|
options: string[];
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PrimitiveField = {
|
||||||
|
type: "string" | "number" | "boolean";
|
||||||
|
format?: string; // e.g. date
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ObjectField = {
|
||||||
|
[key: string]: PrimitiveField | EnumField;
|
||||||
|
};
|
||||||
|
|
||||||
|
type KTPSchema = {
|
||||||
|
formTitle: string;
|
||||||
|
description?: string;
|
||||||
|
jenisPermohonan: EnumField;
|
||||||
|
dataPemohon: ObjectField;
|
||||||
|
pernyataanPemohon: PrimitiveField;
|
||||||
|
tanggalPengajuan: PrimitiveField & { format?: string };
|
||||||
|
pengesahan: ObjectField;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Helper: convert file to base64 (used for foto/tandaTangan/sidikJari)
|
||||||
|
// ---------------------------
|
||||||
|
async function fileToBase64(file: File | null): Promise<string | null> {
|
||||||
|
if (!file) return null;
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(String(reader.result));
|
||||||
|
reader.onerror = (err) => reject(err);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Reusable small components
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
function FieldLabel({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
label: React.ReactNode;
|
||||||
|
description?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Group justify="apart" style={{ width: "100%" }}>
|
||||||
|
<Group gap={6}>
|
||||||
|
<Text size="sm" fw={600}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
{description && (
|
||||||
|
<ActionIcon size={18} variant="subtle" aria-hidden>
|
||||||
|
<IconInfoCircle size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
{description && (
|
||||||
|
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Main Form Component
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
const schema: KTPSchema = {
|
||||||
|
formTitle: "Formulir Permohonan Kartu Tanda Penduduk (KTP)",
|
||||||
|
description:
|
||||||
|
"Blangko resmi untuk pengajuan KTP elektronik (e-KTP). Digunakan untuk pembuatan KTP baru, penggantian karena hilang/rusak, atau perubahan data.",
|
||||||
|
jenisPermohonan: {
|
||||||
|
type: "enum",
|
||||||
|
options: [
|
||||||
|
"Baru",
|
||||||
|
"Perpanjangan",
|
||||||
|
"Penggantian Hilang",
|
||||||
|
"Penggantian Rusak",
|
||||||
|
"Perubahan Data",
|
||||||
|
],
|
||||||
|
description: "Jenis permohonan pembuatan atau perubahan KTP.",
|
||||||
|
},
|
||||||
|
dataPemohon: {
|
||||||
|
namaLengkap: {
|
||||||
|
type: "string",
|
||||||
|
description: "Nama lengkap sesuai akta kelahiran.",
|
||||||
|
},
|
||||||
|
nik: {
|
||||||
|
type: "string",
|
||||||
|
description: "Nomor Induk Kependudukan (16 digit).",
|
||||||
|
},
|
||||||
|
jenisKelamin: {
|
||||||
|
type: "enum",
|
||||||
|
options: ["Laki-laki", "Perempuan"],
|
||||||
|
description: "Jenis kelamin pemohon.",
|
||||||
|
} as any,
|
||||||
|
tempatTanggalLahir: {
|
||||||
|
type: "string",
|
||||||
|
description: "Tempat dan tanggal lahir pemohon.",
|
||||||
|
},
|
||||||
|
golonganDarah: {
|
||||||
|
type: "enum",
|
||||||
|
options: ["A", "B", "AB", "O", "Tidak Tahu"],
|
||||||
|
description: "Golongan darah pemohon.",
|
||||||
|
} as any,
|
||||||
|
alamat: { type: "string", description: "Alamat lengkap domisili." },
|
||||||
|
rt: { type: "string", description: "Nomor RT." },
|
||||||
|
rw: { type: "string", description: "Nomor RW." },
|
||||||
|
desaKelurahan: {
|
||||||
|
type: "string",
|
||||||
|
description: "Nama desa atau kelurahan tempat tinggal.",
|
||||||
|
},
|
||||||
|
kecamatan: {
|
||||||
|
type: "string",
|
||||||
|
description: "Nama kecamatan tempat tinggal.",
|
||||||
|
},
|
||||||
|
kabupatenKota: { type: "string", description: "Nama kabupaten atau kota." },
|
||||||
|
agama: {
|
||||||
|
type: "enum",
|
||||||
|
options: [
|
||||||
|
"Islam",
|
||||||
|
"Kristen",
|
||||||
|
"Katolik",
|
||||||
|
"Hindu",
|
||||||
|
"Buddha",
|
||||||
|
"Konghucu",
|
||||||
|
"Lainnya",
|
||||||
|
],
|
||||||
|
description: "Agama pemohon.",
|
||||||
|
} as any,
|
||||||
|
statusPerkawinan: {
|
||||||
|
type: "enum",
|
||||||
|
options: ["Belum Kawin", "Kawin", "Cerai Hidup", "Cerai Mati"],
|
||||||
|
description: "Status perkawinan pemohon.",
|
||||||
|
} as any,
|
||||||
|
pekerjaan: { type: "string", description: "Jenis pekerjaan pemohon." },
|
||||||
|
kewarganegaraan: {
|
||||||
|
type: "enum",
|
||||||
|
options: ["WNI", "WNA"],
|
||||||
|
description: "Kewarganegaraan pemohon.",
|
||||||
|
} as any,
|
||||||
|
foto: {
|
||||||
|
type: "string",
|
||||||
|
description: "File foto pemohon ukuran 4x6 (upload path/base64).",
|
||||||
|
},
|
||||||
|
tandaTangan: {
|
||||||
|
type: "string",
|
||||||
|
description: "Tanda tangan digital pemohon (upload path/base64).",
|
||||||
|
},
|
||||||
|
sidikJari: {
|
||||||
|
type: "string",
|
||||||
|
description: "Hasil rekam sidik jari pemohon (scan/file).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pernyataanPemohon: {
|
||||||
|
type: "string",
|
||||||
|
description: "Pernyataan bahwa data yang diberikan benar dan sah.",
|
||||||
|
},
|
||||||
|
tanggalPengajuan: {
|
||||||
|
type: "string",
|
||||||
|
format: "date",
|
||||||
|
description: "Tanggal pengajuan formulir.",
|
||||||
|
},
|
||||||
|
pengesahan: {
|
||||||
|
petugasRegistrasi: {
|
||||||
|
type: "string",
|
||||||
|
description: "Nama petugas registrasi kependudukan yang memproses.",
|
||||||
|
},
|
||||||
|
kepalaDinas: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"Nama Kepala Dinas Kependudukan dan Catatan Sipil yang mengesahkan.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FormKartuTandaPenduduk() {
|
||||||
|
// Initial values - sensible defaults / smart placeholders
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
jenisPermohonan: schema.jenisPermohonan.options[0],
|
||||||
|
// dataPemohon
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
jenisKelamin: "",
|
||||||
|
tempatTanggalLahir: "",
|
||||||
|
golonganDarah: "",
|
||||||
|
alamat: "",
|
||||||
|
rt: "",
|
||||||
|
rw: "",
|
||||||
|
desaKelurahan: "",
|
||||||
|
kecamatan: "",
|
||||||
|
kabupatenKota: "",
|
||||||
|
agama: "",
|
||||||
|
statusPerkawinan: "",
|
||||||
|
pekerjaan: "",
|
||||||
|
kewarganegaraan: "WNI",
|
||||||
|
foto: null as string | null,
|
||||||
|
tandaTangan: null as string | null,
|
||||||
|
sidikJari: null as string | null,
|
||||||
|
pernyataanPemohon:
|
||||||
|
"Saya menyatakan data yang saya berikan adalah benar dan sah.",
|
||||||
|
tanggalPengajuan: null as Date | null,
|
||||||
|
petugasRegistrasi: "",
|
||||||
|
kepalaDinas: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
validate: {
|
||||||
|
namaLengkap: (value) => (!value ? "Nama lengkap harus diisi" : null),
|
||||||
|
nik: (value) => {
|
||||||
|
if (!value) return "NIK harus diisi";
|
||||||
|
const digits = value.replace(/\D/g, "");
|
||||||
|
if (digits.length !== 16) return "NIK harus 16 digit";
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
jenisKelamin: (v) => (!v ? "Pilih jenis kelamin" : null),
|
||||||
|
alamat: (v) => (!v ? "Alamat harus diisi" : null),
|
||||||
|
pernyataanPemohon: (v) =>
|
||||||
|
!v || v.length < 10
|
||||||
|
? "Pernyataan harus diisi minimal 10 karakter"
|
||||||
|
: null,
|
||||||
|
tanggalPengajuan: (v) => (!v ? "Pilih tanggal pengajuan" : null),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// local UI state for file upload previews
|
||||||
|
const [fotoName, setFotoName] = useState<string | null>(null);
|
||||||
|
const [ttdName, setTtdName] = useState<string | null>(null);
|
||||||
|
const [sidikName, setSidikName] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// submit handler - in real app this would call an API
|
||||||
|
const handleSubmit = async (values: typeof form.values) => {
|
||||||
|
// For demo: convert any stored File objects into base64 is handled at selection time.
|
||||||
|
// Compose payload
|
||||||
|
const payload = {
|
||||||
|
...values,
|
||||||
|
tanggalPengajuan: values.tanggalPengajuan
|
||||||
|
? values.tanggalPengajuan.toISOString().slice(0, 10)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Here you'd normally POST to server
|
||||||
|
// We'll just console.log and show success
|
||||||
|
console.log("Submitting KTP form:", payload);
|
||||||
|
alert("Form submitted — cek console (development).\nNIK: " + values.nik);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="md" w={"100%"}>
|
||||||
|
<Card shadow="sm" radius="md" p="xl">
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group justify="apart">
|
||||||
|
<Group>
|
||||||
|
<IconBuildingBank size={28} />
|
||||||
|
<div>
|
||||||
|
<Text fw={700} size="lg">
|
||||||
|
{schema.formTitle}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{schema.description}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(async (values) => {
|
||||||
|
await handleSubmit(values);
|
||||||
|
form.reset();
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Stack gap="lg">
|
||||||
|
{/* Jenis Permohonan */}
|
||||||
|
<Card withBorder p="md">
|
||||||
|
<FieldLabel
|
||||||
|
label={<span>Jenis Permohonan</span>}
|
||||||
|
description={schema.jenisPermohonan.description}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
mt="sm"
|
||||||
|
data={schema.jenisPermohonan.options}
|
||||||
|
placeholder="Pilih jenis permohonan"
|
||||||
|
{...form.getInputProps("jenisPermohonan")}
|
||||||
|
leftSection={<IconId size={16} />}
|
||||||
|
aria-label="jenis permohonan"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Data Pemohon - collapsible */}
|
||||||
|
<Accordion variant="separated" defaultValue="dataPemohon">
|
||||||
|
<Accordion.Item value="dataPemohon">
|
||||||
|
<Accordion.Control icon={<IconUser size={16} />}>
|
||||||
|
Data Pemohon
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Nama Lengkap"
|
||||||
|
placeholder="Contoh: Budi Santoso"
|
||||||
|
description={
|
||||||
|
schema.dataPemohon?.namaLengkap?.description
|
||||||
|
}
|
||||||
|
{...form.getInputProps("namaLengkap")}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="NIK"
|
||||||
|
placeholder="16 digit NIK"
|
||||||
|
description={schema.dataPemohon?.nik?.description}
|
||||||
|
{...form.getInputProps("nik")}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Select
|
||||||
|
label="Jenis Kelamin"
|
||||||
|
placeholder="Pilih..."
|
||||||
|
data={["Laki-laki", "Perempuan"]}
|
||||||
|
description={
|
||||||
|
schema.dataPemohon?.jenisKelamin?.description
|
||||||
|
}
|
||||||
|
{...form.getInputProps("jenisKelamin")}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Tempat, Tanggal Lahir"
|
||||||
|
placeholder="Contoh: Denpasar, 01 Januari 1990"
|
||||||
|
description={
|
||||||
|
schema.dataPemohon?.tempatTanggalLahir?.description
|
||||||
|
}
|
||||||
|
{...form.getInputProps("tempatTanggalLahir")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Select
|
||||||
|
label="Golongan Darah"
|
||||||
|
placeholder="Pilih..."
|
||||||
|
data={["A", "B", "AB", "O", "Tidak Tahu"]}
|
||||||
|
description={
|
||||||
|
schema.dataPemohon?.golonganDarah?.description
|
||||||
|
}
|
||||||
|
{...form.getInputProps("golonganDarah")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Textarea
|
||||||
|
label="Alamat"
|
||||||
|
placeholder="Alamat lengkap domisili"
|
||||||
|
description={schema.dataPemohon?.alamat?.description}
|
||||||
|
autosize
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("alamat")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<TextInput label="RT" {...form.getInputProps("rt")} />
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<TextInput label="RW" {...form.getInputProps("rw")} />
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Desa / Kelurahan"
|
||||||
|
{...form.getInputProps("desaKelurahan")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Kecamatan"
|
||||||
|
{...form.getInputProps("kecamatan")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Kabupaten / Kota"
|
||||||
|
{...form.getInputProps("kabupatenKota")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Select
|
||||||
|
label="Agama"
|
||||||
|
data={[
|
||||||
|
"Islam",
|
||||||
|
"Kristen",
|
||||||
|
"Katolik",
|
||||||
|
"Hindu",
|
||||||
|
"Buddha",
|
||||||
|
"Konghucu",
|
||||||
|
"Lainnya",
|
||||||
|
]}
|
||||||
|
{...form.getInputProps("agama")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Select
|
||||||
|
label="Status Perkawinan"
|
||||||
|
data={[
|
||||||
|
"Belum Kawin",
|
||||||
|
"Kawin",
|
||||||
|
"Cerai Hidup",
|
||||||
|
"Cerai Mati",
|
||||||
|
]}
|
||||||
|
{...form.getInputProps("statusPerkawinan")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Pekerjaan"
|
||||||
|
{...form.getInputProps("pekerjaan")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Select
|
||||||
|
label="Kewarganegaraan"
|
||||||
|
data={["WNI", "WNA"]}
|
||||||
|
{...form.getInputProps("kewarganegaraan")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* Uploads: foto, tanda tangan, sidik jari */}
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FieldLabel
|
||||||
|
label={<span>Foto (4x6)</span>}
|
||||||
|
description={schema.dataPemohon?.foto?.description}
|
||||||
|
/>
|
||||||
|
<FileButton
|
||||||
|
onChange={async (file) => {
|
||||||
|
if (!file) return;
|
||||||
|
const base64 = await fileToBase64(file);
|
||||||
|
form.setFieldValue("foto", base64);
|
||||||
|
setFotoName(file.name);
|
||||||
|
}}
|
||||||
|
accept="image/*"
|
||||||
|
>
|
||||||
|
{(props) => (
|
||||||
|
<Button
|
||||||
|
leftSection={<IconUpload size={16} />}
|
||||||
|
{...props}
|
||||||
|
mt="sm"
|
||||||
|
>
|
||||||
|
{fotoName || "Upload Foto"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FieldLabel
|
||||||
|
label={<span>Tanda Tangan</span>}
|
||||||
|
description={
|
||||||
|
schema.dataPemohon?.tandaTangan?.description
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FileButton
|
||||||
|
onChange={async (file) => {
|
||||||
|
if (!file) return;
|
||||||
|
const base64 = await fileToBase64(file);
|
||||||
|
form.setFieldValue("tandaTangan", base64);
|
||||||
|
setTtdName(file.name);
|
||||||
|
}}
|
||||||
|
accept="image/*"
|
||||||
|
>
|
||||||
|
{(props) => (
|
||||||
|
<Button
|
||||||
|
leftSection={<IconUpload size={16} />}
|
||||||
|
{...props}
|
||||||
|
mt="sm"
|
||||||
|
>
|
||||||
|
{ttdName || "Upload TTD"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FieldLabel
|
||||||
|
label={<span>Sidik Jari</span>}
|
||||||
|
description={
|
||||||
|
schema.dataPemohon?.sidikJari?.description
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FileButton
|
||||||
|
onChange={async (file) => {
|
||||||
|
if (!file) return;
|
||||||
|
const base64 = await fileToBase64(file);
|
||||||
|
form.setFieldValue("sidikJari", base64);
|
||||||
|
setSidikName(file.name);
|
||||||
|
}}
|
||||||
|
accept="image/*,application/pdf"
|
||||||
|
>
|
||||||
|
{(props) => (
|
||||||
|
<Button
|
||||||
|
leftSection={<IconUpload size={16} />}
|
||||||
|
{...props}
|
||||||
|
mt="sm"
|
||||||
|
>
|
||||||
|
{sidikName || "Upload Sidik Jari"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
{/* Pernyataan Pemohon */}
|
||||||
|
<Accordion.Item value="pernyataanPemohon">
|
||||||
|
<Accordion.Control icon={<IconInfoCircle size={16} />}>
|
||||||
|
Pernyataan Pemohon
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Textarea
|
||||||
|
label="Pernyataan"
|
||||||
|
autosize
|
||||||
|
minRows={3}
|
||||||
|
{...form.getInputProps("pernyataanPemohon")}
|
||||||
|
/>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
{/* Tanggal Pengajuan */}
|
||||||
|
<Accordion.Item value="tanggal">
|
||||||
|
<Accordion.Control icon={<IconCalendar size={16} />}>
|
||||||
|
Tanggal Pengajuan
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<DatePicker {...form.getInputProps("tanggalPengajuan")} />
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
{/* Pengesahan */}
|
||||||
|
<Accordion.Item value="pengesahan">
|
||||||
|
<Accordion.Control icon={<IconBuildingBank size={16} />}>
|
||||||
|
Pengesahan
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Petugas Registrasi"
|
||||||
|
{...form.getInputProps("petugasRegistrasi")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Kepala Dinas"
|
||||||
|
{...form.getInputProps("kepalaDinas")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Group justify="right" gap="sm">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={() => form.reset()}
|
||||||
|
leftSection={<IconX size={16} />}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" leftSection={<IconCheck size={16} />}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Tip: Semua input penting memiliki validasi inline. NIK harus 16
|
||||||
|
digit.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
945
src/pages/darmasaba/form_keterangan_kelahiran.tsx
Normal file
945
src/pages/darmasaba/form_keterangan_kelahiran.tsx
Normal file
@@ -0,0 +1,945 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
ActionIcon,
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
NumberInput,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import {
|
||||||
|
IconCheck,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconPlus,
|
||||||
|
IconTrash,
|
||||||
|
IconUser,
|
||||||
|
IconX,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import React from "react";
|
||||||
|
// Date/Time pickers live in @mantine/dates
|
||||||
|
import { DatePicker, TimeInput } from "@mantine/dates";
|
||||||
|
|
||||||
|
/* ----------------------------- Types ----------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strongly-typed form shape inferred from provided JSON schema.
|
||||||
|
* Keep aligned with the JSON schema structure.
|
||||||
|
*/
|
||||||
|
type SaksiItem = {
|
||||||
|
namaLengkap: string;
|
||||||
|
nik: string;
|
||||||
|
alamat: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Pengesahan = {
|
||||||
|
kepalaDesaLurah?: string;
|
||||||
|
camat?: string;
|
||||||
|
petugasRegistrasi?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BirthFormValues = {
|
||||||
|
// dataBayi
|
||||||
|
dataBayi: {
|
||||||
|
namaLengkap?: string;
|
||||||
|
jenisKelamin?: string;
|
||||||
|
tempatLahir?: string;
|
||||||
|
tanggalLahir?: Date | null;
|
||||||
|
jamLahir?: Date | null;
|
||||||
|
beratBadan?: number | null;
|
||||||
|
panjangBadan?: number | null;
|
||||||
|
};
|
||||||
|
// dataIbu
|
||||||
|
dataIbu: {
|
||||||
|
namaLengkap?: string;
|
||||||
|
nik?: string;
|
||||||
|
tempatTanggalLahir?: string;
|
||||||
|
pekerjaan?: string;
|
||||||
|
alamat?: string;
|
||||||
|
};
|
||||||
|
// dataAyah
|
||||||
|
dataAyah: {
|
||||||
|
namaLengkap?: string;
|
||||||
|
nik?: string;
|
||||||
|
tempatTanggalLahir?: string;
|
||||||
|
pekerjaan?: string;
|
||||||
|
alamat?: string;
|
||||||
|
};
|
||||||
|
// dataPelapor
|
||||||
|
dataPelapor: {
|
||||||
|
namaLengkap?: string;
|
||||||
|
nik?: string;
|
||||||
|
hubunganDenganBayi?: string;
|
||||||
|
alamat?: string;
|
||||||
|
};
|
||||||
|
// saksi: array
|
||||||
|
saksi: SaksiItem[];
|
||||||
|
// other
|
||||||
|
keteranganTambahan?: string;
|
||||||
|
tanggalPelaporan?: Date | null;
|
||||||
|
pengesahan: Pengesahan;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ------------------------- Reusable Components ------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FormField: wraps label, description (helper), input control and error UI.
|
||||||
|
* Keeps consistent spacing/typography and supports left-side icon tooltips when needed.
|
||||||
|
*/
|
||||||
|
function FormField({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
children,
|
||||||
|
required = false,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
required?: boolean;
|
||||||
|
id?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Stack gap="xs" style={{ width: "100%" }}>
|
||||||
|
<Group justify="apart" gap="xs" align="center" style={{ width: "100%" }}>
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Text fw={600} size="sm" component="label" htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
{description ? (
|
||||||
|
<Tooltip label={description} withArrow>
|
||||||
|
<ActionIcon size="sm" aria-label={`${label} info`}>
|
||||||
|
<IconInfoCircle size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
{required ? <Badge c="red">Wajib</Badge> : null}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Box>{children}</Box>
|
||||||
|
|
||||||
|
{description ? (
|
||||||
|
<Text size="xs" color="dimmed" mt="xs">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FormSection: card with optional accordion/collapse for nested object grouping.
|
||||||
|
*/
|
||||||
|
function FormSection({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card shadow="sm" radius="md" p="md" withBorder>
|
||||||
|
<Accordion variant="separated">
|
||||||
|
<Accordion.Item value="open">
|
||||||
|
<Accordion.Control>
|
||||||
|
<Group justify="apart" align="center" gap="md">
|
||||||
|
<Group gap="sm" align="center">
|
||||||
|
{icon}
|
||||||
|
<div>
|
||||||
|
<Text fw={700}>{title}</Text>
|
||||||
|
{subtitle ? (
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Accordion.Control>
|
||||||
|
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Stack gap="md">{children}</Stack>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------- Main Component --------------------------- */
|
||||||
|
|
||||||
|
export default function FormKeteranganKelahiran() {
|
||||||
|
// Setup form with sensible defaults (smart defaults: empty strings, null dates)
|
||||||
|
const form = useForm<BirthFormValues>({
|
||||||
|
initialValues: {
|
||||||
|
dataBayi: {
|
||||||
|
namaLengkap: "",
|
||||||
|
jenisKelamin: undefined,
|
||||||
|
tempatLahir: "",
|
||||||
|
tanggalLahir: null,
|
||||||
|
jamLahir: null,
|
||||||
|
beratBadan: null,
|
||||||
|
panjangBadan: null,
|
||||||
|
},
|
||||||
|
dataIbu: {
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
tempatTanggalLahir: "",
|
||||||
|
pekerjaan: "",
|
||||||
|
alamat: "",
|
||||||
|
},
|
||||||
|
dataAyah: {
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
tempatTanggalLahir: "",
|
||||||
|
pekerjaan: "",
|
||||||
|
alamat: "",
|
||||||
|
},
|
||||||
|
dataPelapor: {
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
hubunganDenganBayi: "",
|
||||||
|
alamat: "",
|
||||||
|
},
|
||||||
|
saksi: [
|
||||||
|
// start with one empty witness to guide user
|
||||||
|
{ namaLengkap: "", nik: "", alamat: "" },
|
||||||
|
],
|
||||||
|
keteranganTambahan: "",
|
||||||
|
tanggalPelaporan: null,
|
||||||
|
pengesahan: {
|
||||||
|
kepalaDesaLurah: "",
|
||||||
|
camat: "",
|
||||||
|
petugasRegistrasi: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Validation rules derived from schema and best practices
|
||||||
|
validate: {
|
||||||
|
dataBayi: {
|
||||||
|
namaLengkap: (val) =>
|
||||||
|
val && val.trim().length > 0 ? null : "Nama bayi diperlukan.",
|
||||||
|
jenisKelamin: (val) => (val ? null : "Pilih jenis kelamin."),
|
||||||
|
tanggalLahir: (val) => (val ? null : "Tanggal lahir diperlukan."),
|
||||||
|
beratBadan: (val) =>
|
||||||
|
val == null || val > 0 ? null : "Berat harus lebih dari 0.",
|
||||||
|
panjangBadan: (val) =>
|
||||||
|
val == null || val > 0 ? null : "Panjang harus lebih dari 0.",
|
||||||
|
},
|
||||||
|
dataIbu: {
|
||||||
|
namaLengkap: (val) =>
|
||||||
|
val && val.trim().length > 0 ? null : "Nama ibu diperlukan.",
|
||||||
|
nik: (val) =>
|
||||||
|
val && /^[0-9]{16}$/.test(val.trim()) ? null : "NIK harus 16 digit.",
|
||||||
|
},
|
||||||
|
dataAyah: {
|
||||||
|
// if father provided, validate NIK format when non-empty
|
||||||
|
nik: (val) =>
|
||||||
|
val === "" || /^[0-9]{16}$/.test(val?.trim() || "")
|
||||||
|
? null
|
||||||
|
: "NIK harus 16 digit.",
|
||||||
|
},
|
||||||
|
dataPelapor: {
|
||||||
|
namaLengkap: (val) =>
|
||||||
|
val && val.trim().length > 0 ? null : "Nama pelapor diperlukan.",
|
||||||
|
nik: (val) =>
|
||||||
|
val && /^[0-9]{16}$/.test(val.trim()) ? null : "NIK harus 16 digit.",
|
||||||
|
},
|
||||||
|
saksi: {
|
||||||
|
// top-level validation for array minimal length handled in submit
|
||||||
|
} as any,
|
||||||
|
tanggalPelaporan: (val) => (val ? null : "Tanggal pelaporan diperlukan."),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ------------------- Dynamic saksi (witness) helpers ------------------- */
|
||||||
|
|
||||||
|
const addSaksi = () => {
|
||||||
|
form.setFieldValue("saksi", [
|
||||||
|
...form.values.saksi,
|
||||||
|
{ namaLengkap: "", nik: "", alamat: "" },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSaksi = (index: number) => {
|
||||||
|
const arr = [...form.values.saksi];
|
||||||
|
arr.splice(index, 1);
|
||||||
|
form.setFieldValue("saksi", arr);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------------- Submit / Reset handlers ---------------------- */
|
||||||
|
|
||||||
|
const handleSubmit = (values: BirthFormValues) => {
|
||||||
|
// Extra validation: ensure at least one saksi is filled meaningfully
|
||||||
|
const hasValidSaksi =
|
||||||
|
values.saksi.length > 0 &&
|
||||||
|
values.saksi.some((s) => (s.namaLengkap || "").trim().length > 0);
|
||||||
|
|
||||||
|
if (!hasValidSaksi) {
|
||||||
|
form.setFieldError("saksi", "Minimal satu saksi harus diisi.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final normalized payload (dates converted to ISO)
|
||||||
|
const payload = {
|
||||||
|
...values,
|
||||||
|
dataBayi: {
|
||||||
|
...values.dataBayi,
|
||||||
|
tanggalLahir: values.dataBayi.tanggalLahir
|
||||||
|
? values.dataBayi.tanggalLahir.toISOString().split("T")[0]
|
||||||
|
: null,
|
||||||
|
jamLahir: values.dataBayi.jamLahir
|
||||||
|
? values.dataBayi.jamLahir.toISOString().split("T")[1]?.slice(0, 8)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
tanggalPelaporan: values.tanggalPelaporan
|
||||||
|
? values.tanggalPelaporan.toISOString().split("T")[0]
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// For demo: print to console. Integrate with API in real app.
|
||||||
|
// Accessibility: focus the first invalid field if any (not shown here).
|
||||||
|
console.log("Submitted Birth Certificate Payload:", payload);
|
||||||
|
|
||||||
|
// Visual confirmation: we'll set a small success field (in production, use notification)
|
||||||
|
// Reset form or keep values based on UX decision. We'll keep values and indicate success.
|
||||||
|
// For example, you can call form.reset() to clear.
|
||||||
|
// form.reset();
|
||||||
|
alert("Form berhasil disubmit. Lihat console untuk payload.");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
form.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ------------------------------ Render ------------------------------ */
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size={"md"} w={"100%"}>
|
||||||
|
<Box>
|
||||||
|
<Stack gap="lg" style={{ maxWidth: 980, margin: "0 auto" }}>
|
||||||
|
<Group justify="apart">
|
||||||
|
<Title order={3}>Formulir Surat Keterangan Kelahiran</Title>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Badge variant="light" c="gray">
|
||||||
|
Blangko resmi
|
||||||
|
</Badge>
|
||||||
|
<Text size="sm" color="dimmed">
|
||||||
|
Blangko untuk pelaporan kelahiran & dasar penerbitan Akta
|
||||||
|
Kelahiran
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) => {
|
||||||
|
handleSubmit(values);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Section: Data Bayi */}
|
||||||
|
<FormSection
|
||||||
|
title="Data Bayi"
|
||||||
|
subtitle="Informasi lengkap tentang bayi yang lahir"
|
||||||
|
icon={<IconUser size={20} />}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="bayi-namaLengkap"
|
||||||
|
label="Nama Lengkap"
|
||||||
|
description="Nama lengkap bayi yang baru lahir (jika sudah ditentukan)."
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="bayi-namaLengkap"
|
||||||
|
placeholder="Contoh: Putu Gede"
|
||||||
|
{...form.getInputProps("dataBayi.namaLengkap")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="bayi-jenisKelamin"
|
||||||
|
label="Jenis Kelamin"
|
||||||
|
description="Pilih jenis kelamin bayi."
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
id="bayi-jenisKelamin"
|
||||||
|
placeholder="Pilih"
|
||||||
|
data={["Laki-laki", "Perempuan"]}
|
||||||
|
{...form.getInputProps("dataBayi.jenisKelamin")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
label="Tempat Lahir"
|
||||||
|
id="bayi-tempatLahir"
|
||||||
|
description="Nama desa/kelurahan/kecamatan/kabupaten/kota."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="bayi-tempatLahir"
|
||||||
|
placeholder="Contoh: RS X, Kecamatan Y"
|
||||||
|
{...form.getInputProps("dataBayi.tempatLahir")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<FormField
|
||||||
|
id="bayi-tanggalLahir"
|
||||||
|
label="Tanggal Lahir"
|
||||||
|
description="Tanggal lahir bayi."
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<DatePicker
|
||||||
|
id="bayi-tanggalLahir"
|
||||||
|
value={form.values.dataBayi.tanggalLahir}
|
||||||
|
onChange={(d) =>
|
||||||
|
form.setFieldValue("dataBayi.tanggalLahir", d as any)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<FormField
|
||||||
|
id="bayi-jamLahir"
|
||||||
|
label="Jam Lahir"
|
||||||
|
description="Jam lahir bayi."
|
||||||
|
>
|
||||||
|
<TimeInput
|
||||||
|
id="bayi-jamLahir"
|
||||||
|
placeholder="HH:MM"
|
||||||
|
value={form.values.dataBayi.jamLahir as any}
|
||||||
|
onChange={(d) =>
|
||||||
|
form.setFieldValue("dataBayi.jamLahir", d as any)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<FormField
|
||||||
|
id="bayi-beratBadan"
|
||||||
|
label="Berat Badan (kg)"
|
||||||
|
description="Berat dalam kilogram."
|
||||||
|
>
|
||||||
|
<NumberInput
|
||||||
|
id="bayi-beratBadan"
|
||||||
|
placeholder="3.2"
|
||||||
|
step={0.01}
|
||||||
|
min={0}
|
||||||
|
{...form.getInputProps("dataBayi.beratBadan")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<FormField
|
||||||
|
id="bayi-panjangBadan"
|
||||||
|
label="Panjang Badan (cm)"
|
||||||
|
description="Panjang dalam sentimeter."
|
||||||
|
>
|
||||||
|
<NumberInput
|
||||||
|
id="bayi-panjangBadan"
|
||||||
|
placeholder="50"
|
||||||
|
step={0.5}
|
||||||
|
min={0}
|
||||||
|
{...form.getInputProps("dataBayi.panjangBadan")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
{/* Section: Data Ibu */}
|
||||||
|
<FormSection
|
||||||
|
title="Data Ibu"
|
||||||
|
subtitle="Data identitas ibu kandung"
|
||||||
|
icon={<IconUser size={20} />}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="ibu-nama"
|
||||||
|
label="Nama Lengkap Ibu"
|
||||||
|
required
|
||||||
|
description="Nama lengkap ibu kandung bayi."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="ibu-nama"
|
||||||
|
placeholder="Nama lengkap"
|
||||||
|
{...form.getInputProps("dataIbu.namaLengkap")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="ibu-nik"
|
||||||
|
label="NIK Ibu"
|
||||||
|
required
|
||||||
|
description="Nomor Induk Kependudukan 16 digit."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="ibu-nik"
|
||||||
|
placeholder="16 digit NIK"
|
||||||
|
{...form.getInputProps("dataIbu.nik")}
|
||||||
|
inputMode="numeric"
|
||||||
|
aria-label="NIK Ibu"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="ibu-ttl"
|
||||||
|
label="Tempat & Tanggal Lahir Ibu"
|
||||||
|
description="Format: Kota, DD/MM/YYYY (bebas text)."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="ibu-ttl"
|
||||||
|
placeholder="Contoh: Denpasar, 01/01/1990"
|
||||||
|
{...form.getInputProps("dataIbu.tempatTanggalLahir")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="ibu-pekerjaan"
|
||||||
|
label="Pekerjaan Ibu"
|
||||||
|
description="Pekerjaan ibu."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="ibu-pekerjaan"
|
||||||
|
placeholder="Contoh: Ibu Rumah Tangga"
|
||||||
|
{...form.getInputProps("dataIbu.pekerjaan")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<FormField
|
||||||
|
id="ibu-alamat"
|
||||||
|
label="Alamat Ibu"
|
||||||
|
description="Alamat lengkap sesuai KTP."
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
id="ibu-alamat"
|
||||||
|
placeholder="Alamat lengkap"
|
||||||
|
autosize
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("dataIbu.alamat")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
{/* Section: Data Ayah */}
|
||||||
|
<FormSection
|
||||||
|
title="Data Ayah"
|
||||||
|
subtitle="Data identitas ayah kandung (jika tersedia)"
|
||||||
|
icon={<IconUser size={20} />}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="ayah-nama"
|
||||||
|
label="Nama Lengkap Ayah"
|
||||||
|
description="Nama lengkap ayah kandung bayi."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="ayah-nama"
|
||||||
|
placeholder="Nama lengkap"
|
||||||
|
{...form.getInputProps("dataAyah.namaLengkap")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="ayah-nik"
|
||||||
|
label="NIK Ayah"
|
||||||
|
description="Nomor Induk Kependudukan (16 digit)."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="ayah-nik"
|
||||||
|
placeholder="16 digit NIK"
|
||||||
|
{...form.getInputProps("dataAyah.nik")}
|
||||||
|
inputMode="numeric"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="ayah-ttl"
|
||||||
|
label="Tempat & Tanggal Lahir Ayah"
|
||||||
|
description="Format: Kota, DD/MM/YYYY (bebas text)."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="ayah-ttl"
|
||||||
|
placeholder="Contoh: Badung, 15/05/1988"
|
||||||
|
{...form.getInputProps("dataAyah.tempatTanggalLahir")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="ayah-pekerjaan"
|
||||||
|
label="Pekerjaan Ayah"
|
||||||
|
description="Pekerjaan ayah."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="ayah-pekerjaan"
|
||||||
|
placeholder="Contoh: Petani"
|
||||||
|
{...form.getInputProps("dataAyah.pekerjaan")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<FormField
|
||||||
|
id="ayah-alamat"
|
||||||
|
label="Alamat Ayah"
|
||||||
|
description="Alamat lengkap ayah."
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
id="ayah-alamat"
|
||||||
|
placeholder="Alamat lengkap"
|
||||||
|
autosize
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("dataAyah.alamat")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
{/* Section: Data Pelapor */}
|
||||||
|
<FormSection
|
||||||
|
title="Data Pelapor"
|
||||||
|
subtitle="Orang yang melaporkan kelahiran"
|
||||||
|
icon={<IconUser size={20} />}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="pelapor-nama"
|
||||||
|
label="Nama Pelapor"
|
||||||
|
required
|
||||||
|
description="Bisa ayah/ibu/kerabat."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="pelapor-nama"
|
||||||
|
placeholder="Nama pelapor"
|
||||||
|
{...form.getInputProps("dataPelapor.namaLengkap")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="pelapor-nik"
|
||||||
|
label="NIK Pelapor"
|
||||||
|
required
|
||||||
|
description="NIK 16 digit."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="pelapor-nik"
|
||||||
|
placeholder="16 digit NIK"
|
||||||
|
{...form.getInputProps("dataPelapor.nik")}
|
||||||
|
inputMode="numeric"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="pelapor-hubungan"
|
||||||
|
label="Hubungan dengan Bayi"
|
||||||
|
description="Contoh: Ayah, Ibu, Kakek, Nenek, dll."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="pelapor-hubungan"
|
||||||
|
placeholder="Contoh: Ayah"
|
||||||
|
{...form.getInputProps(
|
||||||
|
"dataPelapor.hubunganDenganBayi",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="pelapor-alamat"
|
||||||
|
label="Alamat Pelapor"
|
||||||
|
description="Alamat lengkap pelapor."
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
id="pelapor-alamat"
|
||||||
|
autosize
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("dataPelapor.alamat")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
{/* Section: Saksi (array) */}
|
||||||
|
<FormSection
|
||||||
|
title="Saksi"
|
||||||
|
subtitle="Daftar saksi yang menyaksikan proses kelahiran"
|
||||||
|
icon={<IconUser size={20} />}
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{form.values.saksi.map((s, idx) => (
|
||||||
|
<Card key={idx} radius="md" p="sm" withBorder>
|
||||||
|
<Grid align="center">
|
||||||
|
<Grid.Col span={10}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id={`saksi-${idx}-nama`}
|
||||||
|
label={`Saksi ${idx + 1} - Nama Lengkap`}
|
||||||
|
description="Nama lengkap saksi."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id={`saksi-${idx}-nama`}
|
||||||
|
placeholder="Nama lengkap"
|
||||||
|
value={form.values.saksi[idx]?.namaLengkap}
|
||||||
|
onChange={(e) => {
|
||||||
|
const arr = [...form.values.saksi] as any;
|
||||||
|
arr[idx] = {
|
||||||
|
...arr[idx],
|
||||||
|
namaLengkap: e.target.value,
|
||||||
|
};
|
||||||
|
form.setFieldValue("saksi", arr);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id={`saksi-${idx}-nik`}
|
||||||
|
label="NIK"
|
||||||
|
description="NIK 16 digit (opsional jika tidak punya)."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id={`saksi-${idx}-nik`}
|
||||||
|
placeholder="16 digit NIK"
|
||||||
|
value={form.values.saksi[idx]?.nik}
|
||||||
|
onChange={(e) => {
|
||||||
|
const arr = [...form.values.saksi] as any;
|
||||||
|
arr[idx] = {
|
||||||
|
...arr[idx],
|
||||||
|
nik: e.target.value,
|
||||||
|
};
|
||||||
|
form.setFieldValue("saksi", arr);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<FormField
|
||||||
|
id={`saksi-${idx}-alamat`}
|
||||||
|
label="Alamat Saksi"
|
||||||
|
description="Alamat lengkap saksi."
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
id={`saksi-${idx}-alamat`}
|
||||||
|
autosize
|
||||||
|
minRows={2}
|
||||||
|
value={form.values.saksi[idx]?.alamat}
|
||||||
|
onChange={(e) => {
|
||||||
|
const arr = [...form.values.saksi] as any;
|
||||||
|
arr[idx] = {
|
||||||
|
...arr[idx],
|
||||||
|
alamat: e.target.value,
|
||||||
|
};
|
||||||
|
form.setFieldValue("saksi", arr);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={2}>
|
||||||
|
<Group justify="right">
|
||||||
|
<ActionIcon
|
||||||
|
color="red"
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => removeSaksi(idx)}
|
||||||
|
aria-label={`Hapus saksi ${idx + 1}`}
|
||||||
|
>
|
||||||
|
<IconTrash />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Group justify="left">
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus />}
|
||||||
|
variant="outline"
|
||||||
|
onClick={addSaksi}
|
||||||
|
aria-label="Tambah saksi"
|
||||||
|
>
|
||||||
|
Tambah Saksi
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Display saksi-level error if exists */}
|
||||||
|
{form.errors.saksi ? (
|
||||||
|
<Text color="red" size="sm">
|
||||||
|
{form.errors.saksi as unknown as string}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
{/* Additional notes, pelaporan, pengesahan */}
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="keteranganTambahan"
|
||||||
|
label="Keterangan Tambahan"
|
||||||
|
description="Catatan atau keterangan lain yang perlu dicantumkan."
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
id="keteranganTambahan"
|
||||||
|
autosize
|
||||||
|
minRows={3}
|
||||||
|
{...form.getInputProps("keteranganTambahan")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="tanggalPelaporan"
|
||||||
|
label="Tanggal Pelaporan"
|
||||||
|
description="Tanggal saat pelaporan surat keterangan kelahiran."
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<DatePicker
|
||||||
|
id="tanggalPelaporan"
|
||||||
|
value={form.values.tanggalPelaporan}
|
||||||
|
onChange={(d) =>
|
||||||
|
form.setFieldValue("tanggalPelaporan", d as any)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<FormSection
|
||||||
|
title="Pengesahan"
|
||||||
|
subtitle="Pihak-pihak yang menandatangani/pengesahan"
|
||||||
|
icon={<IconUser size={20} />}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField
|
||||||
|
id="pengesahan-kepala"
|
||||||
|
label="Kepala Desa / Lurah"
|
||||||
|
description="Nama Kepala Desa atau Lurah yang mengesahkan."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="pengesahan-kepala"
|
||||||
|
placeholder="Nama"
|
||||||
|
{...form.getInputProps("pengesahan.kepalaDesaLurah")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField
|
||||||
|
id="pengesahan-camat"
|
||||||
|
label="Camat"
|
||||||
|
description="Nama Camat yang mengesahkan."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="pengesahan-camat"
|
||||||
|
placeholder="Nama"
|
||||||
|
{...form.getInputProps("pengesahan.camat")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField
|
||||||
|
id="pengesahan-petugas"
|
||||||
|
label="Petugas Registrasi"
|
||||||
|
description="Nama petugas pencatat sipil."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="pengesahan-petugas"
|
||||||
|
placeholder="Nama"
|
||||||
|
{...form.getInputProps("pengesahan.petugasRegistrasi")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Submit / Reset */}
|
||||||
|
<Group justify="right" gap="sm">
|
||||||
|
<Button
|
||||||
|
leftSection={<IconX />}
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleReset}
|
||||||
|
aria-label="Reset form"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
leftSection={<IconCheck />}
|
||||||
|
type="submit"
|
||||||
|
aria-label="Submit form"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
649
src/pages/darmasaba/form_laporan_sampah.tsx
Normal file
649
src/pages/darmasaba/form_laporan_sampah.tsx
Normal file
@@ -0,0 +1,649 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
|
Select,
|
||||||
|
MultiSelect,
|
||||||
|
NumberInput,
|
||||||
|
Switch,
|
||||||
|
Button,
|
||||||
|
FileButton,
|
||||||
|
Divider,
|
||||||
|
Accordion,
|
||||||
|
Grid,
|
||||||
|
ActionIcon,
|
||||||
|
Container,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import {
|
||||||
|
IconMapPin,
|
||||||
|
IconUpload,
|
||||||
|
IconUser,
|
||||||
|
IconPhoto,
|
||||||
|
IconVideo,
|
||||||
|
IconFileText,
|
||||||
|
IconTrash,
|
||||||
|
IconPlus,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Types generated from provided schema
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
type ReportType =
|
||||||
|
| "Sampah Liar"
|
||||||
|
| "Sampah Terbakar"
|
||||||
|
| "Penimbunan Ilegal"
|
||||||
|
| "Lainnya";
|
||||||
|
type StatusType =
|
||||||
|
| "Pending"
|
||||||
|
| "Terverifikasi"
|
||||||
|
| "Dalam Penanganan"
|
||||||
|
| "Selesai"
|
||||||
|
| "Ditolak";
|
||||||
|
type PriorityType = "Rendah" | "Sedang" | "Tinggi" | "Darurat";
|
||||||
|
|
||||||
|
type Reporter = {
|
||||||
|
isAnonymous: boolean;
|
||||||
|
name?: string | null;
|
||||||
|
phone?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
userId?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Location = {
|
||||||
|
address?: string | null;
|
||||||
|
village?: string | null;
|
||||||
|
subDistrict?: string | null;
|
||||||
|
city?: string | null;
|
||||||
|
province?: string | null;
|
||||||
|
postalCode?: string | null;
|
||||||
|
latitude?: number | null;
|
||||||
|
longitude?: number | null;
|
||||||
|
placeType?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WasteDetails = {
|
||||||
|
wasteTypes: string[];
|
||||||
|
estimatedVolume?: string | null;
|
||||||
|
hazardous?: boolean;
|
||||||
|
detailB3?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Evidence = {
|
||||||
|
photos: string[]; // urls or base64
|
||||||
|
videos: string[]; // urls or base64
|
||||||
|
attachments: string[]; // other files
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReportFormValues = {
|
||||||
|
reportId?: string | null;
|
||||||
|
reportType?: ReportType | null;
|
||||||
|
status?: StatusType | null;
|
||||||
|
priority?: PriorityType | null;
|
||||||
|
reporter: Reporter;
|
||||||
|
location: Location;
|
||||||
|
wasteDetails: WasteDetails;
|
||||||
|
evidence: Evidence;
|
||||||
|
notes?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Helper: file -> base64
|
||||||
|
// ---------------------------
|
||||||
|
async function fileToBase64(file: File | null): Promise<string | null> {
|
||||||
|
if (!file) return null;
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(String(reader.result));
|
||||||
|
reader.onerror = (err) => reject(err);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Main component
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
export default function FormLaporanSampah() {
|
||||||
|
const form = useForm<ReportFormValues>({
|
||||||
|
initialValues: {
|
||||||
|
reportId: "",
|
||||||
|
reportType: null,
|
||||||
|
status: "Pending",
|
||||||
|
priority: "Sedang",
|
||||||
|
reporter: {
|
||||||
|
isAnonymous: false,
|
||||||
|
name: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
userId: "",
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
address: "",
|
||||||
|
village: "",
|
||||||
|
subDistrict: "",
|
||||||
|
city: "",
|
||||||
|
province: "",
|
||||||
|
postalCode: "",
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
placeType: "",
|
||||||
|
},
|
||||||
|
wasteDetails: {
|
||||||
|
wasteTypes: [],
|
||||||
|
estimatedVolume: "",
|
||||||
|
hazardous: false,
|
||||||
|
detailB3: "",
|
||||||
|
},
|
||||||
|
evidence: {
|
||||||
|
photos: [],
|
||||||
|
videos: [],
|
||||||
|
attachments: [],
|
||||||
|
},
|
||||||
|
notes: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
validate: {
|
||||||
|
// basic validations
|
||||||
|
reportType: (v) => (!v ? "Pilih tipe laporan" : null),
|
||||||
|
status: (v) => (!v ? "Status diperlukan" : null),
|
||||||
|
// location requires at least address OR lat/lng
|
||||||
|
// We'll validate in onSubmit for combined rules
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// small UI helpers
|
||||||
|
const [photoUploadName, setPhotoUploadName] = useState<string | null>(null);
|
||||||
|
const [videoUploadName, setVideoUploadName] = useState<string | null>(null);
|
||||||
|
const [attachmentName, setAttachmentName] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// add photo/video/attachment entry from URL or uploaded file
|
||||||
|
const addPhotoUrl = (url: string) => {
|
||||||
|
if (!url) return;
|
||||||
|
const arr = [...form.values.evidence.photos, url];
|
||||||
|
form.setFieldValue("evidence", { ...form.values.evidence, photos: arr });
|
||||||
|
};
|
||||||
|
const addVideoUrl = (url: string) => {
|
||||||
|
if (!url) return;
|
||||||
|
const arr = [...form.values.evidence.videos, url];
|
||||||
|
form.setFieldValue("evidence", { ...form.values.evidence, videos: arr });
|
||||||
|
};
|
||||||
|
const addAttachmentUrl = (url: string) => {
|
||||||
|
if (!url) return;
|
||||||
|
const arr = [...form.values.evidence.attachments, url];
|
||||||
|
form.setFieldValue("evidence", {
|
||||||
|
...form.values.evidence,
|
||||||
|
attachments: arr,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// submit handler
|
||||||
|
const handleSubmit = async (values: ReportFormValues) => {
|
||||||
|
// composite validations
|
||||||
|
const hasAddress = Boolean(
|
||||||
|
values.location.address && values.location.address.trim(),
|
||||||
|
);
|
||||||
|
const hasCoords =
|
||||||
|
typeof values.location.latitude === "number" &&
|
||||||
|
typeof values.location.longitude === "number";
|
||||||
|
if (!hasAddress && !hasCoords) {
|
||||||
|
alert("Mohon isi alamat atau koordinat (latitude & longitude).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if hazardous true, ensure detailB3 exists
|
||||||
|
if (values.wasteDetails.hazardous && !values.wasteDetails.detailB3) {
|
||||||
|
alert("Jika terdapat sampah berbahaya, mohon isi rincian B3.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if reporter anonymous, clear personal fields
|
||||||
|
const payload = { ...values };
|
||||||
|
if (payload.reporter.isAnonymous) {
|
||||||
|
payload.reporter = { isAnonymous: true } as Reporter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: send to API — for now console
|
||||||
|
console.log("Submitting report:", payload);
|
||||||
|
alert("Laporan berhasil disubmit (demo). Cek console untuk payload.");
|
||||||
|
form.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="md" w="100%">
|
||||||
|
<Card shadow="sm" radius="md" p="xl">
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group justify="apart">
|
||||||
|
<div>
|
||||||
|
<Text fw={700} size="lg">
|
||||||
|
Form Laporan Sampah & Lingkungan
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Gunakan formulir ini untuk melaporkan masalah sampah/lingkungan.
|
||||||
|
Sertakan bukti foto/video bila memungkinkan.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) => {
|
||||||
|
handleSubmit(values);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<Select
|
||||||
|
label="Tipe Laporan"
|
||||||
|
placeholder="Pilih tipe"
|
||||||
|
data={[
|
||||||
|
"Sampah Liar",
|
||||||
|
"Sampah Terbakar",
|
||||||
|
"Penimbunan Ilegal",
|
||||||
|
"Lainnya",
|
||||||
|
]}
|
||||||
|
{...form.getInputProps("reportType")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<Select
|
||||||
|
label="Status"
|
||||||
|
data={[
|
||||||
|
"Pending",
|
||||||
|
"Terverifikasi",
|
||||||
|
"Dalam Penanganan",
|
||||||
|
"Selesai",
|
||||||
|
"Ditolak",
|
||||||
|
]}
|
||||||
|
{...form.getInputProps("status")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<Select
|
||||||
|
label="Prioritas"
|
||||||
|
data={["Rendah", "Sedang", "Tinggi", "Darurat"]}
|
||||||
|
{...form.getInputProps("priority")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Accordion variant="separated" defaultValue="reporter">
|
||||||
|
<Accordion.Item value="reporter">
|
||||||
|
<Accordion.Control icon={<IconUser size={16} />}>
|
||||||
|
Informasi Pelapor
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Group gap="md" align="center">
|
||||||
|
<Switch
|
||||||
|
label="Laporkan sebagai anonim"
|
||||||
|
{...form.getInputProps("reporter.isAnonymous", {
|
||||||
|
type: "checkbox",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{!form.values.reporter.isAnonymous && (
|
||||||
|
<Grid mt="sm">
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Nama"
|
||||||
|
placeholder="Nama pelapor"
|
||||||
|
{...form.getInputProps("reporter.name")}
|
||||||
|
></TextInput>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Telepon"
|
||||||
|
placeholder="08xx..."
|
||||||
|
{...form.getInputProps("reporter.phone")}
|
||||||
|
></TextInput>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
placeholder="email@contoh.com"
|
||||||
|
{...form.getInputProps("reporter.email")}
|
||||||
|
></TextInput>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="User ID (jika terdaftar)"
|
||||||
|
placeholder="user-uuid"
|
||||||
|
{...form.getInputProps("reporter.userId")}
|
||||||
|
></TextInput>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
<Accordion.Item value="location">
|
||||||
|
<Accordion.Control icon={<IconMapPin size={16} />}>
|
||||||
|
Lokasi Kejadian
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Textarea
|
||||||
|
label="Alamat deskriptif"
|
||||||
|
placeholder="Jalan, RT/RW, desa/kelurahan"
|
||||||
|
{...form.getInputProps("location.address")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<TextInput
|
||||||
|
label="Kelurahan/Desa"
|
||||||
|
{...form.getInputProps("location.village")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<TextInput
|
||||||
|
label="Kecamatan"
|
||||||
|
{...form.getInputProps("location.subDistrict")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<TextInput
|
||||||
|
label="Kabupaten/Kota"
|
||||||
|
{...form.getInputProps("location.city")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<TextInput
|
||||||
|
label="Provinsi"
|
||||||
|
{...form.getInputProps("location.province")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<TextInput
|
||||||
|
label="Kode Pos"
|
||||||
|
{...form.getInputProps("location.postalCode")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<Select
|
||||||
|
label="Jenis Lokasi"
|
||||||
|
data={[
|
||||||
|
"Pinggir Jalan",
|
||||||
|
"Sungai/Drainase",
|
||||||
|
"Lapangan",
|
||||||
|
"Hutan",
|
||||||
|
"Permukiman",
|
||||||
|
"Lainnya",
|
||||||
|
]}
|
||||||
|
{...form.getInputProps("location.placeType")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<NumberInput
|
||||||
|
label="Latitude"
|
||||||
|
placeholder="-6.200000"
|
||||||
|
style={{
|
||||||
|
precision: 6,
|
||||||
|
}}
|
||||||
|
{...form.getInputProps("location.latitude")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<NumberInput
|
||||||
|
label="Longitude"
|
||||||
|
placeholder="106.816666"
|
||||||
|
style={{
|
||||||
|
precision: 6,
|
||||||
|
}}
|
||||||
|
{...form.getInputProps("location.longitude")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
<Accordion.Item value="wasteDetails">
|
||||||
|
<Accordion.Control icon={<IconFileText size={16} />}>
|
||||||
|
Rincian Sampah
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<MultiSelect
|
||||||
|
label="Jenis Sampah"
|
||||||
|
placeholder="Pilih atau ketik jenis sampah"
|
||||||
|
data={[
|
||||||
|
"Plastik",
|
||||||
|
"Organik",
|
||||||
|
"Elektronik",
|
||||||
|
"Konstruksi",
|
||||||
|
"Ban",
|
||||||
|
"Kertas",
|
||||||
|
"Kaca",
|
||||||
|
"Lainnya",
|
||||||
|
]}
|
||||||
|
searchable
|
||||||
|
{...form.getInputProps("wasteDetails.wasteTypes")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Estimasi Volume"
|
||||||
|
placeholder="Mis. '2 karung', '3 m3'"
|
||||||
|
{...form.getInputProps(
|
||||||
|
"wasteDetails.estimatedVolume",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Group justify="left" gap="sm" align="center">
|
||||||
|
<Switch
|
||||||
|
label="Mengandung B3 (Berbahaya)"
|
||||||
|
{...form.getInputProps("wasteDetails.hazardous", {
|
||||||
|
type: "checkbox",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{form.values.wasteDetails.hazardous && (
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Textarea
|
||||||
|
label="Rincian B3"
|
||||||
|
placeholder="Jelaskan bahan berbahaya"
|
||||||
|
{...form.getInputProps("wasteDetails.detailB3")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
<Accordion.Item value="evidence">
|
||||||
|
<Accordion.Control icon={<IconPhoto size={16} />}>
|
||||||
|
Bukti & Lampiran
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Unggah foto, video, atau lampiran lain. Anda bisa
|
||||||
|
mengunggah file (disimpan sebagai base64) atau
|
||||||
|
menempelkan URL.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Group>
|
||||||
|
<FileButton
|
||||||
|
onChange={async (file) => {
|
||||||
|
if (!file) return;
|
||||||
|
const base64 = await fileToBase64(file);
|
||||||
|
if (!base64) return;
|
||||||
|
addPhotoUrl(base64);
|
||||||
|
setPhotoUploadName(file.name);
|
||||||
|
}}
|
||||||
|
accept="image/*"
|
||||||
|
>
|
||||||
|
{(props) => (
|
||||||
|
<Button
|
||||||
|
leftSection={<IconUpload size={16} />}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
Upload Foto
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => {
|
||||||
|
// quick add placeholder example photo (smart default)
|
||||||
|
addPhotoUrl(
|
||||||
|
"https://via.placeholder.com/800x600.png?text=Foto+contoh",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconPlus />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack mt="sm">
|
||||||
|
{form.values.evidence.photos.map((p, idx) => (
|
||||||
|
<Group key={idx} justify="apart">
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
style={{ wordBreak: "break-all" }}
|
||||||
|
>
|
||||||
|
{p.length > 60 ? p.slice(0, 60) + "..." : p}
|
||||||
|
</Text>
|
||||||
|
<ActionIcon
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
const arr =
|
||||||
|
form.values.evidence.photos.filter(
|
||||||
|
(_, i) => i !== idx,
|
||||||
|
);
|
||||||
|
form.setFieldValue("evidence", {
|
||||||
|
...form.values.evidence,
|
||||||
|
photos: arr,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Group>
|
||||||
|
<FileButton
|
||||||
|
onChange={async (file) => {
|
||||||
|
if (!file) return;
|
||||||
|
const base64 = await fileToBase64(file);
|
||||||
|
if (!base64) return;
|
||||||
|
addAttachmentUrl(base64);
|
||||||
|
setAttachmentName(file.name);
|
||||||
|
}}
|
||||||
|
accept="*/*"
|
||||||
|
>
|
||||||
|
{(props) => (
|
||||||
|
<Button
|
||||||
|
leftSection={<IconUpload size={16} />}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
Upload Lampiran
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
|
|
||||||
|
<FileButton
|
||||||
|
onChange={async (file) => {
|
||||||
|
if (!file) return;
|
||||||
|
const base64 = await fileToBase64(file);
|
||||||
|
if (!base64) return;
|
||||||
|
addVideoUrl(base64);
|
||||||
|
setVideoUploadName(file.name);
|
||||||
|
}}
|
||||||
|
accept="video/*"
|
||||||
|
>
|
||||||
|
{(props) => (
|
||||||
|
<Button
|
||||||
|
leftSection={<IconUpload size={16} />}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
Upload Video
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack mt="sm">
|
||||||
|
{form.values.evidence.videos.map((v, idx) => (
|
||||||
|
<Group key={idx} justify="apart">
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
style={{ wordBreak: "break-all" }}
|
||||||
|
>
|
||||||
|
{v.length > 60 ? v.slice(0, 60) + "..." : v}
|
||||||
|
</Text>
|
||||||
|
<ActionIcon
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
const arr =
|
||||||
|
form.values.evidence.videos.filter(
|
||||||
|
(_, i) => i !== idx,
|
||||||
|
);
|
||||||
|
form.setFieldValue("evidence", {
|
||||||
|
...form.values.evidence,
|
||||||
|
videos: arr,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Catatan Tambahan"
|
||||||
|
placeholder="Informasi lain yang relevan"
|
||||||
|
{...form.getInputProps("notes")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="right" gap="sm">
|
||||||
|
<Button variant="default" onClick={() => form.reset()}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">Kirim Laporan</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Tip: Sertakan minimal 1 foto jika memungkinkan untuk mempercepat
|
||||||
|
verifikasi.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
549
src/pages/darmasaba/form_surat_keterangan_belum_kawin.tsx
Normal file
549
src/pages/darmasaba/form_surat_keterangan_belum_kawin.tsx
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// pages/surat-keterangan-belum-kawin/page.tsx
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Card,
|
||||||
|
Stack,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
Divider,
|
||||||
|
Group,
|
||||||
|
SimpleGrid,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
|
Select,
|
||||||
|
RadioGroup,
|
||||||
|
Radio,
|
||||||
|
FileInput,
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { DatePicker } from "@mantine/dates";
|
||||||
|
import { showNotification } from "@mantine/notifications";
|
||||||
|
import {
|
||||||
|
IconFileText,
|
||||||
|
IconUser,
|
||||||
|
IconId,
|
||||||
|
IconMapPin,
|
||||||
|
IconCalendar,
|
||||||
|
IconBuildingStore,
|
||||||
|
IconBadge,
|
||||||
|
IconUpload,
|
||||||
|
IconCheck,
|
||||||
|
IconX,
|
||||||
|
IconAlertCircle,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function FormSuratKeteranganBelumKawin() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [ttdPreview, setTtdPreview] = useState<string | null>(null);
|
||||||
|
const [stempelPreview, setStempelPreview] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Mantine form state + validation rules
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
instansiPenerbit: {
|
||||||
|
kabupatenKota: "",
|
||||||
|
kecamatan: "",
|
||||||
|
desaKelurahan: "",
|
||||||
|
nomorSurat: "",
|
||||||
|
},
|
||||||
|
dataPemohon: {
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
tempatTanggalLahir: "",
|
||||||
|
jenisKelamin: "Laki-laki",
|
||||||
|
agama: "Islam",
|
||||||
|
pekerjaan: "",
|
||||||
|
alamat: "",
|
||||||
|
statusPerkawinan: "Belum Kawin",
|
||||||
|
},
|
||||||
|
isiSurat: {
|
||||||
|
pernyataan:
|
||||||
|
"Yang bertanda tangan di bawah ini menerangkan bahwa yang bersangkutan benar-benar belum pernah menikah sampai dengan tanggal surat ini.",
|
||||||
|
tujuan: "",
|
||||||
|
},
|
||||||
|
tanggalPenerbitan: null,
|
||||||
|
pengesahan: {
|
||||||
|
kepalaDesaLurah: "",
|
||||||
|
jabatan: "",
|
||||||
|
tandaTangan: null,
|
||||||
|
stempel: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
instansiPenerbit: {
|
||||||
|
kabupatenKota: (value: any) =>
|
||||||
|
value.trim().length > 0 ? null : "Kabupaten/Kota wajib diisi.",
|
||||||
|
kecamatan: (value: any) =>
|
||||||
|
value.trim().length > 0 ? null : "Kecamatan wajib diisi.",
|
||||||
|
desaKelurahan: (value: any) =>
|
||||||
|
value.trim().length > 0 ? null : "Desa/Kelurahan wajib diisi.",
|
||||||
|
nomorSurat: (value: any) =>
|
||||||
|
value.trim().length > 0 ? null : "Nomor surat wajib diisi.",
|
||||||
|
},
|
||||||
|
dataPemohon: {
|
||||||
|
namaLengkap: (value: any) =>
|
||||||
|
value.trim() ? null : "Nama lengkap wajib diisi.",
|
||||||
|
nik: (value: any) =>
|
||||||
|
/^\d{16}$/.test(value)
|
||||||
|
? null
|
||||||
|
: "NIK harus 16 digit angka tanpa spasi.",
|
||||||
|
tempatTanggalLahir: (value: any) =>
|
||||||
|
value.trim().length > 0
|
||||||
|
? null
|
||||||
|
: "Tempat dan tanggal lahir wajib diisi.",
|
||||||
|
pekerjaan: (value: any) =>
|
||||||
|
value.trim() ? null : "Pekerjaan wajib diisi.",
|
||||||
|
alamat: (value: any) => (value.trim() ? null : "Alamat wajib diisi."),
|
||||||
|
},
|
||||||
|
isiSurat: {
|
||||||
|
pernyataan: (value: any) =>
|
||||||
|
value.trim().length > 20
|
||||||
|
? null
|
||||||
|
: "Pernyataan harus singkat tapi jelas (min 20 karakter).",
|
||||||
|
tujuan: (value: any) =>
|
||||||
|
value.trim() ? null : "Sebutkan tujuan penerbitan surat.",
|
||||||
|
},
|
||||||
|
tanggalPenerbitan: (value: any) =>
|
||||||
|
value ? null : "Tanggal penerbitan wajib dipilih.",
|
||||||
|
pengesahan: {
|
||||||
|
kepalaDesaLurah: (value: any) =>
|
||||||
|
value.trim() ? null : "Nama kepala desa/lurah wajib diisi.",
|
||||||
|
jabatan: (value: any) => (value.trim() ? null : "Jabatan wajib diisi."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle file inputs and provide previews
|
||||||
|
const handleTtdChange = (file: File | null) => {
|
||||||
|
form.setFieldValue("pengesahan.tandaTangan", file as any);
|
||||||
|
if (file) {
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
setTtdPreview(url);
|
||||||
|
} else {
|
||||||
|
setTtdPreview(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStempelChange = (file: File | null) => {
|
||||||
|
form.setFieldValue("pengesahan.stempel", file as any);
|
||||||
|
if (file) {
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
setStempelPreview(url);
|
||||||
|
} else {
|
||||||
|
setStempelPreview(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simulate submit (replace with real API call)
|
||||||
|
const handleSubmit = async (values: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Example: prepare form data for file upload
|
||||||
|
const payload = new FormData();
|
||||||
|
payload.append(
|
||||||
|
"data",
|
||||||
|
JSON.stringify({
|
||||||
|
...values,
|
||||||
|
tanggalPenerbitan:
|
||||||
|
values.tanggalPenerbitan?.toISOString().slice(0, 10) ?? null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (values.pengesahan.tandaTangan) {
|
||||||
|
payload.append("tandaTangan", values.pengesahan.tandaTangan);
|
||||||
|
}
|
||||||
|
if (values.pengesahan.stempel) {
|
||||||
|
payload.append("stempel", values.pengesahan.stempel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the URL below with your API endpoint
|
||||||
|
const res = await fetch("/api/surat/belum-kawin", {
|
||||||
|
method: "POST",
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error("Gagal mengirim data ke server.");
|
||||||
|
|
||||||
|
showNotification({
|
||||||
|
title: "Sukses",
|
||||||
|
message: "Surat berhasil diajukan / disimpan.",
|
||||||
|
color: "green",
|
||||||
|
icon: <IconCheck />,
|
||||||
|
});
|
||||||
|
|
||||||
|
// optional: navigate back or clear form
|
||||||
|
navigate("/darmasaba");
|
||||||
|
form.reset();
|
||||||
|
setTtdPreview(null);
|
||||||
|
setStempelPreview(null);
|
||||||
|
} catch (err: any) {
|
||||||
|
showNotification({
|
||||||
|
title: "Terjadi Kesalahan",
|
||||||
|
message: err?.message || "Gagal mengirim data.",
|
||||||
|
color: "red",
|
||||||
|
icon: <IconX />,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="md" py="xl">
|
||||||
|
<Stack gap="xl">
|
||||||
|
<Group justify="apart" align="center">
|
||||||
|
<Title order={2}>
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconFileText />
|
||||||
|
<span>Surat Keterangan Belum Kawin</span>
|
||||||
|
</Group>
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Text color="dimmed">
|
||||||
|
Blangko resmi untuk menyatakan bahwa seseorang belum pernah menikah.
|
||||||
|
Lengkapi semua data lalu tekan <strong>Ajukan</strong>.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Card withBorder radius="md" p="lg" aria-labelledby="instansi-heading">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Group justify="apart" align="center">
|
||||||
|
<Title order={4} id="instansi-heading">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconBuildingStore />
|
||||||
|
Instansi Penerbit
|
||||||
|
</Group>
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<SimpleGrid cols={2} spacing="md">
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Kabupaten / Kota"
|
||||||
|
placeholder="Contoh: Kabupaten Badung"
|
||||||
|
description="Nama Kabupaten/Kota penerbit surat"
|
||||||
|
leftSection={<IconBadge />}
|
||||||
|
{...form.getInputProps("instansiPenerbit.kabupatenKota")}
|
||||||
|
aria-label="Kabupaten atau Kota"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Kecamatan"
|
||||||
|
placeholder="Contoh: Kuta"
|
||||||
|
description="Nama Kecamatan penerbit surat"
|
||||||
|
leftSection={<IconMapPin />}
|
||||||
|
{...form.getInputProps("instansiPenerbit.kecamatan")}
|
||||||
|
aria-label="Kecamatan"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Desa / Kelurahan"
|
||||||
|
placeholder="Contoh: Desa XYZ"
|
||||||
|
description="Nama Desa atau Kelurahan penerbit"
|
||||||
|
{...form.getInputProps("instansiPenerbit.desaKelurahan")}
|
||||||
|
aria-label="Desa atau Kelurahan"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Nomor Surat"
|
||||||
|
placeholder="Format: 123/ABC/2025"
|
||||||
|
description="Nomor surat sesuai register desa/kelurahan"
|
||||||
|
{...form.getInputProps("instansiPenerbit.nomorSurat")}
|
||||||
|
aria-label="Nomor surat"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card withBorder radius="md" p="lg" aria-labelledby="pemohon-heading">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Group justify="apart" align="center">
|
||||||
|
<Title order={4} id="pemohon-heading">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconUser />
|
||||||
|
Data Pemohon
|
||||||
|
</Group>
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<SimpleGrid cols={2} spacing="md">
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Nama Lengkap"
|
||||||
|
placeholder="Nama sesuai KTP"
|
||||||
|
description="Masukkan nama lengkap seperti tertera di KTP"
|
||||||
|
leftSection={<IconUser />}
|
||||||
|
{...form.getInputProps("dataPemohon.namaLengkap")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="NIK (16 digit)"
|
||||||
|
placeholder="Contoh: 3272011201010001"
|
||||||
|
description="Nomor Induk Kependudukan, 16 digit tanpa spasi"
|
||||||
|
leftSection={<IconId />}
|
||||||
|
{...form.getInputProps("dataPemohon.nik")}
|
||||||
|
inputMode="numeric"
|
||||||
|
maxLength={16}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Tempat, Tanggal Lahir"
|
||||||
|
placeholder="Contoh: Denpasar, 01 Januari 1990"
|
||||||
|
description="Masukkan tempat dan tanggal lahir"
|
||||||
|
{...form.getInputProps("dataPemohon.tempatTanggalLahir")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Agama"
|
||||||
|
placeholder="Pilih agama"
|
||||||
|
data={[
|
||||||
|
"Islam",
|
||||||
|
"Kristen",
|
||||||
|
"Katolik",
|
||||||
|
"Hindu",
|
||||||
|
"Buddha",
|
||||||
|
"Konghucu",
|
||||||
|
"Lainnya",
|
||||||
|
]}
|
||||||
|
description="Agama sesuai identitas"
|
||||||
|
{...form.getInputProps("dataPemohon.agama")}
|
||||||
|
/>
|
||||||
|
<RadioGroup
|
||||||
|
label="Jenis Kelamin"
|
||||||
|
{...form.getInputProps("dataPemohon.jenisKelamin")}
|
||||||
|
>
|
||||||
|
<Radio value="Laki-laki" label="Laki-laki" />
|
||||||
|
<Radio value="Perempuan" label="Perempuan" />
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Pekerjaan"
|
||||||
|
placeholder="Contoh: Petani / PNS / Swasta"
|
||||||
|
{...form.getInputProps("dataPemohon.pekerjaan")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
minRows={2}
|
||||||
|
label="Alamat"
|
||||||
|
placeholder="Alamat lengkap sesuai domisili"
|
||||||
|
description="Contoh: Jl. Mawar No. 1 RT 01 RW 02"
|
||||||
|
{...form.getInputProps("dataPemohon.alamat")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Status Perkawinan"
|
||||||
|
description="Dalam surat ini nilai default harus 'Belum Kawin'"
|
||||||
|
{...form.getInputProps("dataPemohon.statusPerkawinan")}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card withBorder radius="md" p="lg" aria-labelledby="isi-heading">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Group justify="apart" align="center">
|
||||||
|
<Title order={4} id="isi-heading">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconFileText />
|
||||||
|
Isi Surat
|
||||||
|
</Group>
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Pernyataan"
|
||||||
|
minRows={4}
|
||||||
|
description="Teks pernyataan resmi (boleh diedit)."
|
||||||
|
placeholder="Tulis pernyataan singkat yang menyatakan pemohon belum pernah menikah..."
|
||||||
|
{...form.getInputProps("isiSurat.pernyataan")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Tujuan Penerbitan Surat"
|
||||||
|
placeholder="Contoh: Untuk syarat pernikahan / beasiswa / administrasi"
|
||||||
|
description="Jelaskan singkat tujuan pembuatan surat"
|
||||||
|
{...form.getInputProps("isiSurat.tujuan")}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card withBorder radius="md" p="lg" aria-labelledby="tanggal-heading">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Group justify="apart" align="center">
|
||||||
|
<Title order={4} id="tanggal-heading">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconCalendar />
|
||||||
|
Tanggal Penerbitan
|
||||||
|
</Group>
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<DatePicker
|
||||||
|
{...form.getInputProps("tanggalPenerbitan")}
|
||||||
|
aria-label="Tanggal penerbitan surat"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
p="lg"
|
||||||
|
aria-labelledby="pengesahan-heading"
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Group justify="apart" align="center">
|
||||||
|
<Title order={4} id="pengesahan-heading">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconBadge />
|
||||||
|
Pengesahan
|
||||||
|
</Group>
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<SimpleGrid cols={2} spacing="md">
|
||||||
|
<TextInput
|
||||||
|
label="Nama Kepala Desa / Lurah"
|
||||||
|
placeholder="Nama pejabat yang menandatangani"
|
||||||
|
description="Masukkan nama pejabat yang menandatangani"
|
||||||
|
{...form.getInputProps("pengesahan.kepalaDesaLurah")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Jabatan"
|
||||||
|
placeholder="Contoh: Kepala Desa"
|
||||||
|
description="Jabatan pejabat yang mengesahkan"
|
||||||
|
{...form.getInputProps("pengesahan.jabatan")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FileInputWrapper
|
||||||
|
label="Tanda Tangan (scan)"
|
||||||
|
placeholder="Upload file tanda tangan (jpg,png,pdf)"
|
||||||
|
accept="image/*,application/pdf"
|
||||||
|
onChange={handleTtdChange}
|
||||||
|
preview={ttdPreview}
|
||||||
|
name="pengesahan.tandaTangan"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FileInputWrapper
|
||||||
|
label="Stempel (scan)"
|
||||||
|
placeholder="Upload file stempel (jpg,png,pdf)"
|
||||||
|
accept="image/*,application/pdf"
|
||||||
|
onChange={handleStempelChange}
|
||||||
|
preview={stempelPreview}
|
||||||
|
name="pengesahan.stempel"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Group justify="right" mt="md">
|
||||||
|
<Tooltip
|
||||||
|
label="Pastikan semua data sudah benar sebelum mengajukan"
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconUpload />}
|
||||||
|
onClick={() =>
|
||||||
|
form.validate() && form.isValid() && handleSubmit(form.values)
|
||||||
|
}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
Ajukan
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset();
|
||||||
|
setTtdPreview(null);
|
||||||
|
setStempelPreview(null);
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
leftSection={<IconAlertCircle />}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Text size="sm" color="dimmed">
|
||||||
|
Catatan: Dokumen yang diupload hanya akan dipakai untuk verifikasi
|
||||||
|
administrasi. Pastikan file bersih dan terbaca.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Small wrapper component for file input + preview with accessible labels.
|
||||||
|
* Kept inside the same file for simplicity — extract to components/ when reusing.
|
||||||
|
*/
|
||||||
|
function FileInputWrapper({
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
accept,
|
||||||
|
onChange,
|
||||||
|
preview,
|
||||||
|
name,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
placeholder?: string;
|
||||||
|
accept?: string;
|
||||||
|
onChange: (file: File | null) => void;
|
||||||
|
preview?: string | null;
|
||||||
|
name: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group justify="apart" align="center">
|
||||||
|
<Text fw={500}>{label}</Text>
|
||||||
|
<Tooltip label="Upload scan yang jelas (jpg/png/pdf)">
|
||||||
|
<IconFileText size={16} />
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<FileInput
|
||||||
|
accept={accept}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(f) => onChange(f)}
|
||||||
|
leftSection={<IconUpload />}
|
||||||
|
aria-label={label}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{preview ? (
|
||||||
|
<div>
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
Preview:
|
||||||
|
</Text>
|
||||||
|
{/* If preview is an image it will show; pdf preview might not render as image */}
|
||||||
|
{/* Use <object> or <img> depending on file type — keep simple here */}
|
||||||
|
<div style={{ marginTop: 6 }}>
|
||||||
|
<img
|
||||||
|
src={preview}
|
||||||
|
alt={`${label} preview`}
|
||||||
|
style={{ maxWidth: "200px", borderRadius: 4 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,677 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
FileInput,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { DatePicker } from "@mantine/dates";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import {
|
||||||
|
IconBuildingStore,
|
||||||
|
IconCalendar,
|
||||||
|
IconCheck,
|
||||||
|
IconFileUpload,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconMapPin,
|
||||||
|
IconShieldCheck,
|
||||||
|
IconUser,
|
||||||
|
IconX,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
/* ---------------------------
|
||||||
|
Types (strong typing)
|
||||||
|
--------------------------- */
|
||||||
|
|
||||||
|
type MasaBerlaku = {
|
||||||
|
mulai: Date | null;
|
||||||
|
sampai: Date | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Organisasi = {
|
||||||
|
namaOrganisasi?: string;
|
||||||
|
jenisOrganisasi?: string;
|
||||||
|
bidangKegiatan?: string;
|
||||||
|
aktePendirian?: string;
|
||||||
|
npwp?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AlamatDomisili = {
|
||||||
|
alamatLengkap?: string;
|
||||||
|
rt?: string;
|
||||||
|
rw?: string;
|
||||||
|
desaKelurahan?: string;
|
||||||
|
kecamatan?: string;
|
||||||
|
kabupatenKota?: string;
|
||||||
|
provinsi?: string;
|
||||||
|
kodePos?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PenanggungJawab = {
|
||||||
|
namaLengkap?: string;
|
||||||
|
nik?: string;
|
||||||
|
jabatan?: string;
|
||||||
|
kontak?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Pengesahan = {
|
||||||
|
dikeluarkanDi?: string;
|
||||||
|
tanggalDikeluarkan?: Date | null;
|
||||||
|
lurahAtauCamat?: string;
|
||||||
|
jabatanPejabat?: string;
|
||||||
|
tandaTanganStempel?: File | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SkdoFormValues = {
|
||||||
|
nomorSurat?: string;
|
||||||
|
organisasi: Organisasi;
|
||||||
|
alamatDomisili: AlamatDomisili;
|
||||||
|
penanggungJawab: PenanggungJawab;
|
||||||
|
keperluan?: string;
|
||||||
|
masaBerlaku: MasaBerlaku;
|
||||||
|
pengesahan: Pengesahan;
|
||||||
|
// extra helpers (e.g. draft toggle)
|
||||||
|
isDraft?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------------------
|
||||||
|
Reusable smaller components
|
||||||
|
--------------------------- */
|
||||||
|
|
||||||
|
/** Label with an info tooltip icon */
|
||||||
|
function LabelWithInfo({
|
||||||
|
label,
|
||||||
|
info,
|
||||||
|
Icon = IconInfoCircle,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
info?: string;
|
||||||
|
Icon?: React.FC<any>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Group gap="xs" justify="apart" align="center" style={{ width: "100%" }}>
|
||||||
|
<Text fw={600} size="sm">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
{info ? (
|
||||||
|
<Tooltip label={info} withArrow>
|
||||||
|
<ActionIcon aria-label={`${label} info`}>
|
||||||
|
<Icon size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------
|
||||||
|
Main Form Component
|
||||||
|
--------------------------- */
|
||||||
|
|
||||||
|
export default function FormSuratKeteranganDomisiliOrganisasi() {
|
||||||
|
// sensible defaults (smart defaults)
|
||||||
|
const form = useForm<SkdoFormValues>({
|
||||||
|
initialValues: {
|
||||||
|
nomorSurat: "",
|
||||||
|
organisasi: {
|
||||||
|
namaOrganisasi: "",
|
||||||
|
jenisOrganisasi: "",
|
||||||
|
bidangKegiatan: "",
|
||||||
|
aktePendirian: "",
|
||||||
|
npwp: "",
|
||||||
|
},
|
||||||
|
alamatDomisili: {
|
||||||
|
alamatLengkap: "",
|
||||||
|
rt: "",
|
||||||
|
rw: "",
|
||||||
|
desaKelurahan: "",
|
||||||
|
kecamatan: "",
|
||||||
|
kabupatenKota: "",
|
||||||
|
provinsi: "",
|
||||||
|
kodePos: "",
|
||||||
|
},
|
||||||
|
penanggungJawab: {
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
jabatan: "",
|
||||||
|
kontak: "",
|
||||||
|
},
|
||||||
|
keperluan: "",
|
||||||
|
masaBerlaku: {
|
||||||
|
mulai: null,
|
||||||
|
sampai: null,
|
||||||
|
},
|
||||||
|
pengesahan: {
|
||||||
|
dikeluarkanDi: "",
|
||||||
|
tanggalDikeluarkan: null,
|
||||||
|
lurahAtauCamat: "",
|
||||||
|
jabatanPejabat: "",
|
||||||
|
tandaTanganStempel: null,
|
||||||
|
},
|
||||||
|
isDraft: false,
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
// nomorSurat optional, but validate length if filled
|
||||||
|
nomorSurat: (value) =>
|
||||||
|
value && value.length > 100 ? "Nomor surat terlalu panjang" : null,
|
||||||
|
organisasi: {
|
||||||
|
namaOrganisasi: (v: any) =>
|
||||||
|
!v || v.trim().length === 0 ? "Nama organisasi wajib diisi" : null,
|
||||||
|
// jenisOrganisasi optional but if "Lainnya" require additional explanation? (not in schema)
|
||||||
|
} as any,
|
||||||
|
penanggungJawab: {
|
||||||
|
namaLengkap: (v: any) =>
|
||||||
|
!v || v.trim().length === 0
|
||||||
|
? "Nama penanggung jawab wajib diisi"
|
||||||
|
: null,
|
||||||
|
nik: (v: any) => {
|
||||||
|
if (!v) return "NIK wajib diisi";
|
||||||
|
const digits = v.replace(/\D/g, "");
|
||||||
|
if (digits.length !== 16) return "NIK harus 16 digit";
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
kontak: (v: any) => {
|
||||||
|
if (!v) return "Kontak wajib diisi";
|
||||||
|
if (!/^[\d+\-\s()]{6,20}$/.test(v))
|
||||||
|
return "Masukkan nomor telepon/HP yang valid";
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
keperluan: (v) =>
|
||||||
|
!v || v.trim().length === 0 ? "Keperluan wajib diisi" : null,
|
||||||
|
masaBerlaku: {
|
||||||
|
mulai: (v: any) => (v === null ? "Tanggal mulai wajib diisi" : null),
|
||||||
|
sampai: (v: any, values: any) => {
|
||||||
|
if (v === null) return "Tanggal sampai wajib diisi";
|
||||||
|
if (values.masaBerlaku.mulai && v < values.masaBerlaku.mulai)
|
||||||
|
return "Tanggal sampai harus setelah atau sama dengan tanggal mulai";
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
pengesahan: {
|
||||||
|
tanggalDikeluarkan: (v: any) =>
|
||||||
|
v === null ? "Tanggal dikeluarkan wajib diisi" : null,
|
||||||
|
lurahAtauCamat: (v: any) => (!v ? "Nama pejabat wajib diisi" : null),
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------------------------
|
||||||
|
Submit & Reset handlers
|
||||||
|
--------------------------- */
|
||||||
|
|
||||||
|
const handleSubmit = (values: SkdoFormValues) => {
|
||||||
|
// In a real app: send to API, show toast, handle file upload, etc.
|
||||||
|
// Here we simply log the values (and convert File to name).
|
||||||
|
const sanitized = {
|
||||||
|
...values,
|
||||||
|
pengesahan: {
|
||||||
|
...values.pengesahan,
|
||||||
|
tandaTanganStempel: values.pengesahan.tandaTanganStempel
|
||||||
|
? (values.pengesahan.tandaTanganStempel as File).name
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Simple success UX: console + accessible focus
|
||||||
|
// Replace with notifications/toasts in production
|
||||||
|
|
||||||
|
console.log("SKDO Submitted:", sanitized);
|
||||||
|
// accessible focus to top message (not implemented here) or show UI feedback
|
||||||
|
alert("Form submitted — lihat console untuk data (demo)");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => form.reset();
|
||||||
|
|
||||||
|
/* ---------------------------
|
||||||
|
UI Layout
|
||||||
|
--------------------------- */
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="md" w="100%">
|
||||||
|
<Card radius="md" p="lg" withBorder>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Header */}
|
||||||
|
<Group justify="apart" align="center" gap="sm">
|
||||||
|
<Group align="center" gap="sm">
|
||||||
|
<IconShieldCheck size={28} />
|
||||||
|
<Box>
|
||||||
|
<Text fw={700} size="lg">
|
||||||
|
Surat Keterangan Domisili Organisasi (SKDO)
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Blangko resmi untuk permohonan pembuatan Surat Keterangan
|
||||||
|
Domisili Organisasi.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Switch
|
||||||
|
aria-label="Save as draft"
|
||||||
|
label="Simpan sebagai draft"
|
||||||
|
checked={form.values.isDraft}
|
||||||
|
onChange={(e) =>
|
||||||
|
form.setFieldValue("isDraft", e.currentTarget.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Top-level basic fields */}
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<LabelWithInfo
|
||||||
|
label="Nomor Surat"
|
||||||
|
info="Nomor surat (diisi oleh kantor kelurahan/kecamatan jika ada)."
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="e.g. 123/SKDO/2025"
|
||||||
|
{...form.getInputProps("nomorSurat")}
|
||||||
|
aria-label="Nomor Surat"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<LabelWithInfo
|
||||||
|
label="Keperluan"
|
||||||
|
info="Tujuan pembuatan surat domisili."
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Contoh: Pengajuan izin operasional / Pendaftaran NPWP / Pembukaan rekening bank"
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("keperluan")}
|
||||||
|
aria-label="Keperluan"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Organisasi section */}
|
||||||
|
<Accordion variant="contained" chevronPosition="right" multiple>
|
||||||
|
<Accordion.Item value="organisasi">
|
||||||
|
<Accordion.Control icon={<IconBuildingStore />}>
|
||||||
|
Data Organisasi
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<LabelWithInfo
|
||||||
|
label="Nama Organisasi"
|
||||||
|
info="Nama lengkap organisasi/lembaga."
|
||||||
|
Icon={IconBuildingStore}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama organisasi"
|
||||||
|
{...form.getInputProps("organisasi.namaOrganisasi")}
|
||||||
|
aria-label="Nama Organisasi"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo
|
||||||
|
label="Jenis Organisasi"
|
||||||
|
info="Pilih jenis organisasi."
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="Pilih jenis organisasi"
|
||||||
|
data={[
|
||||||
|
"Yayasan",
|
||||||
|
"Perkumpulan",
|
||||||
|
"Lembaga Sosial",
|
||||||
|
"Organisasi Keagamaan",
|
||||||
|
"Komunitas",
|
||||||
|
"Lainnya",
|
||||||
|
]}
|
||||||
|
{...form.getInputProps("organisasi.jenisOrganisasi")}
|
||||||
|
aria-label="Jenis Organisasi"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo
|
||||||
|
label="Bidang Kegiatan"
|
||||||
|
info="Contoh: sosial, pendidikan, lingkungan, olahraga."
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Bidang kegiatan"
|
||||||
|
{...form.getInputProps("organisasi.bidangKegiatan")}
|
||||||
|
aria-label="Bidang Kegiatan"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo
|
||||||
|
label="Nomor Akta Pendirian"
|
||||||
|
info="Jika ada."
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nomor akta (opsional)"
|
||||||
|
{...form.getInputProps("organisasi.aktePendirian")}
|
||||||
|
aria-label="Akte Pendirian"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo label="NPWP Organisasi" info="Jika ada." />
|
||||||
|
<TextInput
|
||||||
|
placeholder="NPWP (opsional)"
|
||||||
|
{...form.getInputProps("organisasi.npwp")}
|
||||||
|
aria-label="NPWP Organisasi"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
{/* Alamat Domisili */}
|
||||||
|
<Accordion.Item value="alamat">
|
||||||
|
<Accordion.Control icon={<IconMapPin />}>
|
||||||
|
Alamat Domisili
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<LabelWithInfo
|
||||||
|
label="Alamat Lengkap"
|
||||||
|
info="Alamat tempat organisasi berdomisili."
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
minRows={2}
|
||||||
|
placeholder="Jalan, nomor gedung, blok, dsb."
|
||||||
|
{...form.getInputProps("alamatDomisili.alamatLengkap")}
|
||||||
|
aria-label="Alamat Lengkap"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={2}>
|
||||||
|
<Text fw={700} size="sm">
|
||||||
|
RT
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
placeholder="001"
|
||||||
|
{...form.getInputProps("alamatDomisili.rt")}
|
||||||
|
aria-label="RT"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={2}>
|
||||||
|
<Text fw={700} size="sm">
|
||||||
|
RW
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
placeholder="002"
|
||||||
|
{...form.getInputProps("alamatDomisili.rw")}
|
||||||
|
aria-label="RW"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<LabelWithInfo label="Desa / Kelurahan" />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama desa/kelurahan"
|
||||||
|
{...form.getInputProps("alamatDomisili.desaKelurahan")}
|
||||||
|
aria-label="Desa Kelurahan"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<LabelWithInfo label="Kecamatan" />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama kecamatan"
|
||||||
|
{...form.getInputProps("alamatDomisili.kecamatan")}
|
||||||
|
aria-label="Kecamatan"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo label="Kabupaten / Kota" />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama kabupaten/kota"
|
||||||
|
{...form.getInputProps("alamatDomisili.kabupatenKota")}
|
||||||
|
aria-label="Kabupaten Kota"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<LabelWithInfo label="Provinsi" />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama provinsi"
|
||||||
|
{...form.getInputProps("alamatDomisili.provinsi")}
|
||||||
|
aria-label="Provinsi"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={2}>
|
||||||
|
<LabelWithInfo label="Kode Pos" />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Kode pos"
|
||||||
|
{...form.getInputProps("alamatDomisili.kodePos")}
|
||||||
|
aria-label="Kode Pos"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
{/* Penanggung Jawab */}
|
||||||
|
<Accordion.Item value="penanggungJawab">
|
||||||
|
<Accordion.Control icon={<IconUser />}>
|
||||||
|
Penanggung Jawab
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo
|
||||||
|
label="Nama Lengkap"
|
||||||
|
info="Nama lengkap ketua/penanggung jawab organisasi."
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama lengkap"
|
||||||
|
{...form.getInputProps("penanggungJawab.namaLengkap")}
|
||||||
|
aria-label="Nama Penanggung Jawab"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo
|
||||||
|
label="NIK"
|
||||||
|
info="16 digit Nomor Induk Kependudukan"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="16 digit NIK"
|
||||||
|
{...form.getInputProps("penanggungJawab.nik")}
|
||||||
|
aria-label="NIK"
|
||||||
|
inputMode="numeric"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo label="Jabatan" />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Ketua / Sekretaris / Direktur"
|
||||||
|
{...form.getInputProps("penanggungJawab.jabatan")}
|
||||||
|
aria-label="Jabatan"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo
|
||||||
|
label="Kontak"
|
||||||
|
info="Nomor telepon/HP penanggung jawab"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="+62 812-3456-7890"
|
||||||
|
{...form.getInputProps("penanggungJawab.kontak")}
|
||||||
|
aria-label="Kontak"
|
||||||
|
inputMode="tel"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
{/* Masa Berlaku */}
|
||||||
|
<Accordion.Item value="masa">
|
||||||
|
<Accordion.Control icon={<IconCalendar />}>
|
||||||
|
Masa Berlaku
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo
|
||||||
|
label="Mulai"
|
||||||
|
info="Tanggal mulai berlaku."
|
||||||
|
Icon={IconCalendar}
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
{...form.getInputProps("masaBerlaku.mulai")}
|
||||||
|
aria-label="Tanggal Mulai"
|
||||||
|
style={{
|
||||||
|
"&::after": {
|
||||||
|
content: " *",
|
||||||
|
color: "red",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo
|
||||||
|
label="Sampai"
|
||||||
|
info="Tanggal berakhir berlaku."
|
||||||
|
Icon={IconCalendar}
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
{...form.getInputProps("masaBerlaku.sampai")}
|
||||||
|
aria-label="Tanggal Sampai"
|
||||||
|
style={{
|
||||||
|
"&::after": {
|
||||||
|
content: " *",
|
||||||
|
color: "red",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
{/* Pengesahan */}
|
||||||
|
<Accordion.Item value="pengesahan">
|
||||||
|
<Accordion.Control icon={<IconShieldCheck />}>
|
||||||
|
Pengesahan
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo label="Dikeluarkan di" />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama kota/kabupaten"
|
||||||
|
{...form.getInputProps("pengesahan.dikeluarkanDi")}
|
||||||
|
aria-label="Dikeluarkan Di"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo label="Tanggal Dikeluarkan" />
|
||||||
|
<DatePicker
|
||||||
|
{...form.getInputProps("pengesahan.tanggalDikeluarkan")}
|
||||||
|
aria-label="Tanggal Dikeluarkan"
|
||||||
|
style={{
|
||||||
|
"&::after": {
|
||||||
|
content: " *",
|
||||||
|
color: "red",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo label="Nama Pejabat (Lurah/Camat)" />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama pejabat penandatangan"
|
||||||
|
{...form.getInputProps("pengesahan.lurahAtauCamat")}
|
||||||
|
aria-label="Nama Pejabat"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<LabelWithInfo label="Jabatan Pejabat" />
|
||||||
|
<TextInput
|
||||||
|
placeholder="Jabatan pejabat (mis. Lurah Kelurahan Sukamaju)"
|
||||||
|
{...form.getInputProps("pengesahan.jabatanPejabat")}
|
||||||
|
aria-label="Jabatan Pejabat"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<LabelWithInfo
|
||||||
|
label="Tanda Tangan & Stempel (scan)"
|
||||||
|
info="Upload file scan tanda tangan dan stempel resmi."
|
||||||
|
Icon={IconFileUpload}
|
||||||
|
/>
|
||||||
|
<FileInput
|
||||||
|
placeholder="Pilih file (jpg, png, pdf)"
|
||||||
|
{...form.getInputProps("pengesahan.tandaTanganStempel")}
|
||||||
|
accept="image/*,application/pdf"
|
||||||
|
leftSection={<IconFileUpload size={16} />}
|
||||||
|
aria-label="Tanda Tangan Stempel"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<Group justify="right" gap="sm">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={handleReset}
|
||||||
|
leftSection={<IconX />}
|
||||||
|
aria-label="Reset form"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => form.onSubmit(handleSubmit)}
|
||||||
|
leftSection={<IconCheck />}
|
||||||
|
aria-label="Submit form"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Minimal accessibility & developer hints */}
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Tip: Gunakan Tab / Shift+Tab untuk navigasi keyboard. Semua input
|
||||||
|
memiliki label yang dapat dibaca screen reader.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
531
src/pages/darmasaba/form_surat_keterangan_kelakuan_baik.tsx
Normal file
531
src/pages/darmasaba/form_surat_keterangan_kelakuan_baik.tsx
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// pages/surat-keterangan-kelakuan-baik/page.tsx
|
||||||
|
// Single-file Next.js App Router page implementing the form described by the JSON schema.
|
||||||
|
// Also contains TypeScript interfaces and some small reusable components/hooks for clarity.
|
||||||
|
|
||||||
|
import React, { useState, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Card,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
Divider,
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
SimpleGrid,
|
||||||
|
TextInput,
|
||||||
|
Select,
|
||||||
|
Textarea,
|
||||||
|
Button,
|
||||||
|
Badge,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { DatePicker } from "@mantine/dates";
|
||||||
|
import { showNotification } from "@mantine/notifications";
|
||||||
|
import {
|
||||||
|
IconUser,
|
||||||
|
IconMapPin,
|
||||||
|
IconCalendar,
|
||||||
|
IconFileText,
|
||||||
|
IconBuildingBank,
|
||||||
|
IconChecklist,
|
||||||
|
IconCheck,
|
||||||
|
IconX,
|
||||||
|
IconCards,
|
||||||
|
IconFile,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
|
// TypeScript interfaces
|
||||||
|
// ----------------------
|
||||||
|
|
||||||
|
export interface Instansi {
|
||||||
|
kabupatenKota: string;
|
||||||
|
kecamatan: string;
|
||||||
|
desaKelurahan: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JenisKelamin = "Laki-laki" | "Perempuan";
|
||||||
|
export type Agama =
|
||||||
|
| "Islam"
|
||||||
|
| "Kristen"
|
||||||
|
| "Katolik"
|
||||||
|
| "Hindu"
|
||||||
|
| "Buddha"
|
||||||
|
| "Konghucu"
|
||||||
|
| "Lainnya";
|
||||||
|
export type StatusPerkawinan =
|
||||||
|
| "Belum Kawin"
|
||||||
|
| "Kawin"
|
||||||
|
| "Cerai Hidup"
|
||||||
|
| "Cerai Mati";
|
||||||
|
|
||||||
|
export interface DataPemohon {
|
||||||
|
namaLengkap: string;
|
||||||
|
nik: string; // expected 16 digits
|
||||||
|
tempatTanggalLahir: string;
|
||||||
|
jenisKelamin: JenisKelamin;
|
||||||
|
agama: Agama;
|
||||||
|
statusPerkawinan: StatusPerkawinan;
|
||||||
|
pekerjaan: string;
|
||||||
|
alamat: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Pengesahan {
|
||||||
|
tempatTerbit: string;
|
||||||
|
tanggalTerbit: string; // ISO date
|
||||||
|
kepalaDesaLurah: string;
|
||||||
|
jabatan: string;
|
||||||
|
tandaTanganCap: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SKCKPengantarForm {
|
||||||
|
formTitle: string;
|
||||||
|
description?: string;
|
||||||
|
instansi: Instansi;
|
||||||
|
nomorSurat: string;
|
||||||
|
dataPemohon: DataPemohon;
|
||||||
|
keterangan: string;
|
||||||
|
keperluan: string;
|
||||||
|
berlakuHingga: string; // ISO date
|
||||||
|
pengesahan: Pengesahan;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
|
// Helper validation
|
||||||
|
// ----------------------
|
||||||
|
|
||||||
|
function isValidNIK(nik: string) {
|
||||||
|
// strict: 16 digits numeric
|
||||||
|
return /^\d{16}$/.test(nik);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
|
// Reusable small component
|
||||||
|
// ----------------------
|
||||||
|
|
||||||
|
function SectionCard({
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
description?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card shadow="sm" radius="md" withBorder aria-labelledby={title}>
|
||||||
|
<Group justify="apart" align="center" style={{ marginBottom: 10 }}>
|
||||||
|
<Group>
|
||||||
|
{icon}
|
||||||
|
<div>
|
||||||
|
<Title order={4} id={title} style={{ lineHeight: 1 }}>
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
{description && (
|
||||||
|
<Text size="sm" color="dimmed">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<Divider my="sm" />
|
||||||
|
<div>{children}</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
|
// Main Page Component
|
||||||
|
// ----------------------
|
||||||
|
|
||||||
|
export default function FormSuratKeteranganKelakuanBaik() {
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<any>({
|
||||||
|
initialValues: {
|
||||||
|
formTitle: "Surat Keterangan Kelakuan Baik (Pengantar SKCK)",
|
||||||
|
description:
|
||||||
|
"Blangko resmi dari Kelurahan/Desa sebagai pengantar untuk pembuatan SKCK di Kepolisian.",
|
||||||
|
instansi: {
|
||||||
|
kabupatenKota: "",
|
||||||
|
kecamatan: "",
|
||||||
|
desaKelurahan: "",
|
||||||
|
},
|
||||||
|
nomorSurat: "",
|
||||||
|
dataPemohon: {
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
tempatTanggalLahir: "",
|
||||||
|
jenisKelamin: "Laki-laki",
|
||||||
|
agama: "Islam",
|
||||||
|
statusPerkawinan: "Belum Kawin",
|
||||||
|
pekerjaan: "",
|
||||||
|
alamat: "",
|
||||||
|
},
|
||||||
|
keterangan: "",
|
||||||
|
keperluan: "",
|
||||||
|
berlakuHingga: new Date().toISOString().slice(0, 10),
|
||||||
|
pengesahan: {
|
||||||
|
tempatTerbit: "",
|
||||||
|
tanggalTerbit: new Date().toISOString().slice(0, 10),
|
||||||
|
kepalaDesaLurah: "",
|
||||||
|
jabatan: "",
|
||||||
|
tandaTanganCap: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
validate: {
|
||||||
|
// top-level validations
|
||||||
|
nomorSurat: (value) =>
|
||||||
|
value.trim().length === 0 ? "Nomor surat wajib diisi" : null,
|
||||||
|
keperluan: (value) =>
|
||||||
|
value.trim().length === 0 ? "Keperluan wajib diisi" : null,
|
||||||
|
keterangan: (value) =>
|
||||||
|
value.trim().length === 0 ? "Keterangan wajib diisi" : null,
|
||||||
|
|
||||||
|
"dataPemohon.namaLengkap": (value: string) =>
|
||||||
|
!value || value.trim().length < 3
|
||||||
|
? "Nama lengkap minimal 3 karakter"
|
||||||
|
: null,
|
||||||
|
"dataPemohon.nik": (value: string) =>
|
||||||
|
!isValidNIK(value) ? "NIK harus 16 digit angka tanpa spasi" : null,
|
||||||
|
"dataPemohon.tempatTanggalLahir": (value: string) =>
|
||||||
|
!value ? "Tempat dan tanggal lahir wajib diisi" : null,
|
||||||
|
"dataPemohon.pekerjaan": (value: string) =>
|
||||||
|
!value ? "Pekerjaan wajib diisi" : null,
|
||||||
|
"dataPemohon.alamat": (value: string) =>
|
||||||
|
!value ? "Alamat wajib diisi" : null,
|
||||||
|
|
||||||
|
"pengesahan.tempatTerbit": (value: string) =>
|
||||||
|
!value ? "Tempat terbit wajib diisi" : null,
|
||||||
|
"pengesahan.tanggalTerbit": (value: string) =>
|
||||||
|
!value ? "Tanggal terbit wajib diisi" : null,
|
||||||
|
"pengesahan.kepalaDesaLurah": (value: string) =>
|
||||||
|
!value ? "Nama penandatangan wajib diisi" : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const agamaOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
"Islam",
|
||||||
|
"Kristen",
|
||||||
|
"Katolik",
|
||||||
|
"Hindu",
|
||||||
|
"Buddha",
|
||||||
|
"Konghucu",
|
||||||
|
"Lainnya",
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusOptions = useMemo(
|
||||||
|
() => ["Belum Kawin", "Kawin", "Cerai Hidup", "Cerai Mati"],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
// simulate API call - replace with your actual endpoint
|
||||||
|
await new Promise((r) => setTimeout(r, 800));
|
||||||
|
|
||||||
|
showNotification({
|
||||||
|
title: "Berhasil",
|
||||||
|
message: "Surat pengantar berhasil disimpan.",
|
||||||
|
icon: <IconCheck size={18} />,
|
||||||
|
color: "green",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optionally redirect or reset form
|
||||||
|
// router.push('/surat/list')
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
showNotification({
|
||||||
|
title: "Gagal",
|
||||||
|
message: "Terjadi kesalahan saat menyimpan. Coba lagi.",
|
||||||
|
icon: <IconX size={18} />,
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="lg" py="xl">
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Group justify="apart" align="center">
|
||||||
|
<div>
|
||||||
|
<Title order={2}>
|
||||||
|
Surat Keterangan Kelakuan Baik (Pengantar SKCK)
|
||||||
|
</Title>
|
||||||
|
<Text color="dimmed">
|
||||||
|
Blangko resmi dari Kelurahan/Desa sebagai pengantar pembuatan SKCK
|
||||||
|
di Kepolisian.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" radius="sm">
|
||||||
|
Formulir Resmi
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(() => handleSubmit())}
|
||||||
|
aria-label="Formulir Pengantar SKCK"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<SimpleGrid cols={3} spacing="md">
|
||||||
|
<SectionCard
|
||||||
|
title="Identitas Instansi"
|
||||||
|
icon={<IconBuildingBank size={28} />}
|
||||||
|
description="Informasi penerbit (kelurahan/desa)"
|
||||||
|
>
|
||||||
|
<SimpleGrid cols={3} spacing="sm">
|
||||||
|
<TextInput
|
||||||
|
label="Kabupaten / Kota"
|
||||||
|
placeholder="Contoh: Badung"
|
||||||
|
leftSection={<IconMapPin size={16} />}
|
||||||
|
required
|
||||||
|
{...form.getInputProps("instansi.kabupatenKota")}
|
||||||
|
aria-label="kabupaten-kota"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Kecamatan"
|
||||||
|
placeholder="Contoh: Kuta"
|
||||||
|
leftSection={<IconMapPin size={16} />}
|
||||||
|
required
|
||||||
|
{...form.getInputProps("instansi.kecamatan")}
|
||||||
|
aria-label="kecamatan"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Desa / Kelurahan"
|
||||||
|
placeholder="Contoh: Tuban"
|
||||||
|
leftSection={<IconMapPin size={16} />}
|
||||||
|
required
|
||||||
|
{...form.getInputProps("instansi.desaKelurahan")}
|
||||||
|
aria-label="desa-kelurahan"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
<SectionCard
|
||||||
|
title="Nomor & Masa Berlaku"
|
||||||
|
icon={<IconFileText size={28} />}
|
||||||
|
description="Nomor registrasi surat dan tanggal masa berlaku"
|
||||||
|
>
|
||||||
|
<SimpleGrid cols={2} spacing="sm">
|
||||||
|
<TextInput
|
||||||
|
label="Nomor Surat"
|
||||||
|
placeholder="2025/KT-001/123"
|
||||||
|
leftSection={<IconFileText size={16} />}
|
||||||
|
required
|
||||||
|
{...form.getInputProps("nomorSurat")}
|
||||||
|
aria-label="nomor-surat"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DatePicker
|
||||||
|
{...form.getInputProps("berlakuHingga")}
|
||||||
|
value={
|
||||||
|
form.values.berlakuHingga
|
||||||
|
? new Date(form.values.berlakuHingga)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onChange={(d) =>
|
||||||
|
form.setFieldValue("berlakuHingga", d as any)
|
||||||
|
}
|
||||||
|
aria-label="berlaku-hingga"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
<SectionCard
|
||||||
|
title="Data Pemohon"
|
||||||
|
icon={<IconUser size={28} />}
|
||||||
|
description="Data pemohon sesuai KTP"
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
<SimpleGrid cols={2}>
|
||||||
|
<TextInput
|
||||||
|
label="Nama Lengkap"
|
||||||
|
placeholder="Nama sesuai KTP"
|
||||||
|
required
|
||||||
|
{...form.getInputProps("dataPemohon.namaLengkap")}
|
||||||
|
leftSection={<IconUser size={16} />}
|
||||||
|
aria-label="nama-lengkap"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="NIK (16 digit)"
|
||||||
|
placeholder="Contoh: 3574xxxxxxxxxxxx"
|
||||||
|
required
|
||||||
|
{...form.getInputProps("dataPemohon.nik")}
|
||||||
|
leftSection={<IconCards size={16} />}
|
||||||
|
aria-label="nik"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<SimpleGrid cols={2} spacing={"md"}>
|
||||||
|
<TextInput
|
||||||
|
label="Tempat, Tanggal Lahir"
|
||||||
|
placeholder="Contoh: Denpasar, 01 Januari 1990"
|
||||||
|
required
|
||||||
|
{...form.getInputProps("dataPemohon.tempatTanggalLahir")}
|
||||||
|
leftSection={<IconCalendar size={16} />}
|
||||||
|
aria-label="tempat-tanggal-lahir"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Jenis Kelamin"
|
||||||
|
data={[
|
||||||
|
{ value: "Laki-laki", label: "Laki-laki" },
|
||||||
|
{ value: "Perempuan", label: "Perempuan" },
|
||||||
|
]}
|
||||||
|
{...form.getInputProps("dataPemohon.jenisKelamin")}
|
||||||
|
aria-label="jenis-kelamin"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<SimpleGrid cols={3} spacing={"md"}>
|
||||||
|
<Select
|
||||||
|
label="Agama"
|
||||||
|
data={agamaOptions.map((a) => ({ value: a, label: a }))}
|
||||||
|
{...form.getInputProps("dataPemohon.agama")}
|
||||||
|
aria-label="agama"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Status Perkawinan"
|
||||||
|
data={statusOptions.map((s) => ({ value: s, label: s }))}
|
||||||
|
{...form.getInputProps("dataPemohon.statusPerkawinan")}
|
||||||
|
aria-label="status-perkawinan"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Pekerjaan"
|
||||||
|
placeholder="Contoh: Buruh, PNS, Wiraswasta"
|
||||||
|
{...form.getInputProps("dataPemohon.pekerjaan")}
|
||||||
|
aria-label="pekerjaan"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Alamat Domisili"
|
||||||
|
placeholder="Alamat lengkap sesuai KTP"
|
||||||
|
autosize
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("dataPemohon.alamat")}
|
||||||
|
aria-label="alamat"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
<SectionCard
|
||||||
|
title="Keterangan & Keperluan"
|
||||||
|
icon={<IconChecklist size={28} />}
|
||||||
|
description="Pernyataan resmi kelurahan/desa"
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Textarea
|
||||||
|
label="Keterangan"
|
||||||
|
placeholder="Contoh: Pemohon berkelakuan baik..."
|
||||||
|
minRows={3}
|
||||||
|
required
|
||||||
|
{...form.getInputProps("keterangan")}
|
||||||
|
leftSection={<IconFileText size={16} />}
|
||||||
|
aria-label="keterangan"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Keperluan"
|
||||||
|
placeholder="Contoh: Melamar pekerjaan di PT. X"
|
||||||
|
required
|
||||||
|
{...form.getInputProps("keperluan")}
|
||||||
|
aria-label="keperluan"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
<SectionCard
|
||||||
|
title="Pengesahan & Penandatangan"
|
||||||
|
icon={<IconFile size={28} />}
|
||||||
|
description="Informasi pejabat yang menandatangani dan cap instansi"
|
||||||
|
>
|
||||||
|
<SimpleGrid cols={2} spacing={"md"}>
|
||||||
|
<TextInput
|
||||||
|
label="Tempat Terbit"
|
||||||
|
placeholder="Contoh: Kuta"
|
||||||
|
{...form.getInputProps("pengesahan.tempatTerbit")}
|
||||||
|
aria-label="tempat-terbit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DatePicker
|
||||||
|
value={
|
||||||
|
form.values.pengesahan.tanggalTerbit
|
||||||
|
? new Date(form.values.pengesahan.tanggalTerbit)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
{...form.getInputProps("pengesahan.tanggalTerbit")}
|
||||||
|
aria-label="tanggal-terbit"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<SimpleGrid cols={2} spacing={"md"}>
|
||||||
|
<TextInput
|
||||||
|
label="Nama Kepala Desa / Lurah"
|
||||||
|
placeholder="Nama penandatangan"
|
||||||
|
{...form.getInputProps("pengesahan.kepalaDesaLurah")}
|
||||||
|
aria-label="kepala-desa"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Jabatan"
|
||||||
|
placeholder="Contoh: Kepala Desa"
|
||||||
|
{...form.getInputProps("pengesahan.jabatan")}
|
||||||
|
aria-label="jabatan"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Tanda Tangan & Cap (keterangan)"
|
||||||
|
placeholder="Contoh: Tanda tangan basah, cap stempel instansi"
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("pengesahan.tandaTanganCap")}
|
||||||
|
aria-label="tanda-tangan-cap"
|
||||||
|
/>
|
||||||
|
</SectionCard>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<Group justify="right" mt="md">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => form.reset()}
|
||||||
|
leftSection={<IconX size={16} />}
|
||||||
|
aria-label="reset-form"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
leftSection={<IconChecklist size={16} />}
|
||||||
|
loading={submitting}
|
||||||
|
aria-label="submit-form"
|
||||||
|
>
|
||||||
|
Simpan & Cetak
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
Pastikan semua data sesuai dokumen resmi. SKCK biasanya berlaku 3
|
||||||
|
bulan sejak diterbitkan.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
622
src/pages/darmasaba/form_surat_keterangan_penghasilan.tsx
Normal file
622
src/pages/darmasaba/form_surat_keterangan_penghasilan.tsx
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
NumberInput,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { DatePicker } from "@mantine/dates";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import {
|
||||||
|
IconBuildingStore,
|
||||||
|
IconCalendarEvent,
|
||||||
|
IconCheck,
|
||||||
|
IconCurrencyDollar,
|
||||||
|
IconFileText,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconRefresh,
|
||||||
|
IconUser,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Types that mirror the JSON schema
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
|
type Currency = "IDR" | string;
|
||||||
|
|
||||||
|
interface IncomeValue {
|
||||||
|
value: number;
|
||||||
|
currency: Currency;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Issuer {
|
||||||
|
name: string;
|
||||||
|
position: string;
|
||||||
|
company: string;
|
||||||
|
address: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Employee {
|
||||||
|
fullName: string;
|
||||||
|
nik: string;
|
||||||
|
placeOfBirth: string;
|
||||||
|
dateOfBirth?: Date | null;
|
||||||
|
position: string;
|
||||||
|
employmentStatus: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Validity {
|
||||||
|
startDate?: Date | null;
|
||||||
|
endDate?: Date | null;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Signatory {
|
||||||
|
name: string;
|
||||||
|
position: string;
|
||||||
|
signature: string; // could be base64, url, or a typed name
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CertificateFormSchema {
|
||||||
|
documentType: string;
|
||||||
|
documentNumber: { value: string; description?: string };
|
||||||
|
issuer: Issuer;
|
||||||
|
employee: Employee;
|
||||||
|
incomeDetails: {
|
||||||
|
basicSalary: IncomeValue;
|
||||||
|
allowances: IncomeValue;
|
||||||
|
deductions: IncomeValue;
|
||||||
|
netIncome: IncomeValue;
|
||||||
|
};
|
||||||
|
validity: Validity;
|
||||||
|
purpose: { value: string; description?: string };
|
||||||
|
signatory: Signatory;
|
||||||
|
issueDate?: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Reusable small components
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
|
function SectionTitle({
|
||||||
|
title,
|
||||||
|
icon: Icon,
|
||||||
|
subtitle,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
icon?: any;
|
||||||
|
subtitle?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Group justify="apart" style={{ width: "100%", marginBottom: 8 }}>
|
||||||
|
<Group>
|
||||||
|
{Icon && <Icon size={18} />}
|
||||||
|
<div>
|
||||||
|
<Title order={5} style={{ margin: 0 }}>
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
{subtitle && (
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single field renderer that maps JSON-like field meta to Mantine controls
|
||||||
|
function FormField({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
children,
|
||||||
|
leftIcon,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
leftIcon?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Stack gap="md" style={{ width: "100%" }}>
|
||||||
|
<Group gap="md" align="flex-start">
|
||||||
|
{leftIcon}
|
||||||
|
<Text fw={600}>{label}</Text>
|
||||||
|
</Group>
|
||||||
|
<div>{children}</div>
|
||||||
|
{description && (
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Main form component
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
|
export default function SuratKeteranganPenghasilan() {
|
||||||
|
// default values: smart defaults (some example data to guide user)
|
||||||
|
const form = useForm<CertificateFormSchema>({
|
||||||
|
initialValues: {
|
||||||
|
documentType: "Surat Keterangan Penghasilan",
|
||||||
|
documentNumber: {
|
||||||
|
value: "",
|
||||||
|
description:
|
||||||
|
"Nomor surat keterangan penghasilan yang dikeluarkan oleh instansi/perusahaan.",
|
||||||
|
},
|
||||||
|
issuer: {
|
||||||
|
name: "",
|
||||||
|
position: "",
|
||||||
|
company: "",
|
||||||
|
address: "",
|
||||||
|
description:
|
||||||
|
"Data pihak yang mengeluarkan surat keterangan penghasilan, biasanya HRD/atasan langsung/perusahaan.",
|
||||||
|
},
|
||||||
|
employee: {
|
||||||
|
fullName: "",
|
||||||
|
nik: "",
|
||||||
|
placeOfBirth: "",
|
||||||
|
dateOfBirth: null,
|
||||||
|
position: "",
|
||||||
|
employmentStatus: "Karyawan Tetap",
|
||||||
|
description: "Data karyawan/pegawai yang bersangkutan.",
|
||||||
|
},
|
||||||
|
incomeDetails: {
|
||||||
|
basicSalary: {
|
||||||
|
value: 0,
|
||||||
|
currency: "IDR",
|
||||||
|
description: "Gaji pokok per bulan.",
|
||||||
|
},
|
||||||
|
allowances: {
|
||||||
|
value: 0,
|
||||||
|
currency: "IDR",
|
||||||
|
description:
|
||||||
|
"Tunjangan-tunjangan tetap (transport, makan, jabatan, dll).",
|
||||||
|
},
|
||||||
|
deductions: {
|
||||||
|
value: 0,
|
||||||
|
currency: "IDR",
|
||||||
|
description: "Potongan gaji bulanan (BPJS, pajak, koperasi, dll).",
|
||||||
|
},
|
||||||
|
netIncome: {
|
||||||
|
value: 0,
|
||||||
|
currency: "IDR",
|
||||||
|
description: "Total penghasilan bersih per bulan setelah potongan.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validity: {
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
description: "Masa berlaku surat keterangan penghasilan.",
|
||||||
|
},
|
||||||
|
purpose: {
|
||||||
|
value: "",
|
||||||
|
description:
|
||||||
|
"Tujuan diterbitkannya surat keterangan penghasilan (contoh: pengajuan KPR, kredit, beasiswa, dll).",
|
||||||
|
},
|
||||||
|
signatory: {
|
||||||
|
name: "",
|
||||||
|
position: "",
|
||||||
|
signature: "",
|
||||||
|
description:
|
||||||
|
"Pihak yang menandatangani surat resmi, biasanya pimpinan perusahaan atau pejabat berwenang.",
|
||||||
|
},
|
||||||
|
issueDate: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
// lightweight validation rules
|
||||||
|
validate: {
|
||||||
|
documentNumber: (val) =>
|
||||||
|
val.value.trim().length === 0 ? "Nomor dokumen diperlukan" : null,
|
||||||
|
employee: (val) =>
|
||||||
|
(val.fullName.trim().length === 0
|
||||||
|
? { fullName: "Nama lengkap diperlukan" }
|
||||||
|
: null) as any,
|
||||||
|
incomeDetails: (val) => {
|
||||||
|
if (val.netIncome.value <= 0)
|
||||||
|
return {
|
||||||
|
netIncome: { value: "Net income harus lebih dari 0" },
|
||||||
|
} as any;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// helper to compute net income automatically when basic/allowances/deductions change
|
||||||
|
function recalcNetIncome() {
|
||||||
|
const basic = form.values.incomeDetails.basicSalary.value || 0;
|
||||||
|
const allowances = form.values.incomeDetails.allowances.value || 0;
|
||||||
|
const deductions = form.values.incomeDetails.deductions.value || 0;
|
||||||
|
const net = basic + allowances - deductions;
|
||||||
|
form.setFieldValue(
|
||||||
|
"incomeDetails.netIncome.value",
|
||||||
|
Math.max(0, Math.round(net)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit handler: production apps would call API here
|
||||||
|
function handleSubmit(values: CertificateFormSchema) {
|
||||||
|
// simulate transformation: format currency, dates, etc.
|
||||||
|
const payload = {
|
||||||
|
...values,
|
||||||
|
issueDate: values.issueDate
|
||||||
|
? values.issueDate.toISOString().slice(0, 10)
|
||||||
|
: null,
|
||||||
|
employee: {
|
||||||
|
...values.employee,
|
||||||
|
dateOfBirth: values.employee.dateOfBirth
|
||||||
|
? values.employee.dateOfBirth.toISOString().slice(0, 10)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
validity: {
|
||||||
|
startDate: values.validity.startDate
|
||||||
|
? values.validity.startDate.toISOString().slice(0, 10)
|
||||||
|
: null,
|
||||||
|
endDate: values.validity.endDate
|
||||||
|
? values.validity.endDate.toISOString().slice(0, 10)
|
||||||
|
: null,
|
||||||
|
description: values.validity.description,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// For demo: log and show a subtle success
|
||||||
|
// Replace with real API call (fetch/axios) in production.
|
||||||
|
|
||||||
|
console.log("Submitting Surat Keterangan Penghasilan:", payload);
|
||||||
|
alert(
|
||||||
|
"Form submitted — cek console untuk payload (demo).\nUntuk produksi, hubungkan endpoint API.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="md" w={"100%"}>
|
||||||
|
<form onSubmit={form.onSubmit((v) => handleSubmit(v))}>
|
||||||
|
<Card shadow="sm" radius="md" padding="lg" withBorder>
|
||||||
|
<SectionTitle
|
||||||
|
title="Surat Keterangan Penghasilan"
|
||||||
|
icon={IconFileText}
|
||||||
|
subtitle="Isi data sesuai blangko resmi. Gunakan tab untuk berpindah antar field."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Grid gutter="md">
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
label="Tipe Dokumen"
|
||||||
|
leftIcon={<IconFileText size={18} />}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
{...form.getInputProps("documentType.value" as any)}
|
||||||
|
value={form.values.documentType}
|
||||||
|
onChange={(e) =>
|
||||||
|
form.setFieldValue("documentType", e.target.value as any)
|
||||||
|
}
|
||||||
|
placeholder="Surat Keterangan Penghasilan"
|
||||||
|
aria-label="Tipe dokumen"
|
||||||
|
/>
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
Jenis dokumen (tidak wajib diubah).
|
||||||
|
</Text>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
label="Nomor Dokumen"
|
||||||
|
leftIcon={<IconInfoCircle size={18} />}
|
||||||
|
description={form.values.documentNumber.description}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="e.g. SKP-2025-0001"
|
||||||
|
{...form.getInputProps("documentNumber.value" as any)}
|
||||||
|
aria-label="Nomor dokumen"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Accordion variant="separated">
|
||||||
|
<Accordion.Item value="issuer">
|
||||||
|
<Accordion.Control>
|
||||||
|
{" "}
|
||||||
|
<Group>
|
||||||
|
{" "}
|
||||||
|
<IconBuildingStore size={16} />{" "}
|
||||||
|
<Text fw={700}>Pihak Penerbit</Text>{" "}
|
||||||
|
</Group>{" "}
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Nama Penerbit"
|
||||||
|
placeholder="Contoh: PT. Contoh Perkasa"
|
||||||
|
{...form.getInputProps("issuer.name" as any)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Jabatan"
|
||||||
|
placeholder="HRD / Manager"
|
||||||
|
{...form.getInputProps("issuer.position" as any)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Textarea
|
||||||
|
label="Alamat Perusahaan"
|
||||||
|
placeholder="Alamat lengkap penerbit"
|
||||||
|
{...form.getInputProps("issuer.address" as any)}
|
||||||
|
minRows={2}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
<Accordion.Item value="employee">
|
||||||
|
<Accordion.Control>
|
||||||
|
{" "}
|
||||||
|
<Group>
|
||||||
|
{" "}
|
||||||
|
<IconUser size={16} />{" "}
|
||||||
|
<Text fw={700}>Data Karyawan</Text>{" "}
|
||||||
|
</Group>{" "}
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Nama Lengkap"
|
||||||
|
placeholder="Nama sesuai KTP"
|
||||||
|
{...form.getInputProps("employee.fullName" as any)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="NIK"
|
||||||
|
placeholder="Nomor Induk KTP"
|
||||||
|
{...form.getInputProps("employee.nik" as any)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Tempat Lahir"
|
||||||
|
placeholder="Kota kelahiran"
|
||||||
|
{...form.getInputProps(
|
||||||
|
"employee.placeOfBirth" as any,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<DatePicker
|
||||||
|
{...form.getInputProps("employee.dateOfBirth" as any)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Jabatan"
|
||||||
|
placeholder="Posisi di perusahaan"
|
||||||
|
{...form.getInputProps("employee.position" as any)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Select
|
||||||
|
label="Status Kerja"
|
||||||
|
data={[
|
||||||
|
"Karyawan Tetap",
|
||||||
|
"Karyawan Kontrak",
|
||||||
|
"Magang",
|
||||||
|
"Konsultan",
|
||||||
|
]}
|
||||||
|
{...form.getInputProps(
|
||||||
|
"employee.employmentStatus" as any,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Textarea
|
||||||
|
label="Catatan / Deskripsi"
|
||||||
|
placeholder="Opsional"
|
||||||
|
{...form.getInputProps("employee.description" as any)}
|
||||||
|
minRows={2}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
<Accordion.Item value="income">
|
||||||
|
<Accordion.Control>
|
||||||
|
{" "}
|
||||||
|
<Group>
|
||||||
|
{" "}
|
||||||
|
<IconCurrencyDollar size={16} />{" "}
|
||||||
|
<Text fw={700}>Rincian Penghasilan</Text>{" "}
|
||||||
|
</Group>{" "}
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<NumberInput
|
||||||
|
label="Gaji Pokok (per bulan)"
|
||||||
|
pattern="^[0-9]*$"
|
||||||
|
min={0}
|
||||||
|
placeholder="0"
|
||||||
|
{...form.getInputProps(
|
||||||
|
"incomeDetails.basicSalary.value" as any,
|
||||||
|
)}
|
||||||
|
onBlur={recalcNetIncome}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<NumberInput
|
||||||
|
label="Tunjangan (per bulan)"
|
||||||
|
pattern="^[0-9]*$"
|
||||||
|
min={0}
|
||||||
|
placeholder="0"
|
||||||
|
{...form.getInputProps(
|
||||||
|
"incomeDetails.allowances.value" as any,
|
||||||
|
)}
|
||||||
|
onBlur={recalcNetIncome}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<NumberInput
|
||||||
|
label="Potongan (per bulan)"
|
||||||
|
pattern="^[0-9]*$"
|
||||||
|
min={0}
|
||||||
|
placeholder="0"
|
||||||
|
{...form.getInputProps(
|
||||||
|
"incomeDetails.deductions.value" as any,
|
||||||
|
)}
|
||||||
|
onBlur={recalcNetIncome}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<NumberInput
|
||||||
|
label="Penghasilan Bersih (per bulan)"
|
||||||
|
readOnly
|
||||||
|
value={form.values.incomeDetails.netIncome.value}
|
||||||
|
pattern="^[0-9]*$"
|
||||||
|
min={0}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{form.values.incomeDetails.basicSalary.description}
|
||||||
|
</Text>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
<Accordion.Item value="validity">
|
||||||
|
<Accordion.Control>
|
||||||
|
{" "}
|
||||||
|
<Group>
|
||||||
|
{" "}
|
||||||
|
<IconCalendarEvent size={16} />{" "}
|
||||||
|
<Text fw={700}>Masa Berlaku</Text>{" "}
|
||||||
|
</Group>{" "}
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<DatePicker
|
||||||
|
{...form.getInputProps("validity.startDate" as any)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<DatePicker
|
||||||
|
{...form.getInputProps("validity.endDate" as any)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Textarea
|
||||||
|
label="Keterangan Masa Berlaku"
|
||||||
|
placeholder="Contoh: Berlaku 1 tahun sejak diterbitkan"
|
||||||
|
{...form.getInputProps("validity.description" as any)}
|
||||||
|
minRows={2}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
<Accordion.Item value="signatory">
|
||||||
|
<Accordion.Control>
|
||||||
|
{" "}
|
||||||
|
<Group>
|
||||||
|
{" "}
|
||||||
|
<IconCheck size={16} />{" "}
|
||||||
|
<Text fw={700}>Penandatangan</Text>{" "}
|
||||||
|
</Group>{" "}
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Nama Penandatangan"
|
||||||
|
placeholder="Nama pejabat"
|
||||||
|
{...form.getInputProps("signatory.name" as any)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Jabatan Penandatangan"
|
||||||
|
placeholder="Direktur / Manager"
|
||||||
|
{...form.getInputProps("signatory.position" as any)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<TextInput
|
||||||
|
label="Tanda Tangan (nama atau link)"
|
||||||
|
placeholder="bila tersedia"
|
||||||
|
{...form.getInputProps("signatory.signature" as any)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Divider my="sm" />
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<DatePicker {...form.getInputProps("issueDate" as any)} />
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label="Tujuan Penggunaan"
|
||||||
|
placeholder="Contoh: Pengajuan KPR"
|
||||||
|
{...form.getInputProps("purpose.value" as any)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
leftSection={<IconRefresh size={16} />}
|
||||||
|
onClick={() => form.reset()}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">Simpan & Cetak</Button>
|
||||||
|
</Group>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
502
src/pages/darmasaba/form_surat_keterangan_tempat_usaha.tsx
Normal file
502
src/pages/darmasaba/form_surat_keterangan_tempat_usaha.tsx
Normal file
@@ -0,0 +1,502 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// pages/sktu/page.tsx
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Card,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
Divider,
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
SimpleGrid,
|
||||||
|
TextInput,
|
||||||
|
NumberInput,
|
||||||
|
Textarea,
|
||||||
|
Select,
|
||||||
|
FileInput,
|
||||||
|
Button,
|
||||||
|
Notification,
|
||||||
|
Tooltip,
|
||||||
|
Loader,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { DatePicker } from "@mantine/dates";
|
||||||
|
import {
|
||||||
|
IconCheck,
|
||||||
|
IconX,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconUser,
|
||||||
|
IconId,
|
||||||
|
IconMapPin,
|
||||||
|
IconPhone,
|
||||||
|
IconBuildingStore,
|
||||||
|
IconCategory,
|
||||||
|
IconSquarePlus,
|
||||||
|
IconRuler,
|
||||||
|
IconUsers,
|
||||||
|
IconFileText,
|
||||||
|
IconSignature,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
// types/form-types.ts
|
||||||
|
export type StatusTempat =
|
||||||
|
| "Milik Sendiri"
|
||||||
|
| "Kontrak/Sewa"
|
||||||
|
| "Pinjam Pakai"
|
||||||
|
| "Lainnya";
|
||||||
|
|
||||||
|
export interface SKTUFormValues {
|
||||||
|
// Data Pemohon
|
||||||
|
namaLengkap: string;
|
||||||
|
nik: string;
|
||||||
|
tempatTanggalLahir: string;
|
||||||
|
alamatPemohon: string;
|
||||||
|
telepon: string;
|
||||||
|
|
||||||
|
// Data Usaha
|
||||||
|
namaUsaha: string;
|
||||||
|
jenisUsaha: string;
|
||||||
|
bidangUsaha: string;
|
||||||
|
alamatUsaha: string;
|
||||||
|
statusTempat: StatusTempat;
|
||||||
|
luasTempat: string; // kept as string to allow free-text like "36" or "36 (sebagian)"
|
||||||
|
jumlahKaryawan: number;
|
||||||
|
npwp?: string;
|
||||||
|
|
||||||
|
// Keterangan Tambahan
|
||||||
|
keteranganTambahan?: string;
|
||||||
|
|
||||||
|
// Tanggal pengajuan
|
||||||
|
tanggalPengajuan: Date | null;
|
||||||
|
|
||||||
|
// Pemohon (penandatangan)
|
||||||
|
pemohon_nama: string;
|
||||||
|
pemohon_tandaTangan: File | null;
|
||||||
|
|
||||||
|
// Pengesahan
|
||||||
|
kepalaDesaLurah: string;
|
||||||
|
camat: string;
|
||||||
|
petugasRegistrasi: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FormSuratKeteranganTempatUsaha() {
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const form = useForm<SKTUFormValues>({
|
||||||
|
initialValues: {
|
||||||
|
// dataPemohon
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
tempatTanggalLahir: "",
|
||||||
|
alamatPemohon: "",
|
||||||
|
telepon: "",
|
||||||
|
// dataUsaha
|
||||||
|
namaUsaha: "",
|
||||||
|
jenisUsaha: "",
|
||||||
|
bidangUsaha: "",
|
||||||
|
alamatUsaha: "",
|
||||||
|
statusTempat: "Milik Sendiri",
|
||||||
|
luasTempat: "",
|
||||||
|
jumlahKaryawan: 0,
|
||||||
|
npwp: "",
|
||||||
|
// keteranganTambahan
|
||||||
|
keteranganTambahan: "",
|
||||||
|
// tanggalPengajuan
|
||||||
|
tanggalPengajuan: null,
|
||||||
|
// pemohon
|
||||||
|
pemohon_nama: "",
|
||||||
|
pemohon_tandaTangan: null,
|
||||||
|
// pengesahan
|
||||||
|
kepalaDesaLurah: "",
|
||||||
|
camat: "",
|
||||||
|
petugasRegistrasi: "",
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
namaLengkap: (v: any) =>
|
||||||
|
v.trim().length > 0 ? null : "Nama lengkap pemohon diperlukan.",
|
||||||
|
nik: (v: any) => {
|
||||||
|
const digits = v.replace(/\D/g, "");
|
||||||
|
if (!digits) return "NIK diperlukan.";
|
||||||
|
if (digits.length !== 16) return "NIK harus 16 digit angka.";
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
tempatTanggalLahir: (v: any) =>
|
||||||
|
v.trim().length > 0 ? null : "Tempat dan tanggal lahir diperlukan.",
|
||||||
|
alamatPemohon: (v: any) =>
|
||||||
|
v.trim().length > 0 ? null : "Alamat sesuai KTP diperlukan.",
|
||||||
|
telepon: (v: any) => {
|
||||||
|
const digits = v.replace(/\D/g, "");
|
||||||
|
if (!digits) return "Nomor telepon diperlukan.";
|
||||||
|
if (digits.length < 8) return "Nomor telepon tampak terlalu pendek.";
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
namaUsaha: (v: any) => (v.trim() ? null : "Nama usaha diperlukan."),
|
||||||
|
jenisUsaha: (v: any) => (v.trim() ? null : "Jenis usaha diperlukan."),
|
||||||
|
alamatUsaha: (v: any) => (v.trim() ? null : "Alamat usaha diperlukan."),
|
||||||
|
luasTempat: (v: any) => {
|
||||||
|
if (!v.trim()) return "Luas tempat diperlukan.";
|
||||||
|
const n = Number(v);
|
||||||
|
if (Number.isNaN(n) || n <= 0)
|
||||||
|
return "Masukkan luas valid (nomor > 0).";
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
jumlahKaryawan: (v: any) =>
|
||||||
|
typeof v === "number" && v >= 0 ? null : "Jumlah karyawan harus >= 0.",
|
||||||
|
tanggalPengajuan: (v: any) =>
|
||||||
|
v ? null : "Tanggal pengajuan harus diisi.",
|
||||||
|
pemohon_nama: (v: any) =>
|
||||||
|
v.trim() ? null : "Nama pemohon (penandatangan) diperlukan.",
|
||||||
|
kepalaDesaLurah: (v: any) =>
|
||||||
|
v.trim() ? null : "Nama kepala desa/lurah diperlukan.",
|
||||||
|
camat: (v: any) => (v.trim() ? null : "Nama camat diperlukan."),
|
||||||
|
petugasRegistrasi: (v: any) =>
|
||||||
|
v.trim() ? null : "Nama petugas registrasi diperlukan.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit(values: SKTUFormValues) {
|
||||||
|
setError(null);
|
||||||
|
setSuccess(null);
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulasi panggilan API
|
||||||
|
await new Promise((res) => setTimeout(res, 1100));
|
||||||
|
|
||||||
|
// Example: convert file to metadata, transform date to ISO
|
||||||
|
const payload = {
|
||||||
|
...values,
|
||||||
|
tanggalPengajuan: values.tanggalPengajuan
|
||||||
|
? values.tanggalPengajuan.toISOString()
|
||||||
|
: null,
|
||||||
|
pemohon_tandaTangan: values.pemohon_tandaTangan
|
||||||
|
? values.pemohon_tandaTangan.name
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: ganti dengan fetch() ke API nyata
|
||||||
|
console.log("SKTU payload", payload);
|
||||||
|
|
||||||
|
setSuccess("Permohonan SKTU berhasil dikirim.");
|
||||||
|
form.reset();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError("Gagal mengirim permohonan. Coba lagi.");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="sm" py="xl">
|
||||||
|
<Stack justify="xl">
|
||||||
|
<Card shadow="sm" radius="md" withBorder>
|
||||||
|
<Group justify="apart" align="flex-start" wrap="nowrap">
|
||||||
|
<div>
|
||||||
|
<Title order={2} aria-live="polite">
|
||||||
|
Formulir Surat Keterangan Tempat Usaha (SKTU)
|
||||||
|
</Title>
|
||||||
|
<Text size="sm" color="dimmed" mt={6}>
|
||||||
|
Blangko resmi untuk pengajuan SKTU — digunakan sebagai bukti
|
||||||
|
legalitas usaha.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
label="Form ini membantu mengumpulkan data pemohon dan usaha"
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<IconInfoCircle size={20} aria-hidden />
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Divider my="md" />
|
||||||
|
|
||||||
|
<form onSubmit={form.onSubmit(handleSubmit)} aria-label="Form SKTU">
|
||||||
|
<Stack justify="lg">
|
||||||
|
{/* Data Pemohon */}
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
p="md"
|
||||||
|
aria-labelledby="pemohon-heading"
|
||||||
|
>
|
||||||
|
<Group justify="apart" align="flex-start" wrap="nowrap">
|
||||||
|
<Title order={4} id="pemohon-heading">
|
||||||
|
Data Pemohon
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<SimpleGrid cols={1} mt="md">
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Nama Lengkap"
|
||||||
|
placeholder="Nama sesuai KTP"
|
||||||
|
leftSection={<IconUser size={18} />}
|
||||||
|
{...form.getInputProps("namaLengkap")}
|
||||||
|
aria-label="Nama Lengkap"
|
||||||
|
description="Nama pemilik usaha sesuai KTP."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="NIK"
|
||||||
|
placeholder="16 digit NIK"
|
||||||
|
leftSection={<IconId size={18} />}
|
||||||
|
{...form.getInputProps("nik")}
|
||||||
|
aria-label="NIK"
|
||||||
|
inputMode="numeric"
|
||||||
|
description="Masukkan 16 digit NIK (hanya angka)."
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<SimpleGrid cols={1} mt="sm">
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Tempat & Tanggal Lahir"
|
||||||
|
placeholder="Contoh: Denpasar, 01 Januari 1990"
|
||||||
|
leftSection={<IconMapPin size={18} />}
|
||||||
|
{...form.getInputProps("tempatTanggalLahir")}
|
||||||
|
aria-label="Tempat dan tanggal lahir"
|
||||||
|
description="Cantumkan tempat dan tanggal lahir (format bebas)."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Telepon"
|
||||||
|
placeholder="08xxxxxxxxxx"
|
||||||
|
leftSection={<IconPhone size={18} />}
|
||||||
|
{...form.getInputProps("telepon")}
|
||||||
|
aria-label="Telepon"
|
||||||
|
description="Nomor yang dapat dihubungi untuk verifikasi."
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
mt="sm"
|
||||||
|
required
|
||||||
|
label="Alamat Pemohon"
|
||||||
|
placeholder="Alamat lengkap sesuai KTP"
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("alamatPemohon")}
|
||||||
|
aria-label="Alamat Pemohon"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Data Usaha */}
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
p="md"
|
||||||
|
aria-labelledby="usaha-heading"
|
||||||
|
>
|
||||||
|
<Title order={4} id="usaha-heading">
|
||||||
|
Data Usaha
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<SimpleGrid cols={1} mt="md">
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Nama Usaha"
|
||||||
|
placeholder="Nama usaha / toko"
|
||||||
|
leftSection={<IconBuildingStore size={18} />}
|
||||||
|
{...form.getInputProps("namaUsaha")}
|
||||||
|
aria-label="Nama Usaha"
|
||||||
|
description="Nama dagang yang digunakan di lapangan."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Jenis Usaha"
|
||||||
|
placeholder="Contoh: Dagang, Jasa, Produksi"
|
||||||
|
leftSection={<IconCategory size={18} />}
|
||||||
|
{...form.getInputProps("jenisUsaha")}
|
||||||
|
aria-label="Jenis Usaha"
|
||||||
|
description="Pilih/isi jenis usaha secara singkat."
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
mt="sm"
|
||||||
|
label="Bidang Usaha"
|
||||||
|
placeholder="Contoh: Warung makan, bengkel motor"
|
||||||
|
{...form.getInputProps("bidangUsaha")}
|
||||||
|
description="Spesifikkan bidang usaha Anda (opsional tapi direkomendasikan)."
|
||||||
|
leftSection={<IconSquarePlus size={18} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
mt="sm"
|
||||||
|
required
|
||||||
|
label="Alamat Usaha"
|
||||||
|
placeholder="Alamat lengkap tempat usaha"
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("alamatUsaha")}
|
||||||
|
leftSection={<IconMapPin size={18} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SimpleGrid cols={1} mt="sm">
|
||||||
|
<Select
|
||||||
|
label="Status Tempat"
|
||||||
|
data={[
|
||||||
|
"Milik Sendiri",
|
||||||
|
"Kontrak/Sewa",
|
||||||
|
"Pinjam Pakai",
|
||||||
|
"Lainnya",
|
||||||
|
]}
|
||||||
|
{...form.getInputProps("statusTempat")}
|
||||||
|
description="Status kepemilikan atau penggunaan tempat usaha."
|
||||||
|
aria-label="Status Tempat"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Luas Tempat (m²)"
|
||||||
|
placeholder="Contoh: 36"
|
||||||
|
leftSection={<IconRuler size={18} />}
|
||||||
|
{...form.getInputProps("luasTempat")}
|
||||||
|
aria-label="Luas Tempat"
|
||||||
|
description="Isi angka luas bangunan / ruangan (meter persegi)."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
label="Jumlah Karyawan"
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
{...form.getInputProps("jumlahKaryawan")}
|
||||||
|
leftSection={<IconUsers size={18} />}
|
||||||
|
aria-label="Jumlah Karyawan"
|
||||||
|
description="Jumlah pekerja tetap/harian di usaha ini."
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
mt="sm"
|
||||||
|
label="NPWP (jika ada)"
|
||||||
|
placeholder="Nomor NPWP"
|
||||||
|
{...form.getInputProps("npwp")}
|
||||||
|
leftSection={<IconFileText size={18} />}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Keterangan Tambahan & Tanggal */}
|
||||||
|
<Card withBorder radius="md" p="md">
|
||||||
|
<Title order={5}>Keterangan Tambahan & Tanggal</Title>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
mt="sm"
|
||||||
|
label="Keterangan Tambahan (opsional)"
|
||||||
|
placeholder="Informasi tambahan mengenai usaha"
|
||||||
|
minRows={3}
|
||||||
|
{...form.getInputProps("keteranganTambahan")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DatePicker
|
||||||
|
mt="sm"
|
||||||
|
{...form.getInputProps("tanggalPengajuan")}
|
||||||
|
aria-label="Tanggal Pengajuan"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Pemohon (penandatangan) */}
|
||||||
|
<Card withBorder radius="md" p="md">
|
||||||
|
<Title order={5}>Pemohon (Penandatangan)</Title>
|
||||||
|
|
||||||
|
<SimpleGrid cols={1} mt="md">
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Nama Pemohon (penandatangan)"
|
||||||
|
placeholder="Nama yang menandatangani surat"
|
||||||
|
{...form.getInputProps("pemohon_nama")}
|
||||||
|
leftSection={<IconUser size={18} />}
|
||||||
|
aria-label="Nama Penandatangan"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FileInput
|
||||||
|
label="Tanda Tangan (scan / file)"
|
||||||
|
placeholder="Upload file tanda tangan (jpg, png, pdf)"
|
||||||
|
accept="image/png, image/jpeg, application/pdf"
|
||||||
|
{...form.getInputProps("pemohon_tandaTangan")}
|
||||||
|
leftSection={<IconSignature size={18} />}
|
||||||
|
aria-label="Tanda Tangan"
|
||||||
|
description="Scan tanda tangan yang digunakan untuk verifikasi."
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Pengesahan */}
|
||||||
|
<Card withBorder radius="md" p="md">
|
||||||
|
<Title order={5}>Pengesahan</Title>
|
||||||
|
|
||||||
|
<SimpleGrid cols={1} mt="md">
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Kepala Desa / Lurah"
|
||||||
|
placeholder="Nama Kepala Desa atau Lurah"
|
||||||
|
{...form.getInputProps("kepalaDesaLurah")}
|
||||||
|
aria-label="Kepala Desa Lurah"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Camat"
|
||||||
|
placeholder="Nama Camat"
|
||||||
|
{...form.getInputProps("camat")}
|
||||||
|
aria-label="Camat"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label="Petugas Registrasi"
|
||||||
|
placeholder="Nama petugas registrasi"
|
||||||
|
{...form.getInputProps("petugasRegistrasi")}
|
||||||
|
aria-label="Petugas Registrasi"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Submission + Feedback */}
|
||||||
|
<Group justify="right" mt="md">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
leftSection={
|
||||||
|
submitting ? <Loader size={16} /> : <IconCheck size={16} />
|
||||||
|
}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
{submitting ? "Mengirim..." : "Kirim Permohonan"}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<Notification
|
||||||
|
icon={<IconCheck size={18} />}
|
||||||
|
color="teal"
|
||||||
|
onClose={() => setSuccess(null)}
|
||||||
|
title="Berhasil"
|
||||||
|
>
|
||||||
|
{success}
|
||||||
|
</Notification>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Notification
|
||||||
|
icon={<IconX size={18} />}
|
||||||
|
color="red"
|
||||||
|
onClose={() => setError(null)}
|
||||||
|
title="Error"
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</Notification>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Text size="xs" color="dimmed" ta="center">
|
||||||
|
Pastikan semua data telah terisi sesuai dokumen resmi. Untuk bantuan,
|
||||||
|
hubungi kantor kelurahan setempat.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
600
src/pages/darmasaba/form_surat_keterangan_tidak_mampu.tsx
Normal file
600
src/pages/darmasaba/form_surat_keterangan_tidak_mampu.tsx
Normal file
@@ -0,0 +1,600 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
ActionIcon,
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
FileInput,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { DatePicker } from "@mantine/dates";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import {
|
||||||
|
IconBuildingCommunity,
|
||||||
|
IconCalendarEvent,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconMailCheck,
|
||||||
|
IconMapPin,
|
||||||
|
IconUser,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Types (strong typing for the form state)
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
type Header = {
|
||||||
|
instansi: string;
|
||||||
|
kecamatan: string;
|
||||||
|
desaKelurahan: string;
|
||||||
|
nomorSurat: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DataPemohon = {
|
||||||
|
namaLengkap: string;
|
||||||
|
nik: string;
|
||||||
|
tempatTanggalLahir: string;
|
||||||
|
jenisKelamin: "Laki-laki" | "Perempuan" | "";
|
||||||
|
agama: string;
|
||||||
|
statusPerkawinan: string;
|
||||||
|
pekerjaan: string;
|
||||||
|
alamat: string;
|
||||||
|
rt: string;
|
||||||
|
rw: string;
|
||||||
|
desaKelurahan: string;
|
||||||
|
kecamatan: string;
|
||||||
|
kabupatenKota: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Keterangan = {
|
||||||
|
isiSurat: string;
|
||||||
|
keperluan: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Penutup = {
|
||||||
|
tempat: string;
|
||||||
|
tanggal: Date | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Pengesahan = {
|
||||||
|
kepalaDesaLurah: string;
|
||||||
|
jabatan: string;
|
||||||
|
tandaTangan: File | null;
|
||||||
|
stempel: File | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SKTMFormValues = {
|
||||||
|
header: Header;
|
||||||
|
dataPemohon: DataPemohon;
|
||||||
|
keterangan: Keterangan;
|
||||||
|
penutup: Penutup;
|
||||||
|
pengesahan: Pengesahan;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Reusable UI components
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
|
||||||
|
return (
|
||||||
|
<Group justify="apart" gap="xs" align="center">
|
||||||
|
<Text fw={600}>{label}</Text>
|
||||||
|
{hint && (
|
||||||
|
<Tooltip label={hint} withArrow>
|
||||||
|
<ActionIcon size={24} variant="subtle">
|
||||||
|
<IconInfoCircle size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormSection({
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
description?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card radius="md" shadow="sm" withBorder>
|
||||||
|
<Group justify="apart" align="center" mb="xs">
|
||||||
|
<Group align="center" gap="xs">
|
||||||
|
{icon}
|
||||||
|
<Text fw={700}>{title}</Text>
|
||||||
|
</Group>
|
||||||
|
{description && <Badge variant="light">{description}</Badge>}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Divider mb="sm" />
|
||||||
|
<Stack gap="sm">{children}</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Helper validators
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
const isRequired = (val: any) =>
|
||||||
|
val === undefined || val === null || String(val).trim() === ""
|
||||||
|
? "Wajib diisi"
|
||||||
|
: null;
|
||||||
|
const validateNIK = (val: string) => {
|
||||||
|
if (!val) return "Wajib diisi";
|
||||||
|
const digits = val.replace(/\D/g, "");
|
||||||
|
if (digits.length !== 16) return "NIK harus 16 digit";
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Main form component
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
export default function FormSuratKeteranganTidakMampu() {
|
||||||
|
// initialize form with sensible defaults
|
||||||
|
const form = useForm<SKTMFormValues>({
|
||||||
|
initialValues: {
|
||||||
|
header: {
|
||||||
|
instansi: "PEMERINTAH KABUPATEN / KOTA",
|
||||||
|
kecamatan: "",
|
||||||
|
desaKelurahan: "",
|
||||||
|
nomorSurat: "",
|
||||||
|
},
|
||||||
|
dataPemohon: {
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
tempatTanggalLahir: "",
|
||||||
|
jenisKelamin: "",
|
||||||
|
agama: "",
|
||||||
|
statusPerkawinan: "",
|
||||||
|
pekerjaan: "",
|
||||||
|
alamat: "",
|
||||||
|
rt: "",
|
||||||
|
rw: "",
|
||||||
|
desaKelurahan: "",
|
||||||
|
kecamatan: "",
|
||||||
|
kabupatenKota: "",
|
||||||
|
},
|
||||||
|
keterangan: {
|
||||||
|
isiSurat: "",
|
||||||
|
keperluan: "",
|
||||||
|
},
|
||||||
|
penutup: {
|
||||||
|
tempat: "",
|
||||||
|
tanggal: null,
|
||||||
|
},
|
||||||
|
pengesahan: {
|
||||||
|
kepalaDesaLurah: "",
|
||||||
|
jabatan: "Kepala Desa",
|
||||||
|
tandaTangan: null,
|
||||||
|
stempel: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
validate: {
|
||||||
|
// header validators
|
||||||
|
header: {
|
||||||
|
instansi: (val) => isRequired(val),
|
||||||
|
kecamatan: (val) => isRequired(val),
|
||||||
|
desaKelurahan: (val) => isRequired(val),
|
||||||
|
nomorSurat: (val) => isRequired(val),
|
||||||
|
},
|
||||||
|
// data pemohon validators
|
||||||
|
dataPemohon: {
|
||||||
|
namaLengkap: (val) => isRequired(val),
|
||||||
|
nik: (val) => validateNIK(val),
|
||||||
|
tempatTanggalLahir: (val) => isRequired(val),
|
||||||
|
jenisKelamin: (val) => (val ? null : "Pilih jenis kelamin"),
|
||||||
|
agama: (val) => isRequired(val),
|
||||||
|
statusPerkawinan: (val) => isRequired(val),
|
||||||
|
pekerjaan: (val) => isRequired(val),
|
||||||
|
alamat: (val) => isRequired(val),
|
||||||
|
rt: (val) => isRequired(val),
|
||||||
|
rw: (val) => isRequired(val),
|
||||||
|
desaKelurahan: (val) => isRequired(val),
|
||||||
|
kecamatan: (val) => isRequired(val),
|
||||||
|
kabupatenKota: (val) => isRequired(val),
|
||||||
|
},
|
||||||
|
// keterangan
|
||||||
|
keterangan: {
|
||||||
|
isiSurat: (val) => isRequired(val),
|
||||||
|
keperluan: (val) => isRequired(val),
|
||||||
|
},
|
||||||
|
// penutup
|
||||||
|
penutup: {
|
||||||
|
tempat: (val) => isRequired(val),
|
||||||
|
tanggal: (val) => (val ? null : "Pilih tanggal penerbitan"),
|
||||||
|
},
|
||||||
|
// pengesahan
|
||||||
|
pengesahan: {
|
||||||
|
kepalaDesaLurah: (val) => isRequired(val),
|
||||||
|
jabatan: (val) => isRequired(val),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit handler
|
||||||
|
const handleSubmit = (values: SKTMFormValues) => {
|
||||||
|
// Convert files to metadata or prepare multipart form if needed.
|
||||||
|
// Here we'll just console.log for demo purposes.
|
||||||
|
console.log("Form submitted:", values);
|
||||||
|
// In production: send to API endpoint (multipart/form-data) or convert File to base64.
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="md" w={"100%"}>
|
||||||
|
<Box>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Group justify="apart" align="center">
|
||||||
|
<Group align="center">
|
||||||
|
<IconBuildingCommunity size={28} />
|
||||||
|
<div>
|
||||||
|
<Text fw={800} size="xl">
|
||||||
|
Surat Keterangan Tidak Mampu (SKTM)
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Blangko resmi untuk pengajuan Surat Keterangan Tidak Mampu —
|
||||||
|
digunakan untuk keperluan pendidikan, kesehatan, atau
|
||||||
|
administrasi.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
<Group>
|
||||||
|
<Badge radius="sm">Form Length: 5 Sections</Badge>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) => {
|
||||||
|
handleSubmit(values);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Stack gap="lg">
|
||||||
|
{/* Header Section */}
|
||||||
|
<FormSection
|
||||||
|
title="Header Surat"
|
||||||
|
icon={<IconMailCheck size={20} />}
|
||||||
|
description="Informasi identitas surat"
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label={
|
||||||
|
<FieldLabel
|
||||||
|
label="Instansi Penerbit"
|
||||||
|
hint="Contoh: PEMERINTAH KABUPATEN/KOTA"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder="PEMERINTAH KABUPATEN/KOTA"
|
||||||
|
{...form.getInputProps("header.instansi")}
|
||||||
|
leftSection={<IconBuildingCommunity size={16} />}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label={
|
||||||
|
<FieldLabel
|
||||||
|
label="Nomor Surat"
|
||||||
|
hint="Nomor resmi surat"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder="123/SKTM/2025"
|
||||||
|
{...form.getInputProps("header.nomorSurat")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<TextInput
|
||||||
|
label={<FieldLabel label="Kecamatan" />}
|
||||||
|
placeholder="Kecamatan"
|
||||||
|
{...form.getInputProps("header.kecamatan")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<TextInput
|
||||||
|
label={<FieldLabel label="Desa / Kelurahan" />}
|
||||||
|
placeholder="Desa / Kelurahan"
|
||||||
|
{...form.getInputProps("header.desaKelurahan")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
{/* Data Pemohon Section */}
|
||||||
|
<Accordion variant="separated" radius="md" defaultValue="pemohon">
|
||||||
|
<Accordion.Item value="pemohon">
|
||||||
|
<Accordion.Control icon={<IconUser size={16} />}>
|
||||||
|
Data Pemohon
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<FormSection
|
||||||
|
title="Data Pemohon"
|
||||||
|
description="Informasi identitas pemohon"
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label={
|
||||||
|
<FieldLabel
|
||||||
|
label="Nama Lengkap"
|
||||||
|
hint="Sesuai KTP"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder="Nama lengkap"
|
||||||
|
{...form.getInputProps("dataPemohon.namaLengkap")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label={
|
||||||
|
<FieldLabel
|
||||||
|
label="NIK"
|
||||||
|
hint="16 digit, tanpa spasi"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder="3201xxxxxxxxxxxx"
|
||||||
|
{...form.getInputProps("dataPemohon.nik")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label={
|
||||||
|
<FieldLabel
|
||||||
|
label="Tempat, Tanggal Lahir"
|
||||||
|
hint="Contoh: Denpasar, 31-12-1990"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder="Tempat, tanggal lahir"
|
||||||
|
{...form.getInputProps(
|
||||||
|
"dataPemohon.tempatTanggalLahir",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Select
|
||||||
|
label={<FieldLabel label="Jenis Kelamin" />}
|
||||||
|
placeholder="Pilih jenis kelamin"
|
||||||
|
data={["Laki-laki", "Perempuan"]}
|
||||||
|
{...form.getInputProps("dataPemohon.jenisKelamin")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Select
|
||||||
|
label={<FieldLabel label="Agama" />}
|
||||||
|
placeholder="Pilih agama"
|
||||||
|
data={[
|
||||||
|
"Islam",
|
||||||
|
"Kristen",
|
||||||
|
"Katolik",
|
||||||
|
"Hindu",
|
||||||
|
"Buddha",
|
||||||
|
"Konghucu",
|
||||||
|
"Lainnya",
|
||||||
|
]}
|
||||||
|
{...form.getInputProps("dataPemohon.agama")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Select
|
||||||
|
label={<FieldLabel label="Status Perkawinan" />}
|
||||||
|
placeholder="Pilih status"
|
||||||
|
data={[
|
||||||
|
"Belum Kawin",
|
||||||
|
"Kawin",
|
||||||
|
"Cerai Hidup",
|
||||||
|
"Cerai Mati",
|
||||||
|
]}
|
||||||
|
{...form.getInputProps(
|
||||||
|
"dataPemohon.statusPerkawinan",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label={<FieldLabel label="Pekerjaan" />}
|
||||||
|
placeholder="Pekerjaan"
|
||||||
|
{...form.getInputProps("dataPemohon.pekerjaan")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Textarea
|
||||||
|
label={<FieldLabel label="Alamat Lengkap" />}
|
||||||
|
placeholder="Alamat domisili"
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("dataPemohon.alamat")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={2}>
|
||||||
|
<TextInput
|
||||||
|
label={<FieldLabel label="RT" />}
|
||||||
|
placeholder="001"
|
||||||
|
{...form.getInputProps("dataPemohon.rt")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={2}>
|
||||||
|
<TextInput
|
||||||
|
label={<FieldLabel label="RW" />}
|
||||||
|
placeholder="002"
|
||||||
|
{...form.getInputProps("dataPemohon.rw")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<TextInput
|
||||||
|
label={<FieldLabel label="Desa / Kelurahan" />}
|
||||||
|
placeholder="Desa"
|
||||||
|
{...form.getInputProps("dataPemohon.desaKelurahan")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<TextInput
|
||||||
|
label={<FieldLabel label="Kecamatan" />}
|
||||||
|
placeholder="Kecamatan"
|
||||||
|
{...form.getInputProps("dataPemohon.kecamatan")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<TextInput
|
||||||
|
label={<FieldLabel label="Kabupaten / Kota" />}
|
||||||
|
placeholder="Kabupaten / Kota"
|
||||||
|
{...form.getInputProps("dataPemohon.kabupatenKota")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
{/* Keterangan Section */}
|
||||||
|
<FormSection
|
||||||
|
title="Keterangan"
|
||||||
|
icon={<IconMapPin size={18} />}
|
||||||
|
description="Isi pernyataan SKTM"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
label={
|
||||||
|
<FieldLabel
|
||||||
|
label="Isi Surat"
|
||||||
|
hint="Jelaskan kondisi ekonomi secara singkat"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder="Pernyataan resmi bahwa yang bersangkutan benar-benar tergolong keluarga tidak mampu..."
|
||||||
|
minRows={4}
|
||||||
|
{...form.getInputProps("keterangan.isiSurat")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label={
|
||||||
|
<FieldLabel
|
||||||
|
label="Keperluan"
|
||||||
|
hint="Contoh: Beasiswa pendidikan / Perawatan kesehatan"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder="Keperluan surat"
|
||||||
|
{...form.getInputProps("keterangan.keperluan")}
|
||||||
|
/>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
{/* Penutup Section */}
|
||||||
|
<FormSection
|
||||||
|
title="Penutup"
|
||||||
|
icon={<IconCalendarEvent size={18} />}
|
||||||
|
description="Tempat dan tanggal penerbitan"
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label={<FieldLabel label="Tempat" />}
|
||||||
|
placeholder="Contoh: Denpasar"
|
||||||
|
{...form.getInputProps("penutup.tempat")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<DatePicker {...form.getInputProps("penutup.tanggal")} />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
{/* Pengesahan Section */}
|
||||||
|
<FormSection
|
||||||
|
title="Pengesahan"
|
||||||
|
description="Tanda tangan & stempel instansi"
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label={<FieldLabel label="Kepala Desa / Lurah" />}
|
||||||
|
placeholder="Nama pejabat"
|
||||||
|
{...form.getInputProps("pengesahan.kepalaDesaLurah")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<TextInput
|
||||||
|
label={<FieldLabel label="Jabatan" />}
|
||||||
|
placeholder="Contoh: Kepala Desa"
|
||||||
|
{...form.getInputProps("pengesahan.jabatan")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FileInput
|
||||||
|
label={
|
||||||
|
<FieldLabel
|
||||||
|
label="Scan Tanda Tangan"
|
||||||
|
hint="Upload file scan tanda tangan (PNG/JPG/PDF)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder="Pilih file..."
|
||||||
|
accept="image/png, image/jpeg, .pdf"
|
||||||
|
{...form.getInputProps("pengesahan.tandaTangan")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FileInput
|
||||||
|
label={
|
||||||
|
<FieldLabel
|
||||||
|
label="Stempel / Cap"
|
||||||
|
hint="Upload file stempel (PNG/JPG/PDF)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder="Pilih file..."
|
||||||
|
accept="image/png, image/jpeg, .pdf"
|
||||||
|
{...form.getInputProps("pengesahan.stempel")}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<Group justify="right" mt="md">
|
||||||
|
<Button variant="default" onClick={() => form.reset()}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">Kirim / Simpan</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Tip: Form ini otomatis menerjemahkan skema JSON ke komponen Mantine.
|
||||||
|
Anda dapat memperluas validasi (contoh: cek format NIK, unggah file
|
||||||
|
maksimal 2MB, dsb) sesuai kebutuhan produksi.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
641
src/pages/darmasaba/form_surat_keterangan_usaha.tsx
Normal file
641
src/pages/darmasaba/form_surat_keterangan_usaha.tsx
Normal file
@@ -0,0 +1,641 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
FileInput,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Notification,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { DatePicker } from "@mantine/dates";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import {
|
||||||
|
IconBuildingStore,
|
||||||
|
IconCheck,
|
||||||
|
IconClipboardText,
|
||||||
|
IconFileText,
|
||||||
|
IconId,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconUser,
|
||||||
|
IconX,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
/* -------------------------
|
||||||
|
Types derived from schema
|
||||||
|
------------------------- */
|
||||||
|
type PemohonData = {
|
||||||
|
namaLengkap: string;
|
||||||
|
nik: string;
|
||||||
|
tempatTanggalLahir: string;
|
||||||
|
jenisKelamin: "Laki-laki" | "Perempuan" | "";
|
||||||
|
pekerjaan: string;
|
||||||
|
alamat: string;
|
||||||
|
desaKelurahan: string;
|
||||||
|
kecamatan: string;
|
||||||
|
kabupatenKota: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UsahaData = {
|
||||||
|
namaUsaha: string;
|
||||||
|
jenisUsaha: string;
|
||||||
|
alamatUsaha: string;
|
||||||
|
lamaUsaha: string;
|
||||||
|
statusTempat: "Milik Sendiri" | "Sewa/Kontrak" | "Pinjam" | "";
|
||||||
|
};
|
||||||
|
|
||||||
|
type PemohonSignature = {
|
||||||
|
nama: string;
|
||||||
|
tandaTangan: File | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Pengesahan = {
|
||||||
|
kepalaDesaLurah: string;
|
||||||
|
camat?: string;
|
||||||
|
stempel?: File | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SkuFormValues = {
|
||||||
|
dataPemohon: PemohonData;
|
||||||
|
dataUsaha: UsahaData;
|
||||||
|
tujuanPembuatan: string;
|
||||||
|
tanggalPengajuan: Date | null;
|
||||||
|
pemohon: PemohonSignature;
|
||||||
|
pengesahan: Pengesahan;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* -------------------------
|
||||||
|
Reusable small components
|
||||||
|
------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FormField:
|
||||||
|
* Maps a tiny field descriptor to a Mantine input with label, description and error handling.
|
||||||
|
* For brevity each field mapping is explicit — easy to extend to a dynamic mapping.
|
||||||
|
*/
|
||||||
|
function FormField(props: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { id, label, description, children } = props;
|
||||||
|
return (
|
||||||
|
<Stack style={{ width: "100%" }}>
|
||||||
|
<Group justify="apart" style={{ alignItems: "flex-start" }}>
|
||||||
|
<Text fw={600} c="dark" id={`${id}-label`}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
{description ? (
|
||||||
|
<Tooltip label={description} withArrow>
|
||||||
|
<IconInfoCircle size={18} aria-hidden />
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
<div aria-labelledby={`${id}-label`}>{children}</div>
|
||||||
|
{description ? (
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FormSection:
|
||||||
|
* Collapsible card/accordion for grouping nested objects (dataPemohon, dataUsaha, pengesahan).
|
||||||
|
*/
|
||||||
|
function FormSection(props: {
|
||||||
|
title: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
defaultOpened?: boolean;
|
||||||
|
}) {
|
||||||
|
const { title, icon, children, defaultOpened = true } = props;
|
||||||
|
return (
|
||||||
|
<Accordion multiple defaultValue={defaultOpened ? ["section"] : []}>
|
||||||
|
<Accordion.Item value="section">
|
||||||
|
<Accordion.Control icon={icon ?? <IconClipboardText size={18} />}>
|
||||||
|
<Text fw={700}>{title}</Text>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>{children}</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------
|
||||||
|
Main Dynamic Form Component
|
||||||
|
------------------------- */
|
||||||
|
|
||||||
|
export default function FormSuratKeteranganUsaha() {
|
||||||
|
// initial values follow the schema and provide sensible defaults.
|
||||||
|
const form = useForm<SkuFormValues>({
|
||||||
|
initialValues: {
|
||||||
|
dataPemohon: {
|
||||||
|
namaLengkap: "",
|
||||||
|
nik: "",
|
||||||
|
tempatTanggalLahir: "",
|
||||||
|
jenisKelamin: "",
|
||||||
|
pekerjaan: "",
|
||||||
|
alamat: "",
|
||||||
|
desaKelurahan: "",
|
||||||
|
kecamatan: "",
|
||||||
|
kabupatenKota: "",
|
||||||
|
},
|
||||||
|
dataUsaha: {
|
||||||
|
namaUsaha: "",
|
||||||
|
jenisUsaha: "",
|
||||||
|
alamatUsaha: "",
|
||||||
|
lamaUsaha: "",
|
||||||
|
statusTempat: "",
|
||||||
|
},
|
||||||
|
tujuanPembuatan: "",
|
||||||
|
tanggalPengajuan: null,
|
||||||
|
pemohon: {
|
||||||
|
nama: "",
|
||||||
|
tandaTangan: null,
|
||||||
|
},
|
||||||
|
pengesahan: {
|
||||||
|
kepalaDesaLurah: "",
|
||||||
|
camat: "",
|
||||||
|
stempel: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Validation rules inspired by schema descriptions
|
||||||
|
validate: {
|
||||||
|
dataPemohon: {
|
||||||
|
namaLengkap: (v) =>
|
||||||
|
v.trim().length === 0 ? "Nama lengkap harus diisi" : null,
|
||||||
|
nik: (v) =>
|
||||||
|
v.trim().length === 0
|
||||||
|
? "NIK harus diisi"
|
||||||
|
: !/^\d{16}$/.test(v.trim())
|
||||||
|
? "NIK harus berupa 16 digit angka"
|
||||||
|
: null,
|
||||||
|
tempatTanggalLahir: (v) =>
|
||||||
|
v.trim().length === 0 ? "Tempat/tanggal lahir harus diisi" : null,
|
||||||
|
jenisKelamin: (v) => (v === "" ? "Pilih jenis kelamin" : null),
|
||||||
|
pekerjaan: (v) =>
|
||||||
|
v.trim().length === 0 ? "Pekerjaan harus diisi" : null,
|
||||||
|
alamat: (v) => (v.trim().length === 0 ? "Alamat harus diisi" : null),
|
||||||
|
desaKelurahan: (v) =>
|
||||||
|
v.trim().length === 0 ? "Nama desa/kelurahan harus diisi" : null,
|
||||||
|
kecamatan: (v) =>
|
||||||
|
v.trim().length === 0 ? "Kecamatan harus diisi" : null,
|
||||||
|
kabupatenKota: (v) =>
|
||||||
|
v.trim().length === 0 ? "Kabupaten/Kota harus diisi" : null,
|
||||||
|
},
|
||||||
|
dataUsaha: {
|
||||||
|
namaUsaha: (v) =>
|
||||||
|
v.trim().length === 0 ? "Nama usaha harus diisi" : null,
|
||||||
|
jenisUsaha: (v) =>
|
||||||
|
v.trim().length === 0 ? "Jenis usaha harus diisi" : null,
|
||||||
|
alamatUsaha: (v) =>
|
||||||
|
v.trim().length === 0 ? "Alamat usaha harus diisi" : null,
|
||||||
|
lamaUsaha: (v) =>
|
||||||
|
v.trim().length === 0 ? "Lama usaha harus diisi" : null,
|
||||||
|
statusTempat: (v) =>
|
||||||
|
v === "" ? "Pilih status kepemilikan tempat usaha" : null,
|
||||||
|
},
|
||||||
|
tujuanPembuatan: (v) =>
|
||||||
|
v.trim().length === 0 ? "Tujuan pembuatan harus diisi" : null,
|
||||||
|
tanggalPengajuan: (v) => (v === null ? "Pilih tanggal pengajuan" : null),
|
||||||
|
pemohon: {
|
||||||
|
nama: (v) =>
|
||||||
|
v.trim().length === 0 ? "Nama pemohon harus diisi" : null,
|
||||||
|
// tandaTangan optional but we can require at least a file for UX if desired:
|
||||||
|
tandaTangan: (_) => null,
|
||||||
|
},
|
||||||
|
pengesahan: {
|
||||||
|
kepalaDesaLurah: (v) =>
|
||||||
|
v.trim().length === 0 ? "Nama kepala desa/lurah harus diisi" : null,
|
||||||
|
camat: () => null,
|
||||||
|
stempel: () => null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulated submit handler — replace with real API call.
|
||||||
|
const [submitStatus, setSubmitStatus] = React.useState<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const handleSubmit = (values: SkuFormValues) => {
|
||||||
|
// We intentionally don't send anything asynchronously here in this demo.
|
||||||
|
// Convert File objects to metadata strings for preview if present.
|
||||||
|
const payload = {
|
||||||
|
...values,
|
||||||
|
pemohon: {
|
||||||
|
...values.pemohon,
|
||||||
|
tandaTangan: values.pemohon.tandaTangan
|
||||||
|
? values.pemohon.tandaTangan.name
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
pengesahan: {
|
||||||
|
...values.pengesahan,
|
||||||
|
stempel: values.pengesahan.stempel
|
||||||
|
? values.pengesahan.stempel.name
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
tanggalPengajuan: values.tanggalPengajuan
|
||||||
|
? values.tanggalPengajuan.toISOString().slice(0, 10)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// For now: show success and JSON preview
|
||||||
|
setSubmitStatus({
|
||||||
|
success: true,
|
||||||
|
message: "Form berhasil divalidasi. Lihat payload.",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("SKU payload:", payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
form.reset();
|
||||||
|
setSubmitStatus(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size={"md"} w={"100%"}>
|
||||||
|
<Card shadow="sm" radius="md" p="lg">
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group justify="apart" gap="sm">
|
||||||
|
<Title order={3}>
|
||||||
|
<Group>
|
||||||
|
<IconClipboardText size={22} />
|
||||||
|
<span>Surat Keterangan Usaha (SKU)</span>
|
||||||
|
</Group>
|
||||||
|
</Title>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Blangko resmi dari desa/kelurahan
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Blangko resmi untuk keterangan usaha dari pemerintah desa/kelurahan
|
||||||
|
sebagai syarat administrasi.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Data Pemohon Section */}
|
||||||
|
<FormSection title="Data Pemohon" icon={<IconUser size={18} />}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<FormField
|
||||||
|
id="namaLengkap"
|
||||||
|
label="Nama Lengkap"
|
||||||
|
description="Nama lengkap pemohon sesuai KTP."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Contoh: Budi Santoso"
|
||||||
|
{...form.getInputProps("dataPemohon.namaLengkap")}
|
||||||
|
aria-describedby="namaLengkap-desc"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="nik"
|
||||||
|
label="NIK"
|
||||||
|
description="Nomor Induk Kependudukan (16 digit)."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="16 digit NIK"
|
||||||
|
maxLength={16}
|
||||||
|
inputMode="numeric"
|
||||||
|
{...form.getInputProps("dataPemohon.nik")}
|
||||||
|
aria-describedby="nik-desc"
|
||||||
|
leftSection={<IconId size={16} />}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="ttl"
|
||||||
|
label="Tempat, Tanggal Lahir"
|
||||||
|
description="Contoh: Denpasar, 1 Januari 1990"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Tempat, Tanggal Lahir"
|
||||||
|
{...form.getInputProps("dataPemohon.tempatTanggalLahir")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="jenisKelamin"
|
||||||
|
label="Jenis Kelamin"
|
||||||
|
description="Pilih jenis kelamin pemohon."
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="Pilih"
|
||||||
|
data={["Laki-laki", "Perempuan"]}
|
||||||
|
{...form.getInputProps("dataPemohon.jenisKelamin")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="pekerjaan"
|
||||||
|
label="Pekerjaan"
|
||||||
|
description="Pekerjaan utama pemohon."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Pekerjaan"
|
||||||
|
{...form.getInputProps("dataPemohon.pekerjaan")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<FormField
|
||||||
|
id="alamat"
|
||||||
|
label="Alamat"
|
||||||
|
description="Alamat lengkap sesuai domisili."
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Alamat lengkap"
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("dataPemohon.alamat")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField
|
||||||
|
id="desaKelurahan"
|
||||||
|
label="Desa / Kelurahan"
|
||||||
|
description="Nama desa/kelurahan."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
{...form.getInputProps("dataPemohon.desaKelurahan")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField
|
||||||
|
id="kecamatan"
|
||||||
|
label="Kecamatan"
|
||||||
|
description="Nama kecamatan."
|
||||||
|
>
|
||||||
|
<TextInput {...form.getInputProps("dataPemohon.kecamatan")} />
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField
|
||||||
|
id="kabupatenKota"
|
||||||
|
label="Kabupaten / Kota"
|
||||||
|
description="Nama kabupaten/kota."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
{...form.getInputProps("dataPemohon.kabupatenKota")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Data Usaha Section */}
|
||||||
|
<FormSection
|
||||||
|
title="Data Usaha"
|
||||||
|
icon={<IconBuildingStore size={18} />}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField id="namaUsaha" label="Nama Usaha">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Nama usaha"
|
||||||
|
{...form.getInputProps("dataUsaha.namaUsaha")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="jenisUsaha"
|
||||||
|
label="Jenis Usaha"
|
||||||
|
description="Contoh: warung makan, bengkel, toko kelontong"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Jenis usaha"
|
||||||
|
{...form.getInputProps("dataUsaha.jenisUsaha")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<FormField id="alamatUsaha" label="Alamat Usaha">
|
||||||
|
<Textarea
|
||||||
|
placeholder="Alamat lengkap lokasi usaha"
|
||||||
|
{...form.getInputProps("dataUsaha.alamatUsaha")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="lamaUsaha"
|
||||||
|
label="Lama Usaha"
|
||||||
|
description="Contoh: 3 tahun"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Lama usaha (mis. 3 tahun)"
|
||||||
|
{...form.getInputProps("dataUsaha.lamaUsaha")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="statusTempat"
|
||||||
|
label="Status Tempat"
|
||||||
|
description="Pilih status kepemilikan tempat usaha"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="Pilih..."
|
||||||
|
data={["Milik Sendiri", "Sewa/Kontrak", "Pinjam"]}
|
||||||
|
{...form.getInputProps("dataUsaha.statusTempat")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Tujuan & Tanggal */}
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<FormField
|
||||||
|
id="tujuanPembuatan"
|
||||||
|
label="Tujuan Pembuatan"
|
||||||
|
description="Contoh: pengajuan kredit bank"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Tujuan permohonan SKU"
|
||||||
|
minRows={2}
|
||||||
|
{...form.getInputProps("tujuanPembuatan")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<FormField
|
||||||
|
id="tanggalPengajuan"
|
||||||
|
label="Tanggal Pengajuan"
|
||||||
|
description="Tanggal pemohon mengajukan permohonan."
|
||||||
|
>
|
||||||
|
<DatePicker
|
||||||
|
value={form.values.tanggalPengajuan}
|
||||||
|
onChange={(d) =>
|
||||||
|
form.setFieldValue("tanggalPengajuan", d as any)
|
||||||
|
}
|
||||||
|
aria-label="Tanggal Pengajuan"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Pemohon Signature */}
|
||||||
|
<FormSection
|
||||||
|
title="Pemohon (Tanda Tangan)"
|
||||||
|
icon={<IconFileText size={18} />}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="pemohonNama"
|
||||||
|
label="Nama Pemohon"
|
||||||
|
description="Ditulis ulang sebagai tanda tangan."
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
{...form.getInputProps("pemohon.nama")}
|
||||||
|
placeholder="Nama pemohon"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField
|
||||||
|
id="tandaTangan"
|
||||||
|
label="Tanda Tangan (scan/file)"
|
||||||
|
description="Unggah file tanda tangan (scan)."
|
||||||
|
>
|
||||||
|
<FileInput
|
||||||
|
placeholder="Pilih file"
|
||||||
|
accept="image/*, .pdf"
|
||||||
|
value={form.values.pemohon.tandaTangan}
|
||||||
|
onChange={(f) =>
|
||||||
|
form.setFieldValue("pemohon.tandaTangan", f)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Pengesahan */}
|
||||||
|
<FormSection title="Pengesahan" icon={<IconCheck size={18} />}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField id="kepalaDesa" label="Kepala Desa / Lurah">
|
||||||
|
<TextInput
|
||||||
|
{...form.getInputProps("pengesahan.kepalaDesaLurah")}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<FormField id="camat" label="Camat (opsional)">
|
||||||
|
<TextInput {...form.getInputProps("pengesahan.camat")} />
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<FormField
|
||||||
|
id="stempel"
|
||||||
|
label="Stempel (opsional)"
|
||||||
|
description="Unggah gambar stempel resmi jika tersedia."
|
||||||
|
>
|
||||||
|
<FileInput
|
||||||
|
placeholder="Pilih file stempel"
|
||||||
|
accept="image/*, .pdf"
|
||||||
|
value={form.values.pengesahan.stempel}
|
||||||
|
onChange={(f) =>
|
||||||
|
form.setFieldValue("pengesahan.stempel", f)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
{/* Submit / Reset actions */}
|
||||||
|
<Group justify="right" gap="sm">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={handleReset}
|
||||||
|
leftSection={<IconX size={16} />}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const result = form.validate();
|
||||||
|
if (result.hasErrors) {
|
||||||
|
setSubmitStatus({
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
"Terdapat kesalahan pada form. Mohon periksa kembali.",
|
||||||
|
});
|
||||||
|
// scroll to first error? could enhance later.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleSubmit(form.values);
|
||||||
|
}}
|
||||||
|
leftSection={<IconCheck size={16} />}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Submission feedback */}
|
||||||
|
{submitStatus ? (
|
||||||
|
<Notification
|
||||||
|
color={submitStatus.success ? "teal" : "red"}
|
||||||
|
icon={submitStatus.success ? <IconCheck /> : <IconX />}
|
||||||
|
>
|
||||||
|
{submitStatus.message}
|
||||||
|
</Notification>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { Button, Card, Container, Group, Stack, Table, Text, TextInput } from "@mantine/core";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import apiFetch from "@/lib/apiFetch";
|
|
||||||
import { showNotification } from "@mantine/notifications";
|
|
||||||
|
|
||||||
export default function ApiKeyPage() {
|
|
||||||
return (
|
|
||||||
<Container size="md" w={"100%"}>
|
|
||||||
<Stack>
|
|
||||||
<Text>API Key</Text>
|
|
||||||
<CreateApiKey />
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateApiKey() {
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
const [description, setDescription] = useState('');
|
|
||||||
const [expiredAt, setExpiredAt] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
const res = await apiFetch.api.apikey.create.post({ name, description, expiredAt });
|
|
||||||
if (res.status === 200) {
|
|
||||||
setName('');
|
|
||||||
setDescription('');
|
|
||||||
setExpiredAt('');
|
|
||||||
showNotification({
|
|
||||||
title: 'Success',
|
|
||||||
message: 'API key created successfully',
|
|
||||||
color: 'green',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Card >
|
|
||||||
<Stack>
|
|
||||||
<Text>API Create</Text>
|
|
||||||
<TextInput label="Name" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} />
|
|
||||||
<TextInput label="Description" placeholder="Description" value={description} onChange={(e) => setDescription(e.target.value)} />
|
|
||||||
<TextInput label="Expired At" placeholder="Expired At" type="date" value={expiredAt} onChange={(e) => setExpiredAt(e.target.value)} />
|
|
||||||
<Group>
|
|
||||||
<Button variant="outline" onClick={() => { setName(''); setDescription(''); setExpiredAt(''); }}>Cancel</Button>
|
|
||||||
<Button onClick={handleSubmit} type="submit" loading={loading}>Save</Button>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<ListApiKey />
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListApiKey() {
|
|
||||||
const [apiKeys, setApiKeys] = useState<any[]>([]);
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchApiKeys = async () => {
|
|
||||||
const res = await apiFetch.api.apikey.list.get();
|
|
||||||
if (res.status === 200) {
|
|
||||||
setApiKeys(res.data?.apiKeys || []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchApiKeys();
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<Stack>
|
|
||||||
<Text>API List</Text>
|
|
||||||
<Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Expired At</th>
|
|
||||||
<th>Created At</th>
|
|
||||||
<th>Updated At</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{apiKeys.map((apiKey: any, index: number) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{apiKey.name}</td>
|
|
||||||
<td>{apiKey.description}</td>
|
|
||||||
<td>{apiKey.expiredAt.toISOString().split('T')[0]}</td>
|
|
||||||
<td>{apiKey.createdAt.toISOString().split('T')[0]}</td>
|
|
||||||
<td>{apiKey.updatedAt.toISOString().split('T')[0]}</td>
|
|
||||||
<td>
|
|
||||||
<Button variant="outline" onClick={() => {
|
|
||||||
apiFetch.api.apikey.delete.delete({ id: apiKey.id })
|
|
||||||
setApiKeys(apiKeys.filter((api: any) => api.id !== apiKey.id))
|
|
||||||
}}>Delete</Button>
|
|
||||||
<Button variant="outline" onClick={() => {
|
|
||||||
navigator.clipboard.writeText(apiKey.key)
|
|
||||||
showNotification({
|
|
||||||
title: 'Success',
|
|
||||||
message: 'API key copied to clipboard',
|
|
||||||
color: 'green',
|
|
||||||
})
|
|
||||||
}}>Copy</Button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import apiFetch from "@/lib/apiFetch";
|
|
||||||
import { Button, Card, Container, Flex, Group, Paper, Stack, Text, TextInput, Title } from "@mantine/core";
|
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
|
||||||
import { showNotification } from "@mantine/notifications";
|
|
||||||
import { useState } from "react";
|
|
||||||
import useSwr from 'swr'
|
|
||||||
import { proxy, subscribe, useSnapshot } from 'valtio'
|
|
||||||
|
|
||||||
const state = proxy({
|
|
||||||
reload: ""
|
|
||||||
})
|
|
||||||
|
|
||||||
function reloadState() {
|
|
||||||
state.reload = Math.random().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CredentialPage() {
|
|
||||||
return <Container size={"md"} w={"100%"}>
|
|
||||||
<Stack>
|
|
||||||
<CredentialCreate />
|
|
||||||
<CredentialList />
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
}
|
|
||||||
|
|
||||||
function CredentialCreate() {
|
|
||||||
const [name, setName] = useState("")
|
|
||||||
const [apikey, setApikey] = useState("")
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
|
||||||
const { data } = await apiFetch.api.credential.create.post({
|
|
||||||
name: name,
|
|
||||||
value: apikey
|
|
||||||
})
|
|
||||||
|
|
||||||
setName("")
|
|
||||||
setApikey("")
|
|
||||||
|
|
||||||
showNotification({
|
|
||||||
message: data?.message
|
|
||||||
})
|
|
||||||
|
|
||||||
reloadState()
|
|
||||||
}
|
|
||||||
return <Card>
|
|
||||||
<Stack>
|
|
||||||
<Title>Credential Create</Title>
|
|
||||||
<TextInput placeholder="name" value={name} onChange={(e) => setName(e.target.value)} />
|
|
||||||
<TextInput placeholder="apikey" value={apikey} onChange={(e) => setApikey(e.target.value)} />
|
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit}>Save</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
}
|
|
||||||
|
|
||||||
function CredentialList() {
|
|
||||||
const { data, mutate } = useSwr("/", () => apiFetch.api.credential.list.get())
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
|
||||||
const unsubscribe = subscribe(state, async () => {
|
|
||||||
console.log('state has changed to', state)
|
|
||||||
mutate()
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => unsubscribe()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
async function handleRm(id: string) {
|
|
||||||
await apiFetch.api.credential.rm.delete({
|
|
||||||
id: id
|
|
||||||
})
|
|
||||||
|
|
||||||
reloadState()
|
|
||||||
|
|
||||||
}
|
|
||||||
return <Card>
|
|
||||||
<Stack>
|
|
||||||
{data?.data?.list.map((v, k) => <Stack key={k}>
|
|
||||||
<Flex justify={"space-between"}>
|
|
||||||
<Text>{v.name}</Text>
|
|
||||||
<Group>
|
|
||||||
<Button onClick={() => handleRm(v.id)}>delete</Button>
|
|
||||||
</Group>
|
|
||||||
</Flex>
|
|
||||||
</Stack>)}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
AppShell,
|
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Divider,
|
|
||||||
Flex,
|
|
||||||
Group,
|
|
||||||
NavLink,
|
|
||||||
Paper,
|
|
||||||
ScrollArea,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core'
|
|
||||||
import { useLocalStorage } from '@mantine/hooks'
|
|
||||||
import {
|
|
||||||
IconChevronLeft,
|
|
||||||
IconChevronRight,
|
|
||||||
IconDashboard,
|
|
||||||
IconKey,
|
|
||||||
IconLock
|
|
||||||
} from '@tabler/icons-react'
|
|
||||||
import type { User } from 'generated/prisma'
|
|
||||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { default as clientRoute, default as clientRoutes } from '@/clientRoutes'
|
|
||||||
import apiFetch from '@/lib/apiFetch'
|
|
||||||
|
|
||||||
|
|
||||||
function Logout() {
|
|
||||||
return <Group>
|
|
||||||
<Button variant='transparent' size='compact-xs' onClick={async () => {
|
|
||||||
await apiFetch.auth.logout.delete()
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
window.location.href = '/login'
|
|
||||||
}}>Logout</Button>
|
|
||||||
</Group>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DashboardLayout() {
|
|
||||||
const [opened, setOpened] = useLocalStorage({
|
|
||||||
key: 'nav_open',
|
|
||||||
defaultValue: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell
|
|
||||||
padding="md"
|
|
||||||
navbar={{
|
|
||||||
width: 260,
|
|
||||||
breakpoint: 'sm',
|
|
||||||
collapsed: { mobile: !opened, desktop: !opened },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AppShell.Navbar>
|
|
||||||
<AppShell.Section>
|
|
||||||
<Group justify="flex-end" p="xs">
|
|
||||||
<Tooltip
|
|
||||||
label={opened ? 'Collapse navigation' : 'Expand navigation'}
|
|
||||||
withArrow
|
|
||||||
>
|
|
||||||
<ActionIcon
|
|
||||||
variant="light"
|
|
||||||
color="gray"
|
|
||||||
onClick={() => setOpened(v => !v)}
|
|
||||||
aria-label="Toggle navigation"
|
|
||||||
radius="xl"
|
|
||||||
>
|
|
||||||
{opened ? <IconChevronLeft /> : <IconChevronRight />}
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
</AppShell.Section>
|
|
||||||
|
|
||||||
<AppShell.Section grow component={ScrollArea} flex={1}>
|
|
||||||
<NavigationDashboard />
|
|
||||||
</AppShell.Section>
|
|
||||||
|
|
||||||
<AppShell.Section>
|
|
||||||
<HostView />
|
|
||||||
</AppShell.Section>
|
|
||||||
</AppShell.Navbar>
|
|
||||||
|
|
||||||
<AppShell.Main>
|
|
||||||
<Stack>
|
|
||||||
<Paper withBorder shadow="md" radius="lg" p="md">
|
|
||||||
<Flex align="center" gap="md">
|
|
||||||
{!opened && (
|
|
||||||
<Tooltip label="Open navigation menu" withArrow>
|
|
||||||
<ActionIcon
|
|
||||||
variant="light"
|
|
||||||
color="gray"
|
|
||||||
onClick={() => setOpened(true)}
|
|
||||||
aria-label="Open navigation"
|
|
||||||
radius="xl"
|
|
||||||
>
|
|
||||||
<IconChevronRight />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<Title order={3} fw={600}>
|
|
||||||
App Dashboard
|
|
||||||
</Title>
|
|
||||||
</Flex>
|
|
||||||
</Paper>
|
|
||||||
<Outlet />
|
|
||||||
</Stack>
|
|
||||||
</AppShell.Main>
|
|
||||||
</AppShell>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------- Host Info ----------------------- */
|
|
||||||
function HostView() {
|
|
||||||
const [host, setHost] = useState<User | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchHost() {
|
|
||||||
const { data } = await apiFetch.api.user.find.get()
|
|
||||||
setHost(data?.user ?? null)
|
|
||||||
}
|
|
||||||
fetchHost()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card radius="lg" withBorder shadow="sm" p="md">
|
|
||||||
{host ? (
|
|
||||||
<Stack>
|
|
||||||
<Flex gap="md" align="center">
|
|
||||||
<Avatar size="md" radius="xl" color="blue">
|
|
||||||
{host.name?.[0]}
|
|
||||||
</Avatar>
|
|
||||||
<Stack gap={2}>
|
|
||||||
<Text fw={600}>{host.name}</Text>
|
|
||||||
<Text size="sm" c="dimmed">{host.email}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Flex>
|
|
||||||
<Divider />
|
|
||||||
<Logout />
|
|
||||||
</Stack>
|
|
||||||
) : (
|
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
|
||||||
No host information available
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------- Navigation ----------------------- */
|
|
||||||
function NavigationDashboard() {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
const isActive = (path: keyof typeof clientRoute) =>
|
|
||||||
location.pathname.startsWith(clientRoute[path])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack gap="xs" p="sm">
|
|
||||||
<NavLink
|
|
||||||
active={isActive('/dashboard/landing')}
|
|
||||||
leftSection={<IconDashboard size={20} />}
|
|
||||||
label="Dashboard Overview"
|
|
||||||
description="Quick summary and activity highlights"
|
|
||||||
onClick={() => navigate(clientRoutes['/dashboard/landing'])}
|
|
||||||
/>
|
|
||||||
<NavLink
|
|
||||||
active={isActive('/dashboard/apikey')}
|
|
||||||
leftSection={<IconKey size={20} />}
|
|
||||||
label="Dashboard Overview"
|
|
||||||
description="Quick summary and activity highlights"
|
|
||||||
onClick={() => navigate(clientRoutes['/dashboard/apikey'])}
|
|
||||||
/>
|
|
||||||
<NavLink
|
|
||||||
active={isActive('/dashboard/credential')}
|
|
||||||
leftSection={<IconLock size={20} />}
|
|
||||||
label="Dashboard Overview"
|
|
||||||
description="Quick summary and activity highlights"
|
|
||||||
onClick={() => navigate(clientRoutes['/dashboard/credential'])}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import apiFetch from "@/lib/apiFetch";
|
|
||||||
import { Button } from "@mantine/core";
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Dashboard</h1>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
164
src/pages/scr/dashboard/apikey/apikey_page.tsx
Normal file
164
src/pages/scr/dashboard/apikey/apikey_page.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
import { showNotification } from "@mantine/notifications";
|
||||||
|
|
||||||
|
export default function ApiKeyPage() {
|
||||||
|
return (
|
||||||
|
<Container size="md" w={"100%"}>
|
||||||
|
<Stack>
|
||||||
|
<Text>API Key</Text>
|
||||||
|
<CreateApiKey />
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateApiKey() {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [expiredAt, setExpiredAt] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
const res = await apiFetch.api.apikey.create.post({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
expiredAt,
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
setName("");
|
||||||
|
setDescription("");
|
||||||
|
setExpiredAt("");
|
||||||
|
showNotification({
|
||||||
|
title: "Success",
|
||||||
|
message: "API key created successfully",
|
||||||
|
color: "green",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Stack>
|
||||||
|
<Text>API Create</Text>
|
||||||
|
<TextInput
|
||||||
|
label="Name"
|
||||||
|
placeholder="Name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Description"
|
||||||
|
placeholder="Description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Expired At"
|
||||||
|
placeholder="Expired At"
|
||||||
|
type="date"
|
||||||
|
value={expiredAt}
|
||||||
|
onChange={(e) => setExpiredAt(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setName("");
|
||||||
|
setDescription("");
|
||||||
|
setExpiredAt("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} type="submit" loading={loading}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<ListApiKey />
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListApiKey() {
|
||||||
|
const [apiKeys, setApiKeys] = useState<any[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchApiKeys = async () => {
|
||||||
|
const res = await apiFetch.api.apikey.list.get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
setApiKeys(res.data?.apiKeys || []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchApiKeys();
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Stack>
|
||||||
|
<Text>API List</Text>
|
||||||
|
<Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Expired At</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Updated At</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{apiKeys.map((apiKey: any, index: number) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>{apiKey.name}</td>
|
||||||
|
<td>{apiKey.description}</td>
|
||||||
|
<td>{apiKey.expiredAt.toISOString().split("T")[0]}</td>
|
||||||
|
<td>{apiKey.createdAt.toISOString().split("T")[0]}</td>
|
||||||
|
<td>{apiKey.updatedAt.toISOString().split("T")[0]}</td>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
apiFetch.api.apikey.delete.delete({ id: apiKey.id });
|
||||||
|
setApiKeys(
|
||||||
|
apiKeys.filter((api: any) => api.id !== apiKey.id),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(apiKey.key);
|
||||||
|
showNotification({
|
||||||
|
title: "Success",
|
||||||
|
message: "API key copied to clipboard",
|
||||||
|
color: "green",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
src/pages/scr/dashboard/credential/credential_page.tsx
Normal file
116
src/pages/scr/dashboard/credential/credential_page.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Flex,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
|
import { showNotification } from "@mantine/notifications";
|
||||||
|
import { useState } from "react";
|
||||||
|
import useSwr from "swr";
|
||||||
|
import { proxy, subscribe } from "valtio";
|
||||||
|
|
||||||
|
const state = proxy({
|
||||||
|
reload: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
function reloadState() {
|
||||||
|
state.reload = Math.random().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CredentialPage() {
|
||||||
|
return (
|
||||||
|
<Container size={"md"} w={"100%"}>
|
||||||
|
<Stack>
|
||||||
|
<CredentialCreate />
|
||||||
|
<CredentialList />
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CredentialCreate() {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [apikey, setApikey] = useState("");
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const { data } = await apiFetch.api.credential.create.post({
|
||||||
|
name: name,
|
||||||
|
value: apikey,
|
||||||
|
});
|
||||||
|
|
||||||
|
setName("");
|
||||||
|
setApikey("");
|
||||||
|
|
||||||
|
showNotification({
|
||||||
|
message: data?.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
reloadState();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Stack>
|
||||||
|
<Title>Credential Create</Title>
|
||||||
|
<TextInput
|
||||||
|
placeholder="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="apikey"
|
||||||
|
value={apikey}
|
||||||
|
onChange={(e) => setApikey(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<Button onClick={handleSubmit}>Save</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CredentialList() {
|
||||||
|
const { data, mutate } = useSwr("/", () =>
|
||||||
|
apiFetch.api.credential.list.get(),
|
||||||
|
);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
const unsubscribe = subscribe(state, async () => {
|
||||||
|
console.log("state has changed to", state);
|
||||||
|
mutate();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleRm(id: string) {
|
||||||
|
await apiFetch.api.credential.rm.delete({
|
||||||
|
id: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
reloadState();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Stack>
|
||||||
|
{data?.data?.list.map((v, k) => (
|
||||||
|
<Stack key={k}>
|
||||||
|
<Flex justify={"space-between"}>
|
||||||
|
<Text>{v.name}</Text>
|
||||||
|
<Group>
|
||||||
|
<Button onClick={() => handleRm(v.id)}>delete</Button>
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/pages/scr/dashboard/dashboard_home.tsx
Normal file
7
src/pages/scr/dashboard/dashboard_home.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function Dashboard() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
202
src/pages/scr/dashboard/dashboard_layout.tsx
Normal file
202
src/pages/scr/dashboard/dashboard_layout.tsx
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
AppShell,
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Group,
|
||||||
|
NavLink,
|
||||||
|
Paper,
|
||||||
|
ScrollArea,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useLocalStorage } from "@mantine/hooks";
|
||||||
|
import {
|
||||||
|
IconChevronLeft,
|
||||||
|
IconChevronRight,
|
||||||
|
IconDashboard,
|
||||||
|
IconKey,
|
||||||
|
IconLock,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import type { User } from "generated/prisma";
|
||||||
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import {
|
||||||
|
default as clientRoute,
|
||||||
|
default as clientRoutes,
|
||||||
|
} from "@/clientRoutes";
|
||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
|
||||||
|
function Logout() {
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<Button
|
||||||
|
variant="transparent"
|
||||||
|
size="compact-xs"
|
||||||
|
onClick={async () => {
|
||||||
|
await apiFetch.auth.logout.delete();
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
window.location.href = "/login";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardLayout() {
|
||||||
|
const [opened, setOpened] = useLocalStorage({
|
||||||
|
key: "nav_open",
|
||||||
|
defaultValue: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
padding="md"
|
||||||
|
navbar={{
|
||||||
|
width: 260,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !opened, desktop: !opened },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AppShell.Navbar>
|
||||||
|
<AppShell.Section>
|
||||||
|
<Group justify="flex-end" p="xs">
|
||||||
|
<Tooltip
|
||||||
|
label={opened ? "Collapse navigation" : "Expand navigation"}
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => setOpened((v) => !v)}
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
{opened ? <IconChevronLeft /> : <IconChevronRight />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</AppShell.Section>
|
||||||
|
|
||||||
|
<AppShell.Section grow component={ScrollArea} flex={1}>
|
||||||
|
<NavigationDashboard />
|
||||||
|
</AppShell.Section>
|
||||||
|
|
||||||
|
<AppShell.Section>
|
||||||
|
<HostView />
|
||||||
|
</AppShell.Section>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
|
||||||
|
<AppShell.Main>
|
||||||
|
<Stack>
|
||||||
|
<Paper withBorder shadow="md" radius="lg" p="md">
|
||||||
|
<Flex align="center" gap="md">
|
||||||
|
{!opened && (
|
||||||
|
<Tooltip label="Open navigation menu" withArrow>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => setOpened(true)}
|
||||||
|
aria-label="Open navigation"
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
<IconChevronRight />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Title order={3} fw={600}>
|
||||||
|
App Dashboard
|
||||||
|
</Title>
|
||||||
|
</Flex>
|
||||||
|
</Paper>
|
||||||
|
<Outlet />
|
||||||
|
</Stack>
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------- Host Info ----------------------- */
|
||||||
|
function HostView() {
|
||||||
|
const [host, setHost] = useState<User | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchHost() {
|
||||||
|
const { data } = await apiFetch.api.user.find.get();
|
||||||
|
setHost(data?.user ?? null);
|
||||||
|
}
|
||||||
|
fetchHost();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card radius="lg" withBorder shadow="sm" p="md">
|
||||||
|
{host ? (
|
||||||
|
<Stack>
|
||||||
|
<Flex gap="md" align="center">
|
||||||
|
<Avatar size="md" radius="xl" color="blue">
|
||||||
|
{host.name?.[0]}
|
||||||
|
</Avatar>
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text fw={600}>{host.name}</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{host.email}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
<Divider />
|
||||||
|
<Logout />
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
|
No host information available
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------- Navigation ----------------------- */
|
||||||
|
function NavigationDashboard() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isActive = (path: keyof typeof clientRoute) =>
|
||||||
|
location.pathname.startsWith(clientRoute[path]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="xs" p="sm">
|
||||||
|
<NavLink
|
||||||
|
active={isActive("/scr/dashboard/dashboard-home")}
|
||||||
|
leftSection={<IconDashboard size={20} />}
|
||||||
|
label="Dashboard Overview"
|
||||||
|
description="Quick summary and activity highlights"
|
||||||
|
onClick={() => navigate(clientRoutes["/scr/dashboard/dashboard-home"])}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
active={isActive("/scr/dashboard/apikey/apikey")}
|
||||||
|
leftSection={<IconKey size={20} />}
|
||||||
|
label="Dashboard Overview"
|
||||||
|
description="Quick summary and activity highlights"
|
||||||
|
onClick={() => navigate(clientRoutes["/scr/dashboard/apikey/apikey"])}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
active={isActive("/scr/dashboard/credential/credential")}
|
||||||
|
leftSection={<IconLock size={20} />}
|
||||||
|
label="Dashboard Overview"
|
||||||
|
description="Quick summary and activity highlights"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(clientRoutes["/scr/dashboard/credential/credential"])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/pages/scr/scr_layout.tsx
Normal file
25
src/pages/scr/scr_layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Navigate, Outlet } from "react-router-dom";
|
||||||
|
import clientRoutes from "@/clientRoutes";
|
||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
|
||||||
|
export default function ProtectedRoute() {
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function checkSession() {
|
||||||
|
try {
|
||||||
|
// backend otomatis baca cookie JWT dari request
|
||||||
|
const res = await apiFetch.api.user.find.get();
|
||||||
|
setIsAuthenticated(res.status === 200);
|
||||||
|
} catch {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkSession();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isAuthenticated === null) return null; // or loading spinner
|
||||||
|
if (!isAuthenticated) return <Navigate to={clientRoutes["/login"]} replace />;
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
@@ -50,7 +50,7 @@ export async function convertOpenApiToMcp(baseUrl: string): Promise<McpManifest>
|
|||||||
for (const [method, def] of Object.entries<any>(methods)) {
|
for (const [method, def] of Object.entries<any>(methods)) {
|
||||||
const tags = def.tags || ["default"]
|
const tags = def.tags || ["default"]
|
||||||
const tag = tags[0]
|
const tag = tags[0]
|
||||||
const operationId = def.operationId || `${method}_${path.replace(/[\/{}]/g, "_")}`
|
const operationId = def.operationId || `${method}_${path.replace(/\//g, "_")}`
|
||||||
|
|
||||||
manifest.capabilities[tag] ??= {}
|
manifest.capabilities[tag] ??= {}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type JWT = {
|
|||||||
|
|
||||||
const ApiKeyRoute = new Elysia({
|
const ApiKeyRoute = new Elysia({
|
||||||
prefix: '/apikey',
|
prefix: '/apikey',
|
||||||
detail: { tags: ['apikey'] },
|
tags: ["apikey"],
|
||||||
})
|
})
|
||||||
.post(
|
.post(
|
||||||
'/create',
|
'/create',
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
import { jwt as jwtPlugin, type JWTPayloadSpec } from '@elysiajs/jwt'
|
import { jwt as jwtPlugin, type JWTPayloadSpec } from '@elysiajs/jwt'
|
||||||
import Elysia, { t, type Cookie, type HTTPHeaders, type StatusMap } from 'elysia'
|
import Elysia, { t, type Cookie, type HTTPHeaders, type StatusMap } from 'elysia'
|
||||||
import { type ElysiaCookie } from 'elysia/cookies'
|
import { type ElysiaCookie } from 'elysia/cookies'
|
||||||
|
|
||||||
import { prisma } from '@/server/lib/prisma'
|
import { prisma } from '@/server/lib/prisma'
|
||||||
import type { User } from 'generated/prisma'
|
|
||||||
|
|
||||||
const secret = process.env.JWT_SECRET
|
const secret = process.env.JWT_SECRET
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
@@ -109,7 +107,7 @@ async function login({
|
|||||||
|
|
||||||
const Auth = new Elysia({
|
const Auth = new Elysia({
|
||||||
prefix: '/auth',
|
prefix: '/auth',
|
||||||
detail: { description: 'Auth API', summary: 'Auth API', tags: ['auth'] },
|
tags: ["auth"],
|
||||||
})
|
})
|
||||||
.use(
|
.use(
|
||||||
jwtPlugin({
|
jwtPlugin({
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import Elysia, { t } from "elysia";
|
|||||||
import { prisma } from "../lib/prisma";
|
import { prisma } from "../lib/prisma";
|
||||||
|
|
||||||
const CredentialRoute = new Elysia({
|
const CredentialRoute = new Elysia({
|
||||||
prefix: "/credential"
|
prefix: "/credential",
|
||||||
|
tags: ["credential"],
|
||||||
})
|
})
|
||||||
.post("/create", async (ctx) => {
|
.post("/create", async (ctx) => {
|
||||||
const { name, value } = ctx.body
|
const { name, value } = ctx.body
|
||||||
@@ -26,7 +27,7 @@ const CredentialRoute = new Elysia({
|
|||||||
description: 'create credential',
|
description: 'create credential',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.get("/list", async (ctx) => {
|
.get("/list", async () => {
|
||||||
const list = await prisma.credential.findMany()
|
const list = await prisma.credential.findMany()
|
||||||
return {
|
return {
|
||||||
message: "success",
|
message: "success",
|
||||||
@@ -40,7 +41,7 @@ const CredentialRoute = new Elysia({
|
|||||||
})
|
})
|
||||||
.delete("/rm", async (ctx) => {
|
.delete("/rm", async (ctx) => {
|
||||||
const { id } = ctx.body
|
const { id } = ctx.body
|
||||||
const rm = await prisma.credential.delete({
|
await prisma.credential.delete({
|
||||||
where: {
|
where: {
|
||||||
id: id
|
id: id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,34 +5,6 @@ const url = "https://cld-dkr-makuro-seafile.wibudev.com/api2"
|
|||||||
const TOKEN = "fa49bf1774cad2ec89d2882ae2c6ac1f5d7df445"
|
const TOKEN = "fa49bf1774cad2ec89d2882ae2c6ac1f5d7df445"
|
||||||
const REPO_ID = "de64ff3c-0081-45f3-a5a6-6c799a098649"
|
const REPO_ID = "de64ff3c-0081-45f3-a5a6-6c799a098649"
|
||||||
|
|
||||||
const pengaduanDesa: string[] = [
|
|
||||||
"Pengaduan Pelayanan Publik Desa",
|
|
||||||
"Pengaduan Bantuan Sosial (Bansos)",
|
|
||||||
"Pengaduan Penyalahgunaan Dana Desa",
|
|
||||||
"Pengaduan Infrastruktur Rusak (jalan, jembatan, saluran air)",
|
|
||||||
"Pengaduan Lingkungan (sampah, pencemaran, banjir)",
|
|
||||||
"Pengaduan Keamanan dan Ketertiban",
|
|
||||||
"Pengaduan Sengketa Tanah Desa",
|
|
||||||
"Pengaduan Ketenagakerjaan (tenaga kerja lokal, proyek desa)",
|
|
||||||
"Pengaduan Disiplin Aparat Desa",
|
|
||||||
"Pengaduan Administrasi Kependudukan (KTP, KK, surat menyurat)",
|
|
||||||
"Pengaduan Layanan Kesehatan Masyarakat",
|
|
||||||
"Pengaduan Pendidikan (sekolah, bantuan siswa miskin)",
|
|
||||||
"Pengaduan Usaha Mikro dan UMKM Desa",
|
|
||||||
"Pengaduan Kegiatan Bumdes",
|
|
||||||
"Pengaduan Pungutan Liar atau Gratifikasi",
|
|
||||||
"Pengaduan Kekerasan Rumah Tangga atau Sosial",
|
|
||||||
"Pengaduan Pelanggaran Adat dan Norma Sosial",
|
|
||||||
"Pengaduan Proyek Pembangunan Tidak Transparan",
|
|
||||||
"Pengaduan Bencana Alam dan Penanganannya",
|
|
||||||
"Pengaduan Diskriminasi atau Ketidakadilan Sosial",
|
|
||||||
"Pengaduan Pelanggaran Hak Tanah Kas Desa",
|
|
||||||
"Pengaduan Penyaluran Air dan Irigasi",
|
|
||||||
"Pengaduan Akses Internet atau Telekomunikasi Desa",
|
|
||||||
"Pengaduan Fasilitas Umum Tidak Layak",
|
|
||||||
"Pengaduan Kegiatan Tidak Berizin di Wilayah Desa"
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
|
|
||||||
const DarmasabaRoute = new Elysia({
|
const DarmasabaRoute = new Elysia({
|
||||||
prefix: "/darmasaba",
|
prefix: "/darmasaba",
|
||||||
|
|||||||
52
src/server/routes/layanan_route.ts
Normal file
52
src/server/routes/layanan_route.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
|
||||||
|
const layanan = `
|
||||||
|
KTP
|
||||||
|
Kartu Keluarga
|
||||||
|
surat keterangan domisili
|
||||||
|
surat pengantar nikah
|
||||||
|
akta kelahiran
|
||||||
|
akta kematian
|
||||||
|
surat pindah penduduk
|
||||||
|
surat keterangan usaha
|
||||||
|
surat keterangan tidak mampu
|
||||||
|
surat keterangan waris
|
||||||
|
surat perizinan usaha kecil
|
||||||
|
`
|
||||||
|
|
||||||
|
const LayananRoute = new Elysia({
|
||||||
|
prefix: "layanan",
|
||||||
|
tags: ["layanan"],
|
||||||
|
})
|
||||||
|
.get("/list", () => {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: layanan.split("\n")
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
detail: {
|
||||||
|
summary: "list",
|
||||||
|
description: "list layanan yang ada",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post("create-ktp", () => {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: ""
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
jenis: t.Union([
|
||||||
|
t.Literal("ktp"),
|
||||||
|
t.Literal("kk"),
|
||||||
|
]),
|
||||||
|
nama: t.String(),
|
||||||
|
deskripsi: t.String(),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "create",
|
||||||
|
description: "create layanan",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default LayananRoute
|
||||||
51
src/server/routes/user_route.ts
Normal file
51
src/server/routes/user_route.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
import type { User } from "generated/prisma";
|
||||||
|
import { prisma } from "../lib/prisma";
|
||||||
|
|
||||||
|
const UserRoute = new Elysia({
|
||||||
|
prefix: "user",
|
||||||
|
tags: ["user"],
|
||||||
|
})
|
||||||
|
.get('/find', (ctx) => {
|
||||||
|
const { user } = ctx as any
|
||||||
|
return {
|
||||||
|
user: user as User
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
detail: {
|
||||||
|
summary: "find",
|
||||||
|
description: "find user",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post("/upsert", async (ctx) => {
|
||||||
|
const { name, phone } = ctx.body
|
||||||
|
const upsert = await prisma.user.upsert({
|
||||||
|
where: {
|
||||||
|
phone
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name,
|
||||||
|
phone
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
upsert
|
||||||
|
}
|
||||||
|
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String({ minLength: 1, error: "name is required" }),
|
||||||
|
phone: t.String({ minLength: 1, error: "phone is required" })
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "upsert",
|
||||||
|
description: "upsert user",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default UserRoute
|
||||||
127
xx.ts
Normal file
127
xx.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { readdirSync, statSync, writeFileSync } from "fs";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { basename, extname, join, relative } from "path";
|
||||||
|
|
||||||
|
const PAGES_DIR = join(process.cwd(), "src/pages");
|
||||||
|
const OUTPUT_FILE = join(process.cwd(), "src/AppRoutes.tsx");
|
||||||
|
|
||||||
|
// 🧩 Ubah nama file ke nama komponen (PascalCase)
|
||||||
|
const toComponentName = (fileName: string) =>
|
||||||
|
fileName
|
||||||
|
.replace(/_/g, " ")
|
||||||
|
.replace(/\b\w/g, (c) => c.toUpperCase())
|
||||||
|
.replace(/\s/g, "");
|
||||||
|
|
||||||
|
// 🧩 Ubah nama file ke path route
|
||||||
|
function toRoutePath(name: string): string {
|
||||||
|
if (name.toLowerCase() === "home") return "/";
|
||||||
|
if (name.toLowerCase() === "login") return "/login";
|
||||||
|
if (name.toLowerCase() === "notfound") return "/*";
|
||||||
|
if (name.endsWith("_page")) return name.replace("_page", "").toLowerCase();
|
||||||
|
if (name.startsWith("form_")) return name.replace("form_", "").toLowerCase();
|
||||||
|
return name.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧭 Scan folder pages secara rekursif
|
||||||
|
function scan(dir: string): any[] {
|
||||||
|
const items = readdirSync(dir);
|
||||||
|
const routes: any[] = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const full = join(dir, item);
|
||||||
|
const stat = statSync(full);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
routes.push({
|
||||||
|
name: item,
|
||||||
|
path: item.toLowerCase(),
|
||||||
|
children: scan(full),
|
||||||
|
});
|
||||||
|
} else if (extname(item) === ".tsx") {
|
||||||
|
routes.push({
|
||||||
|
name: basename(item, ".tsx"),
|
||||||
|
filePath: relative(join(process.cwd(), "src"), full).replace(/\\/g, "/"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🏗️ Generate <Route> JSX dari struktur folder
|
||||||
|
function generateJSX(routes: any[], parentPath = ""): string {
|
||||||
|
let jsx = "";
|
||||||
|
|
||||||
|
for (const route of routes) {
|
||||||
|
if (route.children) {
|
||||||
|
// cari layout di folder
|
||||||
|
const layout = route.children.find((r: any) => r.name.endsWith("_layout"));
|
||||||
|
if (layout) {
|
||||||
|
const LayoutComponent = toComponentName(layout.name.replace("_layout", "Layout"));
|
||||||
|
const nested = route.children.filter((r: any) => r !== layout);
|
||||||
|
const nestedRoutes = generateJSX(nested, `${parentPath}/${route.path}`);
|
||||||
|
jsx += `
|
||||||
|
<Route path="${parentPath}/${route.path}" element={<${LayoutComponent} />}>
|
||||||
|
${nestedRoutes}
|
||||||
|
</Route>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
jsx += generateJSX(route.children, `${parentPath}/${route.path}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const Component = toComponentName(route.name);
|
||||||
|
const routePath = toRoutePath(route.name);
|
||||||
|
|
||||||
|
// Hapus duplikasi segmen
|
||||||
|
const fullPath =
|
||||||
|
routePath.startsWith("/")
|
||||||
|
? routePath
|
||||||
|
: `${parentPath}/${_.kebabCase(routePath)}`.replace(/\/+/g, "/");
|
||||||
|
|
||||||
|
jsx += `<Route path="${fullPath}" element={<${Component} />} />\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧾 Generate import otomatis
|
||||||
|
function generateImports(routes: any[]): string {
|
||||||
|
const imports = new Set<string>();
|
||||||
|
|
||||||
|
function collect(rs: any[]) {
|
||||||
|
for (const r of rs) {
|
||||||
|
if (r.children) collect(r.children);
|
||||||
|
else {
|
||||||
|
const Comp = toComponentName(r.name);
|
||||||
|
imports.add(`import ${Comp} from "./${r.filePath.replace(/\.tsx$/, "")}";`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collect(routes);
|
||||||
|
return Array.from(imports).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧠 Main generator
|
||||||
|
const allRoutes = scan(PAGES_DIR);
|
||||||
|
const imports = generateImports(allRoutes);
|
||||||
|
const jsxRoutes = generateJSX(allRoutes);
|
||||||
|
|
||||||
|
const finalCode = `
|
||||||
|
// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY
|
||||||
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
|
import ProtectedRoute from "./components/ProtectedRoute";
|
||||||
|
${imports}
|
||||||
|
|
||||||
|
export default function AppRoutes() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
${jsxRoutes}
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
writeFileSync(OUTPUT_FILE, finalCode);
|
||||||
|
console.log("✅ Routes generated successfully → src/AppRoutes.generated.tsx");
|
||||||
|
Bun.spawnSync(["bunx", "prettier", "--write", "src/**/*.tsx"])
|
||||||
Reference in New Issue
Block a user