This commit is contained in:
john aperkat
2025-07-30 04:18:44 +00:00
parent 3206db6010
commit afe9b24f56
19 changed files with 1983 additions and 657 deletions

408
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "workspace", "name": "workspace",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@react-pdf/renderer": "^4.3.0",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
@@ -2912,6 +2913,171 @@
} }
} }
}, },
"node_modules/@react-pdf/fns": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.2.tgz",
"integrity": "sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g=="
},
"node_modules/@react-pdf/font": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.2.tgz",
"integrity": "sha512-/dAWu7Y2RD1RxarDZ9SkYPHgBYOhmcDnet4W/qN/m8k+A2Hr3ja54GymSR7GGxWBtxjKtNauVKrTa9LS1n8WUw==",
"dependencies": {
"@react-pdf/pdfkit": "^4.0.3",
"@react-pdf/types": "^2.9.0",
"fontkit": "^2.0.2",
"is-url": "^1.2.4"
}
},
"node_modules/@react-pdf/image": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.0.3.tgz",
"integrity": "sha512-lvP5ryzYM3wpbO9bvqLZYwEr5XBDX9jcaRICvtnoRqdJOo7PRrMnmB4MMScyb+Xw10mGeIubZAAomNAG5ONQZQ==",
"dependencies": {
"@react-pdf/png-js": "^3.0.0",
"jay-peg": "^1.1.1"
}
},
"node_modules/@react-pdf/layout": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.4.0.tgz",
"integrity": "sha512-Aq+Cc6JYausWLoks2FvHe3PwK9cTuvksB2uJ0AnkKJEUtQbvCq8eCRb1bjbbwIji9OzFRTTzZij7LzkpKHjIeA==",
"dependencies": {
"@react-pdf/fns": "3.1.2",
"@react-pdf/image": "^3.0.3",
"@react-pdf/primitives": "^4.1.1",
"@react-pdf/stylesheet": "^6.1.0",
"@react-pdf/textkit": "^6.0.0",
"@react-pdf/types": "^2.9.0",
"emoji-regex": "^10.3.0",
"queue": "^6.0.1",
"yoga-layout": "^3.2.1"
}
},
"node_modules/@react-pdf/layout/node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="
},
"node_modules/@react-pdf/pdfkit": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-4.0.3.tgz",
"integrity": "sha512-k+Lsuq8vTwWsCqTp+CCB4+2N+sOTFrzwGA7aw3H9ix/PDWR9QksbmNg0YkzGbLAPI6CeawmiLHcf4trZ5ecLPQ==",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@react-pdf/png-js": "^3.0.0",
"browserify-zlib": "^0.2.0",
"crypto-js": "^4.2.0",
"fontkit": "^2.0.2",
"jay-peg": "^1.1.1",
"linebreak": "^1.1.0",
"vite-compatible-readable-stream": "^3.6.1"
}
},
"node_modules/@react-pdf/png-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@react-pdf/png-js/-/png-js-3.0.0.tgz",
"integrity": "sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==",
"dependencies": {
"browserify-zlib": "^0.2.0"
}
},
"node_modules/@react-pdf/primitives": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.1.1.tgz",
"integrity": "sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ=="
},
"node_modules/@react-pdf/reconciler": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-1.1.4.tgz",
"integrity": "sha512-oTQDiR/t4Z/Guxac88IavpU2UgN7eR0RMI9DRKvKnvPz2DUasGjXfChAdMqDNmJJxxV26mMy9xQOUV2UU5/okg==",
"dependencies": {
"object-assign": "^4.1.1",
"scheduler": "0.25.0-rc-603e6108-20241029"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-pdf/reconciler/node_modules/scheduler": {
"version": "0.25.0-rc-603e6108-20241029",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz",
"integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA=="
},
"node_modules/@react-pdf/render": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.3.0.tgz",
"integrity": "sha512-MdWfWaqO6d7SZD75TZ2z5L35V+cHpyA43YNRlJNG0RJ7/MeVGDQv12y/BXOJgonZKkeEGdzM3EpAt9/g4E22WA==",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@react-pdf/fns": "3.1.2",
"@react-pdf/primitives": "^4.1.1",
"@react-pdf/textkit": "^6.0.0",
"@react-pdf/types": "^2.9.0",
"abs-svg-path": "^0.1.1",
"color-string": "^1.9.1",
"normalize-svg-path": "^1.1.0",
"parse-svg-path": "^0.1.2",
"svg-arc-to-cubic-bezier": "^3.2.0"
}
},
"node_modules/@react-pdf/renderer": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.3.0.tgz",
"integrity": "sha512-28gpA69fU9ZQrDzmd5xMJa1bDf8t0PT3ApUKBl2PUpoE/x4JlvCB5X66nMXrfFrgF2EZrA72zWQAkvbg7TE8zw==",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@react-pdf/fns": "3.1.2",
"@react-pdf/font": "^4.0.2",
"@react-pdf/layout": "^4.4.0",
"@react-pdf/pdfkit": "^4.0.3",
"@react-pdf/primitives": "^4.1.1",
"@react-pdf/reconciler": "^1.1.4",
"@react-pdf/render": "^4.3.0",
"@react-pdf/types": "^2.9.0",
"events": "^3.3.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"queue": "^6.0.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-pdf/stylesheet": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.1.0.tgz",
"integrity": "sha512-BGZ2sYNUp38VJUegjva/jsri3iiRGnVNjWI+G9dTwAvLNOmwFvSJzqaCsEnqQ/DW5mrTBk/577FhDY7pv6AidA==",
"dependencies": {
"@react-pdf/fns": "3.1.2",
"@react-pdf/types": "^2.9.0",
"color-string": "^1.9.1",
"hsl-to-hex": "^1.0.0",
"media-engine": "^1.0.3",
"postcss-value-parser": "^4.1.0"
}
},
"node_modules/@react-pdf/textkit": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.0.0.tgz",
"integrity": "sha512-fDt19KWaJRK/n2AaFoVm31hgGmpygmTV7LsHGJNGZkgzXcFyLsx+XUl63DTDPH3iqxj3xUX128t104GtOz8tTw==",
"dependencies": {
"@react-pdf/fns": "3.1.2",
"bidi-js": "^1.0.2",
"hyphen": "^1.6.4",
"unicode-properties": "^1.4.1"
}
},
"node_modules/@react-pdf/types": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.9.0.tgz",
"integrity": "sha512-ckj80vZLlvl9oYrQ4tovEaqKWP3O06Eb1D48/jQWbdwz1Yh7Y9v1cEmwlP8ET+a1Whp8xfdM0xduMexkuPANCQ==",
"dependencies": {
"@react-pdf/font": "^4.0.2",
"@react-pdf/primitives": "^4.1.1",
"@react-pdf/stylesheet": "^6.1.0"
}
},
"node_modules/@reduxjs/toolkit": { "node_modules/@reduxjs/toolkit": {
"version": "2.8.2", "version": "2.8.2",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz",
@@ -3279,6 +3445,14 @@
"url": "https://github.com/sponsors/gregberge" "url": "https://github.com/sponsors/gregberge"
} }
}, },
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@testing-library/dom": { "node_modules/@testing-library/dom": {
"version": "10.4.0", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -4152,6 +4326,11 @@
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"optional": true "optional": true
}, },
"node_modules/abs-svg-path": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz",
"integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA=="
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -4956,6 +5135,25 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/batch": { "node_modules/batch": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -4976,6 +5174,14 @@
"node": ">= 8.0.0" "node": ">= 8.0.0"
} }
}, },
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/big.js": { "node_modules/big.js": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -5086,11 +5292,27 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"dependencies": {
"base64-js": "^1.1.2"
}
},
"node_modules/browser-process-hrtime": { "node_modules/browser-process-hrtime": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
"integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="
}, },
"node_modules/browserify-zlib": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
"dependencies": {
"pako": "~1.0.5"
}
},
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.25.0", "version": "4.25.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
@@ -5426,6 +5648,14 @@
"wrap-ansi": "^7.0.0" "wrap-ansi": "^7.0.0"
} }
}, },
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/clsx": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -5549,6 +5779,15 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
}, },
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color-support": { "node_modules/color-support": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
@@ -5778,6 +6017,11 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/crypto-random-string": { "node_modules/crypto-random-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -6509,6 +6753,11 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"node_modules/dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
},
"node_modules/didyoumean": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -7996,6 +8245,22 @@
} }
} }
}, },
"node_modules/fontkit": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
"dependencies": {
"@swc/helpers": "^0.5.12",
"brotli": "^1.3.2",
"clone": "^2.1.2",
"dfa": "^1.2.0",
"fast-deep-equal": "^3.1.3",
"restructure": "^3.0.0",
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.4.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -8694,6 +8959,19 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"node_modules/hsl-to-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz",
"integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==",
"dependencies": {
"hsl-to-rgb-for-reals": "^1.1.0"
}
},
"node_modules/hsl-to-rgb-for-reals": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz",
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg=="
},
"node_modules/html-encoding-sniffer": { "node_modules/html-encoding-sniffer": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@@ -8888,6 +9166,11 @@
"node": ">=10.17.0" "node": ">=10.17.0"
} }
}, },
"node_modules/hyphen": {
"version": "1.10.6",
"resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.10.6.tgz",
"integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw=="
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -9674,6 +9957,14 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/jay-peg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz",
"integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==",
"dependencies": {
"restructure": "^3.0.0"
}
},
"node_modules/jest": { "node_modules/jest": {
"version": "27.5.1", "version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
@@ -10784,6 +11075,23 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/linebreak/node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -10937,6 +11245,11 @@
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA=="
}, },
"node_modules/media-engine": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz",
"integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg=="
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -11317,6 +11630,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/normalize-svg-path": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz",
"integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==",
"dependencies": {
"svg-arc-to-cubic-bezier": "^3.0.0"
}
},
"node_modules/normalize-url": { "node_modules/normalize-url": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
@@ -11655,6 +11976,11 @@
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/param-case": { "node_modules/param-case": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -11692,6 +12018,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/parse-svg-path": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz",
"integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ=="
},
"node_modules/parse5": { "node_modules/parse5": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
@@ -13293,6 +13624,14 @@
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
}, },
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"dependencies": {
"inherits": "~2.0.3"
}
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -14029,6 +14368,11 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/restructure": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="
},
"node_modules/retry": { "node_modules/retry": {
"version": "0.13.1", "version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@@ -14675,6 +15019,19 @@
"simple-concat": "^1.0.0" "simple-concat": "^1.0.0"
} }
}, },
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/sisteransi": { "node_modules/sisteransi": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -15328,6 +15685,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/svg-arc-to-cubic-bezier": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
"integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g=="
},
"node_modules/svg-parser": { "node_modules/svg-parser": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
@@ -15756,6 +16118,11 @@
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
}, },
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
},
"node_modules/tiny-invariant": { "node_modules/tiny-invariant": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -16070,6 +16437,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"dependencies": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/unicode-property-aliases-ecmascript": { "node_modules/unicode-property-aliases-ecmascript": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
@@ -16078,6 +16454,20 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unicode-trie/node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
},
"node_modules/unique-string": { "node_modules/unique-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
@@ -16260,6 +16650,19 @@
"d3-timer": "^3.0.1" "d3-timer": "^3.0.1"
} }
}, },
"node_modules/vite-compatible-readable-stream": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz",
"integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/w3c-hr-time": { "node_modules/w3c-hr-time": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
@@ -17178,6 +17581,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/yoga-layout": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="
},
"node_modules/zlibjs": { "node_modules/zlibjs": {
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",

View File

@@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@react-pdf/renderer": "^4.3.0",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

BIN
public/asfasf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

BIN
public/ikasapta.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

View File

@@ -2,13 +2,10 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/ikasapta.png" type="image/png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta name="description" content="Website SOLID DATA" />
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
@@ -25,7 +22,7 @@
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>SOLID DATA</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,19 +1,19 @@
{ {
"short_name": "React App", "short_name": "IKASAPTA",
"name": "Create React App Sample", "name": "IKASAPTA HUB",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "ikasapta.png",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon" "type": "image/x-icon"
}, },
{ {
"src": "logo192.png", "src": "ikasapta.png",
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
}, },
{ {
"src": "logo512.png", "src": "ikasapta.png",
"type": "image/png", "type": "image/png",
"sizes": "512x512" "sizes": "512x512"
} }

View File

@@ -43,7 +43,7 @@ const Dashboard = () => {
try { try {
const response = await fetch( const response = await fetch(
"https://bot.kediritechnopark.com/webhook/dashboard/psi", "https://bot.kediritechnopark.com/webhook/solid-data/dashboard",
{ {
method: "GET", method: "GET",
headers: { headers: {
@@ -75,7 +75,7 @@ const Dashboard = () => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
try { try {
const response = await fetch( const response = await fetch(
"https://bot.kediritechnopark.com/webhook/list-user/psi", "https://bot.kediritechnopark.com/webhook/solid-data/list-user",
{ {
method: "GET", method: "GET",
headers: { headers: {
@@ -110,7 +110,7 @@ const Dashboard = () => {
try { try {
const response = await fetch( const response = await fetch(
"https://bot.kediritechnopark.com/webhook/add-officer", "https://bot.kediritechnopark.com/webhook/solid-data/add-officer",
{ {
method: "POST", method: "POST",
headers: { headers: {
@@ -163,7 +163,7 @@ const Dashboard = () => {
try { try {
const response = await fetch( const response = await fetch(
`https://bot.kediritechnopark.com/webhook/psi/delete-officer`, `https://bot.kediritechnopark.com/webhook/solid-data/delete-officer`,
{ {
method: "DELETE", method: "DELETE",
headers: { headers: {
@@ -193,8 +193,11 @@ const Dashboard = () => {
<div className={styles.dashboardContainer}> <div className={styles.dashboardContainer}>
<div className={styles.dashboardHeader}> <div className={styles.dashboardHeader}>
<div className={styles.logoAndTitle}> <div className={styles.logoAndTitle}>
<img src="/PSI.png" alt="Bot Avatar" /> <img src="/ikasapta.png" alt="Bot Avatar" />
<h1 className={styles.h1}>Kawal PSI Dashboard</h1> <h1 className={styles.h1}>SOLID</h1>
<h1 className={styles.h1} styles="color: #43a0a7;">
DATA
</h1>
</div> </div>
<div className={styles.dropdownContainer} ref={menuRef}> <div className={styles.dropdownContainer} ref={menuRef}>
@@ -343,7 +346,7 @@ const Dashboard = () => {
)} )}
<div className={styles.chartSection}> <div className={styles.chartSection}>
<h2>Grafik Pertumbuhan Anggota</h2> <h2>Grafik Upload Document</h2>
{officerPerformanceData.length > 0 ? ( {officerPerformanceData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
<BarChart data={officerPerformanceData}> <BarChart data={officerPerformanceData}>
@@ -355,7 +358,7 @@ const Dashboard = () => {
</ResponsiveContainer> </ResponsiveContainer>
) : ( ) : (
<div className={styles.warning}> <div className={styles.warning}>
📋 Belum ada data performa untuk ditampilkan 📋 Belum ada data upload untuk ditampilkan
</div> </div>
)} )}
</div> </div>
@@ -370,7 +373,7 @@ const Dashboard = () => {
</div> </div>
<div className={styles.footer}> <div className={styles.footer}>
© 2025 Kediri Technopark Dashboard PSI © 2025 Kediri Technopark Dashboard SOLID DATA
</div> </div>
</div> </div>
); );

View File

@@ -14,7 +14,7 @@
--neutral-800: #262626; --neutral-800: #262626;
--neutral-900: #171717; --neutral-900: #171717;
--white: #ffffff; --white: #ffffff;
--success-green: #10b981; --success-green: #43a0a7;
--warning-amber: #f59e0b; --warning-amber: #f59e0b;
--error-red: #ef4444; --error-red: #ef4444;
--text-primary: #0f172a; --text-primary: #0f172a;
@@ -59,7 +59,7 @@ body {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
border-bottom: 3px solid #ef4444; border-bottom: 3px solid #43a0a7;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 50; z-index: 50;
@@ -81,10 +81,18 @@ body {
} }
.dashboardHeader .h1 { .dashboardHeader .h1 {
margin: 2px;
font-size: 1.5rem;
font-weight: 700;
color: #43a0a7;
letter-spacing: -0.025em;
}
.data {
margin: 0; margin: 0;
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
color: #ed4344; color: #154666;
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
@@ -207,7 +215,7 @@ body {
.summaryCard p { .summaryCard p {
font-size: 2rem; font-size: 2rem;
font-weight: 700; font-weight: 700;
color: #ef4444; color: #43a0a7;
margin: 0; margin: 0;
line-height: 1; line-height: 1;
} }
@@ -270,7 +278,7 @@ body {
} }
.submitButton { .submitButton {
background-color: #ef4444; background-color: #43a0a7;
color: var(--text-light); color: var(--text-light);
border: none; border: none;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
@@ -285,7 +293,7 @@ body {
} }
.submitButton:hover { .submitButton:hover {
background-color: #d03b3b; background-color: #357734;
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }
@@ -296,9 +304,9 @@ body {
/* Messages */ /* Messages */
.success { .success {
background-color: rgb(16 185 129 / 0.1); background-color: rgb(67 160 167 / 0.1);
color: var(--success-green); color: var(--success-green);
border: 1px solid rgb(16 185 129 / 0.2); border: 1px solid rgb(67 160 167 / 0.2);
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
margin-top: 1rem; margin-top: 1rem;
@@ -318,9 +326,9 @@ body {
} }
.warning { .warning {
background-color: #ef444417; background-color: rgb(67 160 167 / 0.1);
color: #ef4444; color: #43a0a7;
border: 1px solid #ef444433; border: 1px solid rgb(67 160 167 / 0.2);
padding: 1rem; padding: 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
margin-top: 1rem; margin-top: 1rem;

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import styles from "./FileListComponent.module.css"; import styles from "./FileListComponent.module.css";
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";
import { PDFDownloadLink } from "@react-pdf/renderer";
import KTPPDF from "./KTPPDF";
const FileListComponent = ({ const FileListComponent = ({
setTotalFilesSentToday, setTotalFilesSentToday,
@@ -9,17 +11,18 @@ const FileListComponent = ({
setOfficerPerformanceData, setOfficerPerformanceData,
}) => { }) => {
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
const [filteredFiles, setFilteredFiles] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedFile, setSelectedFile] = useState(null); const [selectedFile, setSelectedFile] = useState(null);
const [successMessage, setSuccessMessage] = useState(""); const [successMessage, setSuccessMessage] = useState("");
const [selectedDocumentType, setSelectedDocumentType] = useState("");
useEffect(() => { useEffect(() => {
const fetchFiles = async () => { const fetchFiles = async () => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
try { try {
const response = await fetch( const response = await fetch(
"https://bot.kediritechnopark.com/webhook/files", "https://bot.kediritechnopark.com/webhook/solid-data/files",
{ {
method: "GET", method: "GET",
headers: { headers: {
@@ -29,35 +32,25 @@ const FileListComponent = ({
} }
); );
if (!response.ok) { if (!response.ok)
throw new Error(`HTTP error! Status: ${response.status}`); throw new Error(`HTTP error! Status: ${response.status}`);
}
const text = await response.text(); const text = await response.text();
if (!text) throw new Error("Server membalas kosong.");
if (!text) {
throw new Error("Server membalas kosong.");
}
const data = JSON.parse(text); const data = JSON.parse(text);
if (!data.success || !Array.isArray(data.data))
if (!data.success || !Array.isArray(data.data)) {
throw new Error("Format respons tidak valid."); throw new Error("Format respons tidak valid.");
}
const fileData = data.data; const fileData = data.data;
// 1. Set ke state
setFiles(fileData); setFiles(fileData);
setFilteredFiles(fileData);
// 2. Hitung total file hari ini
const today = new Date().toISOString().slice(0, 10); const today = new Date().toISOString().slice(0, 10);
const totalToday = fileData.filter((f) => const totalToday = fileData.filter((f) =>
f.created_at.startsWith(today) f.created_at.startsWith(today)
).length; ).length;
setTotalFilesSentToday(totalToday); setTotalFilesSentToday(totalToday);
// 3. Hitung total bulan ini
const now = new Date(); const now = new Date();
const currentMonth = now.getMonth(); const currentMonth = now.getMonth();
const currentYear = now.getFullYear(); const currentYear = now.getFullYear();
@@ -69,10 +62,8 @@ const FileListComponent = ({
}).length; }).length;
setTotalFilesSentMonth(totalThisMonth); setTotalFilesSentMonth(totalThisMonth);
// 4. Total keseluruhan
setTotalFilesSentOverall(fileData.length); setTotalFilesSentOverall(fileData.length);
// 5. Grafik performa per bulan (dinamis)
const dateObjects = fileData.map((item) => new Date(item.created_at)); const dateObjects = fileData.map((item) => new Date(item.created_at));
if (dateObjects.length > 0) { if (dateObjects.length > 0) {
const minDate = new Date(Math.min(...dateObjects)); const minDate = new Date(Math.min(...dateObjects));
@@ -95,19 +86,17 @@ const FileListComponent = ({
const monthKey = `${d.getFullYear()}-${String( const monthKey = `${d.getFullYear()}-${String(
d.getMonth() + 1 d.getMonth() + 1
).padStart(2, "0")}`; ).padStart(2, "0")}`;
if (monthlyDataMap[monthKey] !== undefined) { if (monthlyDataMap[monthKey] !== undefined)
monthlyDataMap[monthKey]++; monthlyDataMap[monthKey]++;
}
}); });
const performanceArray = Object.entries(monthlyDataMap).map( const performanceArray = Object.entries(monthlyDataMap).map(
([month, count]) => { ([month, count]) => {
const [year, monthNum] = month.split("-");
const dateObj = new Date(`${month}-01`); const dateObj = new Date(`${month}-01`);
const label = new Intl.DateTimeFormat("id-ID", { const label = new Intl.DateTimeFormat("id-ID", {
month: "long", month: "long",
year: "numeric", year: "numeric",
}).format(dateObj); // hasil: "Juli 2025" }).format(dateObj);
return { month: label, count }; return { month: label, count };
} }
); );
@@ -124,11 +113,18 @@ const FileListComponent = ({
fetchFiles(); fetchFiles();
}, []); }, []);
const formatPhoneNumber = (phone) => useEffect(() => {
phone?.replace(/(\d{4})(\d{4})(\d{4})/, "$1-$2-$3"); if (selectedDocumentType) {
setFilteredFiles(
files.filter((file) => file.document_type === selectedDocumentType)
);
} else {
setFilteredFiles(files);
}
}, [selectedDocumentType, files]);
const handleRowClick = async (file) => { const handleRowClick = async (file) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
if (!token) { if (!token) {
alert("Token tidak ditemukan. Silakan login kembali."); alert("Token tidak ditemukan. Silakan login kembali.");
return; return;
@@ -136,60 +132,51 @@ const FileListComponent = ({
try { try {
const response = await fetch( const response = await fetch(
`https://bot.kediritechnopark.com/webhook/6915ea36-e1f4-49ad-a7f1-a27ce0bf2279/ktp/${file.nik}`, `https://bot.kediritechnopark.com/webhook/solid-data/merged?nama_lengkap=${encodeURIComponent(
file.nama_lengkap
)}`,
{ {
method: "GET", method: "GET",
headers: { headers: {
Authorization: token, // atau `Bearer ${token}` jika diperlukan Authorization: `Bearer ${token}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} }
); );
if (!response.ok) { if (!response.ok)
throw new Error(`HTTP error! Status: ${response.status}`); throw new Error(`HTTP error! Status: ${response.status}`);
}
const text = await response.text(); const text = await response.text();
if (!text) { if (!text) throw new Error("Respons kosong dari server.");
throw new Error("Respons kosong dari server.");
}
const data = JSON.parse(text); const data = JSON.parse(text);
if (data.error) { if (data.error) {
alert(data.error); alert(data.error);
return; return;
} }
const item = data[0]; setSelectedFile(data[0]);
if (!item) {
alert("Data tidak ditemukan.");
return;
}
// Validasi jika ada image URL
if (item.foto_url && !item.foto_url.match(/\.(jpg|jpeg|png|webp)$/i)) {
console.warn(
"URL foto bukan format gambar yang didukung:",
item.foto_url
);
}
setSelectedFile(item); // tampilkan di modal misalnya
} catch (error) { } catch (error) {
console.error("Gagal mengambil detail:", error.message || error); console.error("Gagal mengambil detail:", error.message);
alert("Gagal mengambil detail. Pastikan data tersedia."); alert("Gagal mengambil detail. Pastikan data tersedia.");
} }
}; };
const closeModal = () => { const getImageSrc = (base64) => {
setSelectedFile(null); if (!base64) return null;
const cleaned = base64.replace(/\s/g, "");
if (cleaned.startsWith("iVBOR")) return `data:image/png;base64,${cleaned}`;
if (cleaned.startsWith("/9j/")) return `data:image/jpeg;base64,${cleaned}`;
if (cleaned.startsWith("UklGR")) return `data:image/webp;base64,${cleaned}`;
return `data:image/*;base64,${cleaned}`;
}; };
const exportToExcel = (data) => {
const domain = window.location.origin;
const closeModal = () => setSelectedFile(null);
const formatPhoneNumber = (phone) =>
phone?.replace(/(\d{4})(\d{4})(\d{4})/, "$1-$2-$3");
const exportToExcel = (data) => {
const modifiedData = data.map((item) => ({ const modifiedData = data.map((item) => ({
ID: item.id, ID: item.id,
Petugas_ID: item.petugas_id, Petugas_ID: item.petugas_id,
@@ -200,7 +187,8 @@ const FileListComponent = ({
Tanggal_Lahir: new Date(item.tanggal_lahir), Tanggal_Lahir: new Date(item.tanggal_lahir),
Jenis_Kelamin: item.jenis_kelamin, Jenis_Kelamin: item.jenis_kelamin,
Alamat: item.alamat, Alamat: item.alamat,
RT_RW: item.rt_rw, RT: item.rt,
RW: item.rw,
Kel_Desa: item.kel_desa, Kel_Desa: item.kel_desa,
Kecamatan: item.kecamatan, Kecamatan: item.kecamatan,
Agama: item.agama, Agama: item.agama,
@@ -213,61 +201,40 @@ const FileListComponent = ({
Pembuatan: new Date(item.pembuatan), Pembuatan: new Date(item.pembuatan),
Kota_Pembuatan: item.kota_pembuatan, Kota_Pembuatan: item.kota_pembuatan,
Created_At: new Date(item.created_at), Created_At: new Date(item.created_at),
ImageURL: `${domain}/${item.nik}`,
})); }));
const worksheet = XLSX.utils.json_to_sheet(modifiedData); const worksheet = XLSX.utils.json_to_sheet(modifiedData);
// Add hyperlink to ImageURL column (last column)
modifiedData.forEach((item, index) => {
const cellAddress = `W${index + 2}`; // Column W (ImageURL), starts at row 2
if (worksheet[cellAddress]) {
worksheet[cellAddress].l = {
Target: item.ImageURL,
Tooltip: "Lihat Gambar",
};
}
});
// Optional: Auto column widths (you can fine-tune)
worksheet["!cols"] = new Array(Object.keys(modifiedData[0]).length).fill({
wch: 20,
});
// Add autofilter
worksheet["!autofilter"] = { ref: `A1:W1` }; // Covers all columns (A to W)
// Export
const workbook = XLSX.utils.book_new(); const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Data"); XLSX.utils.book_append_sheet(workbook, worksheet, "Data");
XLSX.writeFile(workbook, "data-export.xlsx"); XLSX.writeFile(workbook, "data-export.xlsx");
}; };
if (loading) {
return (
<div className={styles.fileListSection}>
<div className={styles.emptyState}>
<div className={styles.spinner}></div>
<div className={styles.emptyStateTitle}>Memuat file...</div>
</div>
</div>
);
}
return ( return (
<div className={styles.fileListSection}> <div className={styles.fileListSection}>
<div className={styles.fileListHeader}> <div className={styles.fileListHeader}>
<h2 className={styles.fileListTitle}>📁 Daftar Anggota</h2> <h2 className={styles.fileListTitle}>📁 Daftar Document</h2>
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}> <div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
<select
value={selectedDocumentType}
onChange={(e) => setSelectedDocumentType(e.target.value)}
className={styles.fileCount}
>
<option value="">Semua</option>
<option value="ktp">KTP</option>
<option value="kk">KK</option>
<option value="akta_kelahiran">Akta Kelahiran</option>
</select>
<button <button
onClick={() => { onClick={() => {
exportToExcel(files); exportToExcel(filteredFiles);
}} }}
className={styles.downloadButton} className={styles.downloadButton}
> >
Unduh Excel Unduh Excel
</button> </button>
<span className={styles.fileCount}>{files.length} anggota</span> <span className={styles.fileCount}>
{filteredFiles.length} document
</span>
</div> </div>
</div> </div>
@@ -279,11 +246,11 @@ const FileListComponent = ({
)} )}
<div className={styles.tableContainer}> <div className={styles.tableContainer}>
{files.length === 0 ? ( {filteredFiles.length === 0 ? (
<div className={styles.emptyState}> <div className={styles.emptyState}>
<div className={styles.emptyStateTitle}>Belum ada data</div> <div className={styles.emptyStateTitle}>Belum ada data</div>
<p className={styles.emptyStateText}> <p className={styles.emptyStateText}>
Tidak ada data KTP yang tersedia saat ini. Tidak ada data KK yang tersedia saat ini.
</p> </p>
</div> </div>
) : ( ) : (
@@ -292,13 +259,12 @@ const FileListComponent = ({
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>NIK</th> <th>NIK</th>
<th>Jenis</th>
<th className={styles.nameColumn}>Nama Lengkap</th> <th className={styles.nameColumn}>Nama Lengkap</th>
<th>No. HP</th>
<th>Email</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{files.map((file, index) => ( {filteredFiles.map((file, index) => (
<tr <tr
key={file.id} key={file.id}
onClick={() => handleRowClick(file)} onClick={() => handleRowClick(file)}
@@ -306,27 +272,26 @@ const FileListComponent = ({
> >
<td>{index + 1}</td> <td>{index + 1}</td>
<td>{file.nik}</td> <td>{file.nik}</td>
<td>{file.document_type}</td>
<td className={styles.nameColumn}>{file.nama_lengkap}</td> <td className={styles.nameColumn}>{file.nama_lengkap}</td>
<td>{formatPhoneNumber(file.no_hp)}</td>
<td>{file.email}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
)} )}
</div> </div>
{/* Modal dan komponen lainnya tetap seperti sebelumnya */}
{/* Modal Detail */}
{selectedFile && ( {selectedFile && (
<div className={styles.modalOverlay} onClick={closeModal}> <div className={styles.modalOverlay} onClick={closeModal}>
{" "}
<div <div
className={styles.modalContent} className={styles.modalContent}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Foto KTP */} {" "}
{selectedFile.data && ( {selectedFile.data && (
<img <img
src={`data:image/jpeg;base64,${selectedFile.data}`} src={getImageSrc(selectedFile.data)}
alt={`Foto KTP - ${selectedFile.nik}`} alt={`Foto KTP - ${selectedFile.nik}`}
style={{ style={{
width: "100%", width: "100%",
@@ -337,89 +302,80 @@ const FileListComponent = ({
boxShadow: "0 2px 6px rgba(0,0,0,0.2)", boxShadow: "0 2px 6px rgba(0,0,0,0.2)",
}} }}
/> />
)} )}{" "}
<h3>🪪 Detail Data Document</h3>
<h3>🪪 Detail Data Anggota</h3> <div style={{ marginBottom: "1rem" }}>
<PDFDownloadLink
document={
<KTPPDF
data={{
...selectedFile,
data:
selectedFile.data?.startsWith("/") ||
selectedFile.data?.length < 50
? null
: selectedFile.data.replace(/\s/g, ""),
fallbackImage: selectedFile.foto_url,
}}
/>
}
fileName={`KTP_${selectedFile.nik}.pdf`}
style={{
textDecoration: "none",
padding: "8px 16px",
color: "#fff",
backgroundColor: "#00adef",
borderRadius: "6px",
display: "inline-block",
}}
>
{({ loading }) =>
loading ? "Menyiapkan PDF..." : "⬇️ Unduh PDF"
}
</PDFDownloadLink>
</div>
<table className={styles.detailTable}> <table className={styles.detailTable}>
<tbody> <tbody>
<tr> {[
<td>NIK</td> ["NIK", selectedFile.nik],
<td>{selectedFile.nik}</td> ["No.Al", selectedFile.no_al],
</tr> ["Nomor Akta Kelahiran", selectedFile.akta_kelahiran_nomor],
<tr> ["Nama Lengkap", selectedFile.nama_lengkap],
<td>Nama Lengkap</td> ["Anak Ke", selectedFile.anak_ke],
<td>{selectedFile.nama_lengkap}</td> ["Tempat Lahir", selectedFile.tempat_lahir],
</tr> ["Tanggal Lahir", selectedFile.tanggal_lahir],
<tr> ["Jenis Kelamin", selectedFile.jenis_kelamin],
<td>Tempat Lahir</td> ["Alamat", selectedFile.alamat],
<td>{selectedFile.tempat_lahir}</td> ["Ayah", selectedFile.ayah],
</tr> ["ibu", selectedFile.ibu],
<tr> ["RT", selectedFile.rt],
<td>Tanggal Lahir</td> ["RW", selectedFile.rw],
<td>{selectedFile.tanggal_lahir}</td> ["Kelurahan/Desa", selectedFile.kel_desa],
</tr> ["Kecamatan", selectedFile.kecamatan],
<tr> ["Agama", selectedFile.agama],
<td>Jenis Kelamin</td> ["Status Perkawinan", selectedFile.status_perkawinan],
<td>{selectedFile.jenis_kelamin}</td> ["Pekerjaan", selectedFile.pekerjaan],
</tr> ["Kewarganegaraan", selectedFile.kewarganegaraan],
<tr> ["No HP", selectedFile.no_hp],
<td>Alamat</td> ["Email", selectedFile.email],
<td>{selectedFile.alamat}</td> ["Berlaku Hingga", selectedFile.berlaku_hingga],
</tr> ["Tanggal Pembuatan", selectedFile.pembuatan],
<tr> ["Kota Pembuatan", selectedFile.kota_pembuatan],
<td>RT/RW</td> ]
<td>{selectedFile.rt_rw}</td> .filter(([_, value]) => value !== null && value !== "")
</tr> .map(([label, value]) => (
<tr> <tr key={label}>
<td>Kelurahan/Desa</td> <td>{label}</td>
<td>{selectedFile.kel_desa}</td> <td>{value}</td>
</tr> </tr>
<tr> ))}
<td>Kecamatan</td>
<td>{selectedFile.kecamatan}</td>
</tr>
<tr>
<td>Agama</td>
<td>{selectedFile.agama}</td>
</tr>
<tr>
<td>Status Perkawinan</td>
<td>{selectedFile.status_perkawinan}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>{selectedFile.pekerjaan}</td>
</tr>
<tr>
<td>Kewarganegaraan</td>
<td>{selectedFile.kewarganegaraan}</td>
</tr>
<tr>
<td>No HP</td>
<td>{selectedFile.no_hp}</td>
</tr>
<tr>
<td>Email</td>
<td>{selectedFile.email}</td>
</tr>
<tr>
<td>Berlaku Hingga</td>
<td>{selectedFile.berlaku_hingga}</td>
</tr>
<tr>
<td>Tanggal Pembuatan</td>
<td>{selectedFile.pembuatan}</td>
</tr>
<tr>
<td>Kota Pembuatan</td>
<td>{selectedFile.kota_pembuatan}</td>
</tr>
</tbody> </tbody>
</table> </table>
<button className={styles.closeButton} onClick={closeModal}> <button className={styles.closeButton} onClick={closeModal}>
Tutup {" "}
</button> Tutup{" "}
</div> </button>{" "}
</div>{" "}
</div> </div>
)} )}
</div> </div>

View File

@@ -14,7 +14,7 @@
--neutral-800: #262626; --neutral-800: #262626;
--neutral-900: #171717; --neutral-900: #171717;
--white: #ffffff; --white: #ffffff;
--success-green: #10b981; --success-green: #43a0a7;
--warning-amber: #f59e0b; --warning-amber: #f59e0b;
--error-red: #ef4444; --error-red: #ef4444;
--text-primary: #0f172a; --text-primary: #0f172a;
@@ -72,19 +72,19 @@
} }
.fileCount { .fileCount {
font-size: 0.875rem; font-size: 0.6rem;
color: #ffffff; color: #ffffff;
font-weight: 500; font-weight: 500;
background-color: #ef4444; background-color: #43a0a7;
padding: 0.25rem 0.75rem; padding: 0.25rem 0.75rem;
border-radius: 1rem; border-radius: 1rem;
border: 1px solid var(--border-light); border: 1px solid var(--border-light);
} }
.successMessage { .successMessage {
background-color: rgb(16 185 129 / 0.1); background-color: rgb(67 160 167 / 0.1);
color: var(--success-green); color: var(--success-green);
border: 1px solid rgb(16 185 129 / 0.2); border: 1px solid rgb(67 160 167 / 0.2);
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -116,7 +116,7 @@
} }
.fileTable th { .fileTable th {
background-color: #ef4444; background-color: #43a0a7;
padding: 0.75rem; padding: 0.75rem;
text-align: center; text-align: center;
font-weight: 600; font-weight: 600;
@@ -176,7 +176,7 @@
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
border: 3px solid var(--neutral-300); border: 3px solid var(--neutral-300);
border-top: 3px solid #ef4444; border-top: 3px solid #43a0a7;
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 0 auto 1rem; margin: 0 auto 1rem;
@@ -203,13 +203,13 @@
} }
.tableContainer::-webkit-scrollbar-thumb { .tableContainer::-webkit-scrollbar-thumb {
background: #ef4444; background: #43a0a7;
border-radius: 4px; border-radius: 4px;
transition: background 0.2s ease; transition: background 0.2s ease;
} }
.tableContainer::-webkit-scrollbar-thumb:hover { .tableContainer::-webkit-scrollbar-thumb:hover {
background: #dc2626; background: #306a2f;
} }
.tableContainer::-webkit-scrollbar-corner { .tableContainer::-webkit-scrollbar-corner {
@@ -218,7 +218,7 @@
.tableContainer { .tableContainer {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #ef4444 var(--neutral-100); scrollbar-color: #43a0a7 var(--neutral-100);
} }
/* Modal Styles - Matching Dashboard Design */ /* Modal Styles - Matching Dashboard Design */
@@ -291,7 +291,7 @@
} }
.closeButton { .closeButton {
background-color: #ef4444; background-color: #43a0a7;
color: var(--text-light); color: var(--text-light);
border: none; border: none;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
@@ -399,7 +399,7 @@
} }
.fileCount { .fileCount {
font-size: 0.75rem; font-size: 0.6rem;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
} }
@@ -482,14 +482,14 @@
} }
.downloadButton { .downloadButton {
background-color: #00adef; background-color: #164665;
color: white; color: white;
border: none; border: none;
padding: 6px 12px; padding: 0.25rem 0.5rem;
border-radius: 8px; border-radius: 1rem;
cursor: pointer; cursor: pointer;
font-weight: bold; font-weight: bold;
font-size: 0.9rem; font-size: 0.6rem;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
} }

123
src/KTPPDF.js Normal file
View File

@@ -0,0 +1,123 @@
// components/KTPPDF.js
import React from "react";
import FileListComponent from "./FileListComponent";
import {
Page,
Text,
Image,
Document,
StyleSheet,
View,
} from "@react-pdf/renderer";
const styles = StyleSheet.create({
page: { padding: 30, fontSize: 12 },
section: { marginBottom: 10 },
title: { fontSize: 18, marginBottom: 10 },
image: {
width: 180,
height: 120,
marginBottom: 10,
objectFit: "contain",
border: "1 solid #000",
},
label: { fontWeight: "bold" },
});
const getImageSrc = (base64) => {
if (!base64) return null;
const cleaned = base64.replace(/\s/g, "");
if (cleaned.startsWith("iVBOR")) {
return `data:image/png;base64,${cleaned}`;
} else if (cleaned.startsWith("/9j/")) {
return `data:image/jpeg;base64,${cleaned}`;
} else if (cleaned.startsWith("UklGR")) {
return `data:image/webp;base64,${cleaned}`;
} else {
return `data:image/*;base64,${cleaned}`;
}
};
const KTPPDF = ({ data }) => (
<Document>
<Page size="A4" style={styles.page}>
<Text style={styles.title}>Biodata Anggota</Text>
{data.data ? (
<Image style={styles.image} src={getImageSrc(data.data)} />
) : data.fallbackImage ? (
<Image style={styles.image} src={data.fallbackImage} />
) : (
<Text>Tidak ada foto KTP tersedia</Text>
)}
<View style={styles.section}>
<Text>
<Text style={styles.label}>NIK:</Text> {data.nik}
</Text>
<Text>
<Text style={styles.label}>Nama Lengkap:</Text> {data.nama_lengkap}
</Text>
<Text>
<Text style={styles.label}>Tempat Lahir:</Text>{" "}
{data.tempat_lahir || "-"}
</Text>
<Text>
<Text style={styles.label}>Tanggal Lahir:</Text>{" "}
{data.tanggal_lahir || "-"}
</Text>
<Text>
<Text style={styles.label}>Jenis Kelamin:</Text>{" "}
{data.jenis_kelamin || "-"}
</Text>
<Text>
<Text style={styles.label}>Alamat:</Text> {data.alamat || "-"}
</Text>
<Text>
<Text style={styles.label}>RT/RW:</Text> {data.rt_rw || "-"}
</Text>
<Text>
<Text style={styles.label}>Kel/Desa:</Text> {data.kel_desa || "-"}
</Text>
<Text>
<Text style={styles.label}>Kecamatan:</Text> {data.kecamatan || "-"}
</Text>
<Text>
<Text style={styles.label}>Agama:</Text> {data.agama || "-"}
</Text>
<Text>
<Text style={styles.label}>Status Perkawinan:</Text>{" "}
{data.status_perkawinan || "-"}
</Text>
<Text>
<Text style={styles.label}>Pekerjaan:</Text> {data.pekerjaan || "-"}
</Text>
<Text>
<Text style={styles.label}>Kewarganegaraan:</Text>{" "}
{data.kewarganegaraan || "-"}
</Text>
<Text>
<Text style={styles.label}>No HP:</Text> {data.no_hp || "-"}
</Text>
<Text>
<Text style={styles.label}>Email:</Text> {data.email || "-"}
</Text>
<Text>
<Text style={styles.label}>Berlaku Hingga:</Text>{" "}
{data.berlaku_hingga || "-"}
</Text>
<Text>
<Text style={styles.label}>Tanggal Pembuatan:</Text>{" "}
{data.pembuatan || "-"}
</Text>
<Text>
<Text style={styles.label}>Kota Pembuatan:</Text>{" "}
{data.kota_pembuatan || "-"}
</Text>
</View>
</Page>
</Document>
);
export default KTPPDF;

View File

@@ -1,6 +1,6 @@
.overlay-box { .overlay-box {
position: absolute; position: absolute;
border: 3px dashed red; border: 3px dashed #43a0a7;
width: 80%; /* atau sesuaikan */ width: 80%; /* atau sesuaikan */
aspect-ratio: 85.6 / 53.98; aspect-ratio: 85.6 / 53.98;
top: 50%; top: 50%;

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ const Login = () => {
try { try {
const loginResponse = await fetch( const loginResponse = await fetch(
"https://bot.kediritechnopark.com/webhook/login/psi", "https://bot.kediritechnopark.com/webhook/solid-data/login",
{ {
method: "POST", method: "POST",
headers: { headers: {
@@ -48,8 +48,8 @@ const Login = () => {
return ( return (
<div className={styles.loginContainer}> <div className={styles.loginContainer}>
<div className={styles.loginBox}> <div className={styles.loginBox}>
<img src="/PSI.png" alt="Logo" className={styles.logo} /> <img src="/ikasapta.png" alt="Logo" className={styles.logo} />
<h1 className={styles.h1}>Kawal PSI</h1> <h1 className={styles.h1}>SOLID DATA</h1>
<p className={styles.subtitle}> <p className={styles.subtitle}>
Silakan masuk untuk melanjutkan ke dashboard Silakan masuk untuk melanjutkan ke dashboard
</p> </p>

View File

@@ -28,9 +28,8 @@
font-size: 28px; font-size: 28px;
font-weight: 700; font-weight: 700;
margin-bottom: 10px; margin-bottom: 10px;
color: #ef4444; /* 🔴 Warna merah PSI */ color: #43a0a7;
} }
.subtitle { .subtitle {
font-size: 14px; font-size: 14px;
color: #6b7280; color: #6b7280;
@@ -56,7 +55,7 @@
} }
.button { .button {
background-color: #ef4444; /* 🔴 Warna merah PSI */ background-color: #43a0a7;
color: #ffffff; color: #ffffff;
padding: 12px 24px; padding: 12px 24px;
border-radius: 24px; border-radius: 24px;
@@ -69,7 +68,7 @@
} }
.button:hover { .button:hover {
background-color: #b71c1c; /* versi lebih gelap saat hover */ background-color: #357734; /* darker shade of #43a0a7 */
} }
.error { .error {

View File

@@ -1,14 +1,15 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import styles from "./ProfileTab.module.css"; import dashboardStyles from "./Dashboard.module.css";
import profileStyles from "./ProfileTab.module.css";
const ProfileTab = () => { const ProfileTab = () => {
const menuRef = useRef(null); const menuRef = useRef(null);
const navigate = useNavigate(); const navigate = useNavigate();
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [profile, setProfile] = useState({}); const [user, setUser] = useState({});
const [profileTemp, setProfileTemp] = useState({}); const [userTemp, setUserTemp] = useState({});
useEffect(() => { useEffect(() => {
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
@@ -27,34 +28,62 @@ const ProfileTab = () => {
}; };
useEffect(() => { useEffect(() => {
const dummyProfile = { const verifyTokenAndFetchData = async () => {
username: "admin", const token = localStorage.getItem("token");
if (!token) {
window.location.href = "/login";
return;
}
try {
const response = await fetch(
"https://bot.kediritechnopark.com/webhook/solid-data/dashboard",
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const data = await response.json();
if (!response.ok || !data[0].username) {
throw new Error("Unauthorized");
}
setUser(data[0]);
setUserTemp(data[0]);
} catch (error) {
console.error("Token tidak valid:", error.message);
localStorage.removeItem("token");
window.location.href = "/login";
}
}; };
setProfile(dummyProfile); verifyTokenAndFetchData();
setProfileTemp(dummyProfile);
}, []); }, []);
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
setProfile((prev) => ({ ...prev, [name]: value })); setUser((prev) => ({ ...prev, [name]: value }));
}; };
const handleSave = async () => { const handleSave = async () => {
try { try {
if (!profile.oldPassword || !profile.newPassword) { if (!user.oldPassword || !user.newPassword) {
alert("Password lama dan baru tidak boleh kosong."); alert("Password lama dan baru tidak boleh kosong.");
return; return;
} }
const payload = { const payload = {
username: profile.username, username: user.username,
oldPassword: profile.oldPassword, oldPassword: user.oldPassword,
newPassword: profile.newPassword, newPassword: user.newPassword,
}; };
const response = await fetch( const response = await fetch(
"https://bot.kediritechnopark.com/webhook/reset-password/psi", "https://bot.kediritechnopark.com/webhook/solid-data/reset-password",
{ {
method: "PUT", method: "PUT",
headers: { headers: {
@@ -77,21 +106,26 @@ const ProfileTab = () => {
const handleCancel = () => { const handleCancel = () => {
setIsEditing(false); setIsEditing(false);
setProfile(profileTemp); setUser(userTemp);
}; };
return ( return (
<div className={styles.dashboardContainer}> <div className={dashboardStyles.dashboardContainer}>
<div className={styles.dashboardHeader}> <div className={dashboardStyles.dashboardHeader}>
<div className={styles.logoAndTitle}> <div className={dashboardStyles.logoAndTitle}>
<img src="/PSI.png" alt="Profile Avatar" /> <img src="/ikasapta.png" alt="Bot Avatar" />
<h1 className={styles.h1}>Kawal PSI Profile</h1> <h1 className={dashboardStyles.h1}>SOLID</h1>
<h1 className={dashboardStyles.h1} styles="color: #43a0a7;">
DATA
</h1>
</div> </div>
<div className={styles.dropdownContainer} ref={menuRef}> <div className={dashboardStyles.dropdownContainer} ref={menuRef}>
<button <button
onClick={() => setIsMenuOpen(!isMenuOpen)} onClick={() => setIsMenuOpen(!isMenuOpen)}
className={styles.dropdownToggle} className={dashboardStyles.dropdownToggle}
aria-expanded={isMenuOpen ? "true" : "false"}
aria-haspopup="true"
> >
<svg <svg
width="15" width="15"
@@ -108,24 +142,32 @@ const ProfileTab = () => {
<line x1="3" y1="18" x2="21" y2="18" /> <line x1="3" y1="18" x2="21" y2="18" />
</svg> </svg>
</button> </button>
{isMenuOpen && ( {isMenuOpen && (
<div className={styles.dropdownMenu}> <div className={dashboardStyles.dropdownMenu}>
<button <button
onClick={() => { onClick={() => {
navigate("/dashboard"); navigate("/dashboard");
setIsMenuOpen(false); setIsMenuOpen(false);
}} }}
className={styles.dropdownItem} className={dashboardStyles.dropdownItem}
> >
Dashboard Dashboard
</button> </button>
<button
onClick={() => {
navigate("/scan");
setIsMenuOpen(false);
}}
className={dashboardStyles.dropdownItem}
>
Scan
</button>
<button <button
onClick={() => { onClick={() => {
handleLogout(); handleLogout();
setIsMenuOpen(false); setIsMenuOpen(false);
}} }}
className={styles.dropdownItem} className={dashboardStyles.dropdownItem}
> >
Logout Logout
</button> </button>
@@ -134,43 +176,43 @@ const ProfileTab = () => {
</div> </div>
</div> </div>
<div className={styles.mainContent}> <div className={profileStyles.mainContent}>
<div className={styles.profileSection}> <div className={profileStyles.profileSection}>
<div className={styles.profileCard}> <div className={profileStyles.profileCard}>
<div className={styles.profileHeader}> <div className={profileStyles.profileHeader}>
<h2>Account</h2> <h2>Account</h2>
{!isEditing ? ( {!isEditing ? (
<button <button
onClick={() => setIsEditing(true)} onClick={() => setIsEditing(true)}
className={styles.editButton} className={profileStyles.editButton}
> >
Change Password Change Password
</button> </button>
) : ( ) : (
<div className={styles.actionButtons}> <div className={profileStyles.actionButtons}>
<button <button
onClick={handleCancel} onClick={handleCancel}
className={styles.cancelButton} className={profileStyles.cancelButton}
> >
Cancel Cancel
</button> </button>
<button onClick={handleSave} className={styles.saveButton}> <button onClick={handleSave} className={profileStyles.saveButton}>
Save Changes Save Changes
</button> </button>
</div> </div>
)} )}
</div> </div>
<div className={styles.profileForm}> <div className={profileStyles.profileForm}>
{!isEditing && ( {!isEditing && (
<div className={styles.inputGroup}> <div className={profileStyles.inputGroup}>
<label className={styles.inputLabel}>Username</label> <label className={profileStyles.inputLabel}>Username</label>
<input <input
type="text" type="text"
name="username" name="username"
value={profile.username} value={user.username}
className={`${styles.input} ${ className={`${profileStyles.input} ${
!isEditing ? styles.readOnly : "" !isEditing ? profileStyles.readOnly : ""
}`} }`}
disabled disabled
/> />
@@ -179,25 +221,25 @@ const ProfileTab = () => {
{isEditing && ( {isEditing && (
<> <>
<div className={styles.inputGroup}> <div className={profileStyles.inputGroup}>
<label className={styles.inputLabel}> <label className={profileStyles.inputLabel}>
Current Password Current Password
</label> </label>
<input <input
type="password" type="password"
name="oldPassword" name="oldPassword"
onChange={handleChange} onChange={handleChange}
className={styles.input} className={profileStyles.input}
placeholder="Enter current password" placeholder="Enter current password"
/> />
</div> </div>
<div className={styles.inputGroup}> <div className={profileStyles.inputGroup}>
<label className={styles.inputLabel}>New Password</label> <label className={profileStyles.inputLabel}>New Password</label>
<input <input
type="password" type="password"
name="newPassword" name="newPassword"
onChange={handleChange} onChange={handleChange}
className={styles.input} className={profileStyles.input}
placeholder="Enter new password" placeholder="Enter new password"
/> />
</div> </div>
@@ -208,8 +250,8 @@ const ProfileTab = () => {
</div> </div>
</div> </div>
<div className={styles.footer}> <div className={dashboardStyles.footer}>
© 2025 Kediri Technopark Dermalounge AI Admin © 2025 Kediri Technopark Dashboard SOLID DATA
</div> </div>
</div> </div>
); );

View File

@@ -1,4 +1,4 @@
/* ProfileTab.module.css - Modern Design */ /* ProfileTab.module.css - Modern Design with Unified Header */
/* Modern Color Palette */ /* Modern Color Palette */
:root { :root {
@@ -14,7 +14,7 @@
--neutral-800: #262626; --neutral-800: #262626;
--neutral-900: #171717; --neutral-900: #171717;
--white: #ffffff; --white: #ffffff;
--success-green: #10b981; --success-green: #43a0a7;
--warning-amber: #f59e0b; --warning-amber: #f59e0b;
--error-red: #ef4444; --error-red: #ef4444;
--text-primary: #0f172a; --text-primary: #0f172a;
@@ -50,6 +50,7 @@ body {
flex-direction: column; flex-direction: column;
} }
/* --- UNIFIED HEADER (sama dengan Dashboard.css) --- */
.dashboardHeader { .dashboardHeader {
background-color: var(--white); background-color: var(--white);
color: var(--text-primary); color: var(--text-primary);
@@ -58,7 +59,7 @@ body {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
border-bottom: 3px solid #ef4444; border-bottom: 3px solid #43a0a7; /* Warna dari Dashboard.css */
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 50; z-index: 50;
@@ -80,10 +81,18 @@ body {
} }
.dashboardHeader .h1 { .dashboardHeader .h1 {
margin: 2px; /* Sama dengan Dashboard.css */
font-size: 1.5rem;
font-weight: 700;
color: #43a0a7; /* Warna dari Dashboard.css */
letter-spacing: -0.025em;
}
.data {
margin: 0; margin: 0;
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
color: #ed4344; color: #154666;
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
@@ -96,6 +105,12 @@ body {
flex-shrink: 0; flex-shrink: 0;
} }
.userDisplayName {
color: var(--text-secondary);
font-weight: 500;
font-size: 0.875rem;
}
.dropdownToggle { .dropdownToggle {
background-color: var(--neutral-100); background-color: var(--neutral-100);
color: var(--text-primary); color: var(--text-primary);
@@ -155,7 +170,7 @@ body {
margin-bottom: 0; margin-bottom: 0;
} }
/* Main Content */ /* --- MAIN CONTENT --- */
.mainContent { .mainContent {
flex-grow: 1; flex-grow: 1;
padding: 2rem 1.5rem; padding: 2rem 1.5rem;
@@ -205,7 +220,7 @@ body {
} }
.editButton { .editButton {
background-color: #ef4444; background-color: #43a0a7; /* Diseragamkan dengan warna header */
color: var(--text-light); color: var(--text-light);
border: none; border: none;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
@@ -218,7 +233,7 @@ body {
} }
.editButton:hover { .editButton:hover {
background-color: var(--dark-blue); background-color: #357734;
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }
@@ -328,7 +343,11 @@ body {
} }
.licenseCard { .licenseCard {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); background: linear-gradient(
135deg,
#43a0a7 0%,
#357734 100%
); /* Diseragamkan dengan warna header */
color: var(--text-light); color: var(--text-light);
padding: 1.5rem; padding: 1.5rem;
border-radius: 1rem; border-radius: 1rem;
@@ -421,7 +440,7 @@ body {
border-top: 1px solid var(--border-light); border-top: 1px solid var(--border-light);
} }
/* Responsive Design */ /* --- RESPONSIVE DESIGN --- */
@media (min-width: 768px) { @media (min-width: 768px) {
.dashboardHeader { .dashboardHeader {
padding: 1rem 2rem; padding: 1rem 2rem;
@@ -436,6 +455,10 @@ body {
font-size: 1.75rem; font-size: 1.75rem;
} }
.userDisplayName {
font-size: 0.875rem;
}
.mainContent { .mainContent {
padding: 2.5rem 2rem; padding: 2.5rem 2rem;
gap: 2.5rem; gap: 2.5rem;

View File

@@ -18,7 +18,7 @@ const ShowImage = () => {
try { try {
const response = await fetch( const response = await fetch(
`https://bot.kediritechnopark.com/webhook/0f4420a8-8517-49ba-8ec5-75adde117813/ktp/img/${nik}`, `https://bot.kediritechnopark.com/webhook/ed467164-05c0-4692-bb81-a8f13116bb1b/ktp/img/ikasapta/:nik/${nik}`,
{ {
method: "GET", method: "GET",
headers: { headers: {