diff --git a/package-lock.json b/package-lock.json index 5f68eee..634cf4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "workspace", "version": "0.1.0", "dependencies": { + "@react-pdf/renderer": "^4.3.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@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": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", @@ -3279,6 +3445,14 @@ "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": { "version": "10.4.0", "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==", "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": { "version": "1.3.8", "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", "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": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -4976,6 +5174,14 @@ "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": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -5086,11 +5292,27 @@ "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", "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": { "version": "4.25.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", @@ -5426,6 +5648,14 @@ "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": { "version": "2.1.1", "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", "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": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -5778,6 +6017,11 @@ "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": { "version": "2.0.0", "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", "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": { "version": "1.2.2", "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": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -8694,6 +8959,19 @@ "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -8888,6 +9166,11 @@ "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": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -9674,6 +9957,14 @@ "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": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", @@ -10784,6 +11075,23 @@ "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": { "version": "1.2.4", "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", "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": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -11317,6 +11630,14 @@ "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": { "version": "6.1.0", "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", "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": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -11692,6 +12018,11 @@ "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": { "version": "6.0.1", "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", "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": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -14029,6 +14368,11 @@ "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": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -14675,6 +15019,19 @@ "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": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -15328,6 +15685,11 @@ "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": { "version": "2.0.4", "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", "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": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -16070,6 +16437,15 @@ "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", @@ -16078,6 +16454,20 @@ "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -16260,6 +16650,19 @@ "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": { "version": "1.0.2", "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" } }, + "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": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", diff --git a/package.json b/package.json index 1ee23e0..2aead23 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@react-pdf/renderer": "^4.3.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", diff --git a/public/PSI.png b/public/PSI.png deleted file mode 100644 index f7075fb..0000000 Binary files a/public/PSI.png and /dev/null differ diff --git a/public/asfasf.png b/public/asfasf.png new file mode 100644 index 0000000..afe8d50 Binary files /dev/null and b/public/asfasf.png differ diff --git a/public/ikasapta.png b/public/ikasapta.png new file mode 100644 index 0000000..612c497 Binary files /dev/null and b/public/ikasapta.png differ diff --git a/public/index.html b/public/index.html index 632e247..06545ff 100644 --- a/public/index.html +++ b/public/index.html @@ -2,13 +2,10 @@ - + - + - React App + SOLID DATA diff --git a/public/manifest.json b/public/manifest.json index 080d6c7..1793573 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,19 +1,19 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "IKASAPTA", + "name": "IKASAPTA HUB", "icons": [ { - "src": "favicon.ico", + "src": "ikasapta.png", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { - "src": "logo192.png", + "src": "ikasapta.png", "type": "image/png", "sizes": "192x192" }, { - "src": "logo512.png", + "src": "ikasapta.png", "type": "image/png", "sizes": "512x512" } diff --git a/src/Dashboard.js b/src/Dashboard.js index 6f3845c..319d589 100644 --- a/src/Dashboard.js +++ b/src/Dashboard.js @@ -43,7 +43,7 @@ const Dashboard = () => { try { const response = await fetch( - "https://bot.kediritechnopark.com/webhook/dashboard/psi", + "https://bot.kediritechnopark.com/webhook/solid-data/dashboard", { method: "GET", headers: { @@ -75,7 +75,7 @@ const Dashboard = () => { const token = localStorage.getItem("token"); try { const response = await fetch( - "https://bot.kediritechnopark.com/webhook/list-user/psi", + "https://bot.kediritechnopark.com/webhook/solid-data/list-user", { method: "GET", headers: { @@ -110,7 +110,7 @@ const Dashboard = () => { try { const response = await fetch( - "https://bot.kediritechnopark.com/webhook/add-officer", + "https://bot.kediritechnopark.com/webhook/solid-data/add-officer", { method: "POST", headers: { @@ -163,7 +163,7 @@ const Dashboard = () => { try { const response = await fetch( - `https://bot.kediritechnopark.com/webhook/psi/delete-officer`, + `https://bot.kediritechnopark.com/webhook/solid-data/delete-officer`, { method: "DELETE", headers: { @@ -193,8 +193,11 @@ const Dashboard = () => {
- Bot Avatar -

Kawal PSI Dashboard

+ Bot Avatar +

SOLID

+

+ DATA +

@@ -343,7 +346,7 @@ const Dashboard = () => { )}
-

Grafik Pertumbuhan Anggota

+

Grafik Upload Document

{officerPerformanceData.length > 0 ? ( @@ -355,7 +358,7 @@ const Dashboard = () => { ) : (
- ๐Ÿ“‹ Belum ada data performa untuk ditampilkan + ๐Ÿ“‹ Belum ada data upload untuk ditampilkan
)}
@@ -370,7 +373,7 @@ const Dashboard = () => {
- ยฉ 2025 Kediri Technopark โ€ข Dashboard PSI + ยฉ 2025 Kediri Technopark โ€ข Dashboard SOLID DATA
); diff --git a/src/Dashboard.module.css b/src/Dashboard.module.css index 728a5fd..9a9ea26 100644 --- a/src/Dashboard.module.css +++ b/src/Dashboard.module.css @@ -14,7 +14,7 @@ --neutral-800: #262626; --neutral-900: #171717; --white: #ffffff; - --success-green: #10b981; + --success-green: #43a0a7; --warning-amber: #f59e0b; --error-red: #ef4444; --text-primary: #0f172a; @@ -59,7 +59,7 @@ body { justify-content: space-between; align-items: center; box-shadow: var(--shadow-sm); - border-bottom: 3px solid #ef4444; + border-bottom: 3px solid #43a0a7; position: sticky; top: 0; z-index: 50; @@ -81,10 +81,18 @@ body { } .dashboardHeader .h1 { + margin: 2px; + font-size: 1.5rem; + font-weight: 700; + color: #43a0a7; + letter-spacing: -0.025em; +} + +.data { margin: 0; font-size: 1.5rem; font-weight: 700; - color: #ed4344; + color: #154666; letter-spacing: -0.025em; } @@ -207,7 +215,7 @@ body { .summaryCard p { font-size: 2rem; font-weight: 700; - color: #ef4444; + color: #43a0a7; margin: 0; line-height: 1; } @@ -270,7 +278,7 @@ body { } .submitButton { - background-color: #ef4444; + background-color: #43a0a7; color: var(--text-light); border: none; padding: 0.75rem 1.5rem; @@ -285,7 +293,7 @@ body { } .submitButton:hover { - background-color: #d03b3b; + background-color: #357734; transform: translateY(-1px); box-shadow: var(--shadow-md); } @@ -296,9 +304,9 @@ body { /* Messages */ .success { - background-color: rgb(16 185 129 / 0.1); + background-color: rgb(67 160 167 / 0.1); 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; border-radius: 0.5rem; margin-top: 1rem; @@ -318,9 +326,9 @@ body { } .warning { - background-color: #ef444417; - color: #ef4444; - border: 1px solid #ef444433; + background-color: rgb(67 160 167 / 0.1); + color: #43a0a7; + border: 1px solid rgb(67 160 167 / 0.2); padding: 1rem; border-radius: 0.5rem; margin-top: 1rem; diff --git a/src/FileListComponent.js b/src/FileListComponent.js index cc559cc..168f5e7 100644 --- a/src/FileListComponent.js +++ b/src/FileListComponent.js @@ -1,6 +1,8 @@ import React, { useState, useEffect } from "react"; import styles from "./FileListComponent.module.css"; import * as XLSX from "xlsx"; +import { PDFDownloadLink } from "@react-pdf/renderer"; +import KTPPDF from "./KTPPDF"; const FileListComponent = ({ setTotalFilesSentToday, @@ -9,17 +11,18 @@ const FileListComponent = ({ setOfficerPerformanceData, }) => { const [files, setFiles] = useState([]); + const [filteredFiles, setFilteredFiles] = useState([]); const [loading, setLoading] = useState(true); const [selectedFile, setSelectedFile] = useState(null); const [successMessage, setSuccessMessage] = useState(""); + const [selectedDocumentType, setSelectedDocumentType] = useState(""); useEffect(() => { const fetchFiles = async () => { const token = localStorage.getItem("token"); - try { const response = await fetch( - "https://bot.kediritechnopark.com/webhook/files", + "https://bot.kediritechnopark.com/webhook/solid-data/files", { method: "GET", headers: { @@ -29,35 +32,25 @@ const FileListComponent = ({ } ); - if (!response.ok) { + if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); - } - 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); - - if (!data.success || !Array.isArray(data.data)) { + if (!data.success || !Array.isArray(data.data)) throw new Error("Format respons tidak valid."); - } const fileData = data.data; - - // 1. Set ke state setFiles(fileData); + setFilteredFiles(fileData); - // 2. Hitung total file hari ini const today = new Date().toISOString().slice(0, 10); const totalToday = fileData.filter((f) => f.created_at.startsWith(today) ).length; setTotalFilesSentToday(totalToday); - // 3. Hitung total bulan ini const now = new Date(); const currentMonth = now.getMonth(); const currentYear = now.getFullYear(); @@ -69,10 +62,8 @@ const FileListComponent = ({ }).length; setTotalFilesSentMonth(totalThisMonth); - // 4. Total keseluruhan setTotalFilesSentOverall(fileData.length); - // 5. Grafik performa per bulan (dinamis) const dateObjects = fileData.map((item) => new Date(item.created_at)); if (dateObjects.length > 0) { const minDate = new Date(Math.min(...dateObjects)); @@ -95,19 +86,17 @@ const FileListComponent = ({ const monthKey = `${d.getFullYear()}-${String( d.getMonth() + 1 ).padStart(2, "0")}`; - if (monthlyDataMap[monthKey] !== undefined) { + if (monthlyDataMap[monthKey] !== undefined) monthlyDataMap[monthKey]++; - } }); const performanceArray = Object.entries(monthlyDataMap).map( ([month, count]) => { - const [year, monthNum] = month.split("-"); const dateObj = new Date(`${month}-01`); const label = new Intl.DateTimeFormat("id-ID", { month: "long", year: "numeric", - }).format(dateObj); // hasil: "Juli 2025" + }).format(dateObj); return { month: label, count }; } ); @@ -124,11 +113,18 @@ const FileListComponent = ({ fetchFiles(); }, []); - const formatPhoneNumber = (phone) => - phone?.replace(/(\d{4})(\d{4})(\d{4})/, "$1-$2-$3"); + useEffect(() => { + if (selectedDocumentType) { + setFilteredFiles( + files.filter((file) => file.document_type === selectedDocumentType) + ); + } else { + setFilteredFiles(files); + } + }, [selectedDocumentType, files]); + const handleRowClick = async (file) => { const token = localStorage.getItem("token"); - if (!token) { alert("Token tidak ditemukan. Silakan login kembali."); return; @@ -136,60 +132,51 @@ const FileListComponent = ({ try { 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", headers: { - Authorization: token, // atau `Bearer ${token}` jika diperlukan + Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, } ); - if (!response.ok) { + if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); - } - const text = await response.text(); - if (!text) { - throw new Error("Respons kosong dari server."); - } + if (!text) throw new Error("Respons kosong dari server."); const data = JSON.parse(text); - if (data.error) { alert(data.error); return; } - const item = 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 + setSelectedFile(data[0]); } catch (error) { - console.error("Gagal mengambil detail:", error.message || error); + console.error("Gagal mengambil detail:", error.message); alert("Gagal mengambil detail. Pastikan data tersedia."); } }; - const closeModal = () => { - setSelectedFile(null); + const getImageSrc = (base64) => { + 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) => ({ ID: item.id, Petugas_ID: item.petugas_id, @@ -200,7 +187,8 @@ const FileListComponent = ({ Tanggal_Lahir: new Date(item.tanggal_lahir), Jenis_Kelamin: item.jenis_kelamin, Alamat: item.alamat, - RT_RW: item.rt_rw, + RT: item.rt, + RW: item.rw, Kel_Desa: item.kel_desa, Kecamatan: item.kecamatan, Agama: item.agama, @@ -213,61 +201,40 @@ const FileListComponent = ({ Pembuatan: new Date(item.pembuatan), Kota_Pembuatan: item.kota_pembuatan, Created_At: new Date(item.created_at), - ImageURL: `${domain}/${item.nik}`, })); 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(); XLSX.utils.book_append_sheet(workbook, worksheet, "Data"); XLSX.writeFile(workbook, "data-export.xlsx"); }; - if (loading) { - return ( -
-
-
-
Memuat file...
-
-
- ); - } - return (
-

๐Ÿ“ Daftar Anggota

+

๐Ÿ“ Daftar Document

+ - {files.length} anggota + + {filteredFiles.length} document +
@@ -279,11 +246,11 @@ const FileListComponent = ({ )}
- {files.length === 0 ? ( + {filteredFiles.length === 0 ? (
Belum ada data

- Tidak ada data KTP yang tersedia saat ini. + Tidak ada data KK yang tersedia saat ini.

) : ( @@ -292,13 +259,12 @@ const FileListComponent = ({ ID NIK + Jenis Nama Lengkap - No. HP - Email - {files.map((file, index) => ( + {filteredFiles.map((file, index) => ( handleRowClick(file)} @@ -306,27 +272,26 @@ const FileListComponent = ({ > {index + 1} {file.nik} + {file.document_type} {file.nama_lengkap} - {formatPhoneNumber(file.no_hp)} - {file.email} ))} )}
- - {/* Modal Detail */} + {/* Modal dan komponen lainnya tetap seperti sebelumnya */} {selectedFile && (
+ {" "}
e.stopPropagation()} > - {/* Foto KTP */} + {" "} {selectedFile.data && ( {`Foto - )} - -

๐Ÿชช Detail Data Anggota

+ )}{" "} +

๐Ÿชช Detail Data Document

+
+ + } + 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" + } + +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {[ + ["NIK", selectedFile.nik], + ["No.Al", selectedFile.no_al], + ["Nomor Akta Kelahiran", selectedFile.akta_kelahiran_nomor], + ["Nama Lengkap", selectedFile.nama_lengkap], + ["Anak Ke", selectedFile.anak_ke], + ["Tempat Lahir", selectedFile.tempat_lahir], + ["Tanggal Lahir", selectedFile.tanggal_lahir], + ["Jenis Kelamin", selectedFile.jenis_kelamin], + ["Alamat", selectedFile.alamat], + ["Ayah", selectedFile.ayah], + ["ibu", selectedFile.ibu], + ["RT", selectedFile.rt], + ["RW", selectedFile.rw], + ["Kelurahan/Desa", selectedFile.kel_desa], + ["Kecamatan", selectedFile.kecamatan], + ["Agama", selectedFile.agama], + ["Status Perkawinan", selectedFile.status_perkawinan], + ["Pekerjaan", selectedFile.pekerjaan], + ["Kewarganegaraan", selectedFile.kewarganegaraan], + ["No HP", selectedFile.no_hp], + ["Email", selectedFile.email], + ["Berlaku Hingga", selectedFile.berlaku_hingga], + ["Tanggal Pembuatan", selectedFile.pembuatan], + ["Kota Pembuatan", selectedFile.kota_pembuatan], + ] + .filter(([_, value]) => value !== null && value !== "") + .map(([label, value]) => ( + + + + + ))}
NIK{selectedFile.nik}
Nama Lengkap{selectedFile.nama_lengkap}
Tempat Lahir{selectedFile.tempat_lahir}
Tanggal Lahir{selectedFile.tanggal_lahir}
Jenis Kelamin{selectedFile.jenis_kelamin}
Alamat{selectedFile.alamat}
RT/RW{selectedFile.rt_rw}
Kelurahan/Desa{selectedFile.kel_desa}
Kecamatan{selectedFile.kecamatan}
Agama{selectedFile.agama}
Status Perkawinan{selectedFile.status_perkawinan}
Pekerjaan{selectedFile.pekerjaan}
Kewarganegaraan{selectedFile.kewarganegaraan}
No HP{selectedFile.no_hp}
Email{selectedFile.email}
Berlaku Hingga{selectedFile.berlaku_hingga}
Tanggal Pembuatan{selectedFile.pembuatan}
Kota Pembuatan{selectedFile.kota_pembuatan}
{label}{value}
-
+ {" "} + Tutup{" "} + {" "} +
{" "}
)}
diff --git a/src/FileListComponent.module.css b/src/FileListComponent.module.css index 53ad054..bc0f14e 100644 --- a/src/FileListComponent.module.css +++ b/src/FileListComponent.module.css @@ -14,7 +14,7 @@ --neutral-800: #262626; --neutral-900: #171717; --white: #ffffff; - --success-green: #10b981; + --success-green: #43a0a7; --warning-amber: #f59e0b; --error-red: #ef4444; --text-primary: #0f172a; @@ -72,19 +72,19 @@ } .fileCount { - font-size: 0.875rem; + font-size: 0.6rem; color: #ffffff; font-weight: 500; - background-color: #ef4444; + background-color: #43a0a7; padding: 0.25rem 0.75rem; border-radius: 1rem; border: 1px solid var(--border-light); } .successMessage { - background-color: rgb(16 185 129 / 0.1); + background-color: rgb(67 160 167 / 0.1); 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; border-radius: 0.5rem; margin-bottom: 1rem; @@ -116,7 +116,7 @@ } .fileTable th { - background-color: #ef4444; + background-color: #43a0a7; padding: 0.75rem; text-align: center; font-weight: 600; @@ -176,7 +176,7 @@ width: 2rem; height: 2rem; border: 3px solid var(--neutral-300); - border-top: 3px solid #ef4444; + border-top: 3px solid #43a0a7; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1rem; @@ -203,13 +203,13 @@ } .tableContainer::-webkit-scrollbar-thumb { - background: #ef4444; + background: #43a0a7; border-radius: 4px; transition: background 0.2s ease; } .tableContainer::-webkit-scrollbar-thumb:hover { - background: #dc2626; + background: #306a2f; } .tableContainer::-webkit-scrollbar-corner { @@ -218,7 +218,7 @@ .tableContainer { scrollbar-width: thin; - scrollbar-color: #ef4444 var(--neutral-100); + scrollbar-color: #43a0a7 var(--neutral-100); } /* Modal Styles - Matching Dashboard Design */ @@ -291,7 +291,7 @@ } .closeButton { - background-color: #ef4444; + background-color: #43a0a7; color: var(--text-light); border: none; padding: 0.75rem 1.5rem; @@ -399,7 +399,7 @@ } .fileCount { - font-size: 0.75rem; + font-size: 0.6rem; padding: 0.25rem 0.5rem; } @@ -482,14 +482,14 @@ } .downloadButton { - background-color: #00adef; + background-color: #164665; color: white; border: none; - padding: 6px 12px; - border-radius: 8px; + padding: 0.25rem 0.5rem; + border-radius: 1rem; cursor: pointer; font-weight: bold; - font-size: 0.9rem; + font-size: 0.6rem; transition: background-color 0.3s ease; } diff --git a/src/KTPPDF.js b/src/KTPPDF.js new file mode 100644 index 0000000..bb1954c --- /dev/null +++ b/src/KTPPDF.js @@ -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 }) => ( + + + Biodata Anggota + {data.data ? ( + + ) : data.fallbackImage ? ( + + ) : ( + Tidak ada foto KTP tersedia + )} + + + + NIK: {data.nik} + + + Nama Lengkap: {data.nama_lengkap} + + + Tempat Lahir:{" "} + {data.tempat_lahir || "-"} + + + Tanggal Lahir:{" "} + {data.tanggal_lahir || "-"} + + + Jenis Kelamin:{" "} + {data.jenis_kelamin || "-"} + + + Alamat: {data.alamat || "-"} + + + RT/RW: {data.rt_rw || "-"} + + + Kel/Desa: {data.kel_desa || "-"} + + + Kecamatan: {data.kecamatan || "-"} + + + Agama: {data.agama || "-"} + + + Status Perkawinan:{" "} + {data.status_perkawinan || "-"} + + + Pekerjaan: {data.pekerjaan || "-"} + + + Kewarganegaraan:{" "} + {data.kewarganegaraan || "-"} + + + No HP: {data.no_hp || "-"} + + + Email: {data.email || "-"} + + + Berlaku Hingga:{" "} + {data.berlaku_hingga || "-"} + + + Tanggal Pembuatan:{" "} + {data.pembuatan || "-"} + + + Kota Pembuatan:{" "} + {data.kota_pembuatan || "-"} + + + + +); + +export default KTPPDF; diff --git a/src/KTPScanner.css b/src/KTPScanner.css index 6e56610..98adda8 100644 --- a/src/KTPScanner.css +++ b/src/KTPScanner.css @@ -1,6 +1,6 @@ .overlay-box { position: absolute; - border: 3px dashed red; + border: 3px dashed #43a0a7; width: 80%; /* atau sesuaikan */ aspect-ratio: 85.6 / 53.98; top: 50%; diff --git a/src/KTPScanner.js b/src/KTPScanner.js index defdc53..42adc7d 100644 --- a/src/KTPScanner.js +++ b/src/KTPScanner.js @@ -1,72 +1,342 @@ import React, { useEffect, useRef, useState } from "react"; -import styless from "./Dashboard.module.css"; - -import { useNavigate } from "react-router-dom"; - -import Modal from "./Modal"; -import PaginatedFormEditable from "./PaginatedFormEditable"; - +// import PaginatedFormEditable from "./PaginatedFormEditable"; // Assuming this is provided externally or defined above const STORAGE_KEY = "camera_canvas_gallery"; +// Placeholder for PaginatedFormEditable if not provided by user. +// In a real scenario, this would be a separate file. +const PaginatedFormEditable = ({ data, handleSimpan }) => { + const [editableData, setEditableData] = useState(data); + + useEffect(() => { + setEditableData(data); + }, [data]); + + const handleChange = (key, value) => { + setEditableData((prev) => ({ + ...prev, + [key]: value, + })); + }; + + if (!editableData) return null; + + return ( +
+

Form Data

+ {Object.entries(editableData).map(([key, value]) => ( +
+ + handleChange(key, e.target.value)} + style={paginatedFormEditableStyles.input} + /> +
+ ))} + +
+ ); +}; + +const paginatedFormEditableStyles = { + container: { + backgroundColor: "#f9f9f9", + borderRadius: "12px", + padding: "20px", + marginTop: "20px", + boxShadow: "0 4px 10px rgba(0,0,0,0.05)", + }, + title: { + fontSize: "20px", + fontWeight: "bold", + marginBottom: "15px", + color: "#333", + }, + fieldGroup: { + marginBottom: "15px", + }, + label: { + display: "block", + marginBottom: "5px", + fontWeight: "600", + color: "#555", + }, + input: { + width: "100%", + padding: "10px", + border: "1px solid #ddd", + borderRadius: "8px", + fontSize: "16px", + boxSizing: "border-box", + }, + saveButton: { + backgroundColor: "#429241", + color: "white", + padding: "12px 20px", + borderRadius: "8px", + border: "none", + fontSize: "16px", + fontWeight: "bold", + cursor: "pointer", + marginTop: "15px", + width: "100%", + }, +}; + +// Custom Modal Component for New Document Type +const NewDocumentModal = ({ isOpen, onClose, onSubmit }) => { + const [documentName, setDocumentName] = useState(""); + const [formFields, setFormFields] = useState([ + { id: crypto.randomUUID(), label: "" }, + ]); // State for dynamic form fields + const [isSubmitting, setIsSubmitting] = useState(false); + + // Reset state when modal opens/closes + useEffect(() => { + if (isOpen) { + setDocumentName(""); + setFormFields([{ id: crypto.randomUUID(), label: "" }]); + } + }, [isOpen]); + + const handleAddField = () => { + setFormFields([...formFields, { id: crypto.randomUUID(), label: "" }]); + }; + + const handleRemoveField = (idToRemove) => { + setFormFields(formFields.filter((field) => field.id !== idToRemove)); + }; + + const handleFieldLabelChange = (id, newLabel) => { + setFormFields( + formFields.map((field) => + field.id === id ? { ...field, label: newLabel } : field + ) + ); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!documentName.trim()) return; + + // Ensure all fields have labels + const hasEmptyField = formFields.some((field) => !field.label.trim()); + if (hasEmptyField) { + // Use a custom message box instead of alert + // For this example, I'll just log to console, but in a real app, a modal would appear. + console.log("Please fill all field labels."); + return; + } + + setIsSubmitting(true); + try { + // Pass both documentName and formFields to the onSubmit handler + await onSubmit( + documentName.trim(), + formFields.map((field) => ({ label: field.label.trim() })) + ); + setDocumentName(""); + setFormFields([{ id: crypto.randomUUID(), label: "" }]); + onClose(); + } catch (error) { + console.error("Error submitting new document type:", error); + } finally { + setIsSubmitting(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+

Tambah Jenis Dokumen Baru

+ +
+
+
+ + setDocumentName(e.target.value)} + placeholder="Data yang ingin di tambahkan" + style={modalStyles.input} + disabled={isSubmitting} + required + /> + +

+ Define Fields for this Document Type: +

+ {formFields.map((field, index) => ( +
+ + handleFieldLabelChange(field.id, e.target.value) + } + placeholder={`Field Name ${index + 1}`} + style={modalStyles.fieldInput} + disabled={isSubmitting} + required + /> + {formFields.length > 1 && ( + + )} +
+ ))} + +
+
+ + +
+
+
+
+ ); +}; const CameraCanvas = () => { const [isMenuOpen, setIsMenuOpen] = useState(false); const menuRef = useRef(null); - const navigate = useNavigate(); + // const useNavigate = () => { /* Placeholder if react-router-dom is not available */ return () => {}; }; // Uncomment if react-router-dom is fully set up + // const navigate = useNavigate(); // Uncomment if react-router-dom is fully set up const videoRef = useRef(null); const canvasRef = useRef(null); const hiddenCanvasRef = useRef(null); const [capturedImage, setCapturedImage] = useState(null); - const [galleryImages, setGalleryImages] = useState([]); + const [galleryImages, setGalleryImages] = useState([]); // This state is not used in the current display logic const [fileTemp, setFileTemp] = useState(null); const [isFreeze, setIsFreeze] = useState(false); const freezeFrameRef = useRef(null); - const [modalOpen, setModalOpen] = useState(false); const [loading, setLoading] = useState(false); + const [KTPdetected, setKTPdetected] = useState(false); // Not directly used for KTP anymore, but kept for consistency + const [showDocumentSelection, setShowDocumentSelection] = useState(true); + const [selectedDocumentType, setSelectedDocumentType] = useState(null); + const [cameraInitialized, setCameraInitialized] = useState(false); + const [showNewDocumentModal, setShowNewDocumentModal] = useState(false); - const [KTPdetected, setKTPdetected] = useState(false); + const handleDocumentTypeSelection = (type) => { + if (type === "new") { + setShowNewDocumentModal(true); + } else { + setSelectedDocumentType(type); + setShowDocumentSelection(false); + initializeCamera(); + } + }; const fileInputRef = useRef(null); const triggerFileSelect = () => { fileInputRef.current?.click(); }; - const loadImageToCanvas = (src, width, height) => { - return new Promise((resolve) => { - const img = new Image(); - img.src = src; - img.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext("2d"); - ctx.drawImage(img, 0, 0, width, height); - resolve(canvas); - }; - }); + + // Removed loadImageToCanvas as it's not directly used in the current flow. + + const handleNewDocumentSubmit = async (documentName, fields) => { + try { + const token = localStorage.getItem("token"); // Ensure token is available + + // Kirim ke webhook + const response = await fetch( + "https://bot.kediritechnopark.com/webhook/solid-data/newtype", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + document_type: documentName, + fields: fields, // Include the new dynamic fields + }), + } + ); + + const result = await response.json(); + + if (response.ok && result.status) { + // Simpan ID dokumen ke localStorage + localStorage.setItem("document_id", result.document_id); + + // Set nama document type agar lanjut ke kamera + setSelectedDocumentType( + result.document_type.toLowerCase().replace(/\s+/g, "_") + ); + + setShowDocumentSelection(false); + + // Lanjutkan ke kamera + initializeCamera(); + + console.log("Document ID:", result.document_id); + console.log( + "New Document Type Created:", + result.document_type, + "with fields:", + fields + ); + } else { + throw new Error(result.message || "Gagal membuat document type"); + } + } catch (error) { + console.error("Error submitting new document type:", error); + // Use a custom message box instead of alert + console.log("Gagal membuat dokumen. Coba lagi."); + } }; - // const isImageSimilar = (canvasA, canvasB) => { - // const ctxA = canvasA.getContext("2d"); - // const ctxB = canvasB.getContext("2d"); - - // const imgA = ctxA.getImageData(0, 0, canvasA.width, canvasA.height); - // const imgB = ctxB.getImageData(0, 0, canvasB.width, canvasB.height); - - // const diffPixels = pixelmatch( - // imgA.data, - // imgB.data, - // null, - // canvasA.width, - // canvasA.height, - // { threshold: 0.5 } - // ); - - // const similarity = diffPixels / (canvasA.width * canvasA.height); - // return similarity < 0.2; // you can adjust the threshold - // }; - const rectRef = useRef({ x: 0, y: 0, @@ -112,86 +382,134 @@ const CameraCanvas = () => { ctx.restore(); }; + const initializeCamera = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: { ideal: "environment" } }, + audio: false, + }); + + if (videoRef.current) { + videoRef.current.srcObject = stream; + + videoRef.current.onloadedmetadata = () => { + videoRef.current.play(); + const video = videoRef.current; + const canvas = canvasRef.current; + const hiddenCanvas = hiddenCanvasRef.current; + const ctx = canvas.getContext("2d"); + + // Set canvas dimensions to match video stream + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + canvas.style.maxWidth = "100%"; + canvas.style.height = "auto"; // Maintain aspect ratio + + hiddenCanvas.width = video.videoWidth; + hiddenCanvas.height = video.videoHeight; + + // Calculate rectangle dimensions based on a common document aspect ratio (e.g., ID card) + const rectWidth = canvas.width * 0.9; + const rectHeight = (53.98 / 85.6) * rectWidth; // Standard ID card aspect ratio + const rectX = (canvas.width - rectWidth) / 2; + const rectY = (canvas.height - rectHeight) / 2; + + rectRef.current = { + x: rectX, + y: rectY, + width: rectWidth, + height: rectHeight, + radius: 20, + }; + + const drawToCanvas = () => { + if (video.readyState === 4) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (isFreeze && freezeFrameRef.current) { + ctx.putImageData(freezeFrameRef.current, 0, 0); + } else { + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + } + drawRoundedRect( + ctx, + rectRef.current.x, + rectRef.current.y, + rectRef.current.width, + rectRef.current.height, + rectRef.current.radius + ); + if (isFreeze) { + fillOutsideRect( + ctx, + rectRef.current, + canvas.width, + canvas.height + ); + } + } + // Continue drawing only if document selection is not active + if (!showDocumentSelection) { + requestAnimationFrame(drawToCanvas); + } + }; + + drawToCanvas(); + setCameraInitialized(true); + }; + } + } catch (err) { + console.error("Gagal mendapatkan kamera:", err); + // Handle camera access denied or not available + // For example, show a message to the user or offer manual upload only. + } + }; + useEffect(() => { + // This effect is for loading gallery images, which isn't directly used + // in the current display but kept for consistency with original code. const savedGallery = localStorage.getItem(STORAGE_KEY); if (savedGallery) setGalleryImages(JSON.parse(savedGallery)); + }, []); - const getCameraStream = async () => { - try { - const stream = await navigator.mediaDevices.getUserMedia({ - video: { facingMode: { ideal: "environment" } }, - audio: false, - }); + // Modified useEffect to only run when isFreeze changes and camera is initialized + useEffect(() => { + if (cameraInitialized) { + const video = videoRef.current; + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); - if (videoRef.current) { - videoRef.current.srcObject = stream; - - videoRef.current.onloadedmetadata = () => { - videoRef.current.play(); - const video = videoRef.current; - const canvas = canvasRef.current; - const hiddenCanvas = hiddenCanvasRef.current; - const ctx = canvas.getContext("2d"); - - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - canvas.style.maxWidth = "100%"; - canvas.style.height = "auto"; - - hiddenCanvas.width = video.videoWidth; - hiddenCanvas.height = video.videoHeight; - - const rectWidth = canvas.width * 0.9; - const rectHeight = (53.98 / 85.6) * rectWidth; - const rectX = (canvas.width - rectWidth) / 2; - const rectY = (canvas.height - rectHeight) / 2; - - rectRef.current = { - x: rectX, - y: rectY, - width: rectWidth, - height: rectHeight, - radius: 20, - }; - - const drawToCanvas = () => { - if (video.readyState === 4) { - ctx.clearRect(0, 0, canvas.width, canvas.height); - if (isFreeze && freezeFrameRef.current) { - ctx.putImageData(freezeFrameRef.current, 0, 0); - } else { - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - } - drawRoundedRect( - ctx, - rectRef.current.x, - rectRef.current.y, - rectRef.current.width, - rectRef.current.height, - rectRef.current.radius - ); - if (isFreeze) { - fillOutsideRect( - ctx, - rectRef.current, - canvas.width, - canvas.height - ); - } - } - requestAnimationFrame(drawToCanvas); - }; - - drawToCanvas(); - }; + const drawToCanvas = () => { + if (video && video.readyState === 4) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (isFreeze && freezeFrameRef.current) { + ctx.putImageData(freezeFrameRef.current, 0, 0); + } else { + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + } + drawRoundedRect( + ctx, + rectRef.current.x, + rectRef.current.y, + rectRef.current.width, + rectRef.current.height, + rectRef.current.radius + ); + if (isFreeze) { + fillOutsideRect(ctx, rectRef.current, canvas.width, canvas.height); + } } - } catch (err) { - console.error("Gagal mendapatkan kamera:", err); - } - }; + if (!showDocumentSelection) { + requestAnimationFrame(drawToCanvas); + } + }; - getCameraStream(); - }, [isFreeze]); + // Start drawing loop whenever isFreeze or cameraInitialized changes + // and document selection is not active. + if (!showDocumentSelection) { + drawToCanvas(); + } + } + }, [isFreeze, cameraInitialized, showDocumentSelection]); const shootImage = async () => { const video = videoRef.current; @@ -200,6 +518,7 @@ const CameraCanvas = () => { const hiddenCtx = hiddenCanvas.getContext("2d"); const visibleCtx = canvasRef.current.getContext("2d"); + // Capture the current frame from the visible canvas to freeze it freezeFrameRef.current = visibleCtx.getImageData( 0, 0, @@ -209,13 +528,16 @@ const CameraCanvas = () => { setIsFreeze(true); setLoading(true); + // Draw the video frame onto the hidden canvas for cropping hiddenCtx.drawImage(video, 0, 0, hiddenCanvas.width, hiddenCanvas.height); + // Create a new canvas for the cropped image const cropCanvas = document.createElement("canvas"); cropCanvas.width = Math.floor(width); cropCanvas.height = Math.floor(height); const cropCtx = cropCanvas.getContext("2d"); + // Draw the cropped portion from the hidden canvas to the crop canvas cropCtx.drawImage( hiddenCanvas, Math.floor(x), @@ -231,9 +553,9 @@ const CameraCanvas = () => { const imageDataUrl = cropCanvas.toDataURL("image/png", 1.0); setCapturedImage(imageDataUrl); - setKTPdetected(true); + setKTPdetected(true); // This variable name might be misleading now, but kept for consistency setLoading(false); - // Continue to OCR etc... + // Continue to OCR etc... (handled by ReadImage) }; function base64ToFile(base64Data, fileName) { @@ -251,7 +573,7 @@ const CameraCanvas = () => { const ReadImage = async (capturedImage) => { try { setLoading(true); - const token = localStorage.getItem("token"); + const token = localStorage.getItem("token"); // Ensure token is available // Ubah base64 ke file const file = base64ToFile(capturedImage, "image.jpg"); @@ -259,13 +581,14 @@ const CameraCanvas = () => { // Gunakan FormData const formData = new FormData(); formData.append("image", file); + formData.append("document_type", selectedDocumentType); // Add document type to form data const res = await fetch( - "https://bot.kediritechnopark.com/webhook/mastersnapper/read", + "https://bot.kediritechnopark.com/webhook/solid-data/scan", { method: "POST", headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${token}`, // No Content-Type for FormData, browser sets it }, body: formData, } @@ -275,64 +598,98 @@ const CameraCanvas = () => { const data = await res.json(); if (data.responseCode == 409) { - console.log(409); + console.log("Error 409: Document already registered."); setFileTemp({ error: 409 }); return; } - console.log(data); + console.log("Scan Result:", data); setFileTemp(data); } catch (error) { console.error("Failed to read image:", error); + // Handle error, e.g., show a message to the user } }; - const handleSaveTemp = async (verifiedData) => { + const handleSaveTemp = async (verifiedData, documentType) => { try { setLoading(true); - const token = localStorage.getItem("token"); + const token = localStorage.getItem("token"); // Ensure token is available const formData = new FormData(); - - // Tambahkan data terverifikasi sebagai JSON string formData.append("data", JSON.stringify(verifiedData)); + if (!documentType) { + console.error("โŒ documentType undefined! Cannot save."); + setLoading(false); + return; + } + + console.log("โœ… Saving data for documentType:", documentType); + formData.append("document_type", documentType); + const res = await fetch( - "https://bot.kediritechnopark.com/webhook/mastersnapper/save", + "https://bot.kediritechnopark.com/webhook/solid-data/save", { method: "POST", headers: { Authorization: `Bearer ${token}`, - // Jangan set Content-Type secara manual untuk FormData + // Content-Type is set by browser for FormData }, body: formData, } ); setLoading(false); - setFileTemp(null); + const result = await res.json(); + console.log("Save Result:", result); + if (res.ok && result.status) { + // Successfully saved, clear temp data and reset camera + setFileTemp(null); + setIsFreeze(false); + setCapturedImage(null); + setKTPdetected(false); + // Optionally, go back to document selection or re-initialize camera for new scan + goBackToSelection(); + } else { + console.error( + "Failed to save data:", + result.message || "Unknown error" + ); + // Show error message to user + } } catch (err) { console.error("Gagal menyimpan ke server:", err); + // Handle error, e.g., show a message to the user } }; const handleDeleteTemp = async () => { + // This function seems to be for deleting temporary data on the server. + // The current implementation is commented out and sends `fileTemp` as body. + // If this is meant to delete a specific temporary scan result, it needs + // an ID or identifier from `fileTemp`. try { - await fetch( - "https://bot.kediritechnopark.com/webhook/mastersnapper/delete", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ fileTemp }), - } - ); - + // Example of how it might be implemented if an ID was available: + // const tempScanId = fileTemp?.id; // Assuming fileTemp has an ID + // if (tempScanId) { + // await fetch( + // `https://bot.kediritechnopark.com/webhook/mastersnapper/delete/${tempScanId}`, + // { + // method: "DELETE", // Or POST with a specific action + // headers: { "Content-Type": "application/json" }, + // } + // ); + // } setFileTemp(null); + setIsFreeze(false); + setCapturedImage(null); } catch (err) { console.error("Gagal menghapus dari server:", err); } }; + // `removeImage` is for galleryImages, which is not currently displayed. const removeImage = (index) => { const newGallery = [...galleryImages]; newGallery.splice(index, 1); @@ -340,7 +697,8 @@ const CameraCanvas = () => { localStorage.setItem(STORAGE_KEY, JSON.stringify(newGallery)); }; - const aspectRatio = 53.98 / 85.6; + // `aspectRatio` is used in `initializeCamera` to calculate rectHeight. + // const aspectRatio = 53.98 / 85.6; // Already used implicitly in initializeCamera const handleManualUpload = async (e) => { const file = e.target.files[0]; @@ -350,7 +708,7 @@ const CameraCanvas = () => { reader.onloadend = () => { const imageDataUrl = reader.result; setCapturedImage(imageDataUrl); - setIsFreeze(true); + setIsFreeze(true); // Freeze the display with the uploaded image const image = new Image(); image.onload = async () => { @@ -359,14 +717,13 @@ const CameraCanvas = () => { const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); + // Clear canvas and draw the uploaded image, respecting the crop area ctx.clearRect(0, 0, canvas.width, canvas.height); - if (isFreeze && freezeFrameRef.current) { - ctx.putImageData(freezeFrameRef.current, 0, 0); - } else { - const video = videoRef.current; - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - } + // Draw the image to the entire canvas first + ctx.drawImage(image, 0, 0, canvas.width, canvas.height); + + // Then draw the rounded rectangle outline drawRoundedRect( ctx, rectRef.current.x, @@ -376,90 +733,10 @@ const CameraCanvas = () => { rectRef.current.radius ); - ctx.save(); - ctx.beginPath(); - ctx.moveTo( - rectRef.current.x + rectRef.current.radius, - rectRef.current.y - ); - ctx.lineTo( - rectRef.current.x + rectRef.current.width - rectRef.current.radius, - rectRef.current.y - ); - ctx.quadraticCurveTo( - rectRef.current.x + rectRef.current.width, - rectRef.current.y, - rectRef.current.x + rectRef.current.width, - rectRef.current.y + rectRef.current.radius - ); - ctx.lineTo( - rectRef.current.x + rectRef.current.width, - rectRef.current.y + rectRef.current.height - rectRef.current.radius - ); - ctx.quadraticCurveTo( - rectRef.current.x + rectRef.current.width, - rectRef.current.y + rectRef.current.height, - rectRef.current.x + rectRef.current.width - rectRef.current.radius, - rectRef.current.y + rectRef.current.height - ); - ctx.lineTo( - rectRef.current.x + rectRef.current.radius, - rectRef.current.y + rectRef.current.height - ); - ctx.quadraticCurveTo( - rectRef.current.x, - rectRef.current.y + rectRef.current.height, - rectRef.current.x, - rectRef.current.y + rectRef.current.height - rectRef.current.radius - ); - ctx.lineTo( - rectRef.current.x, - rectRef.current.y + rectRef.current.radius - ); - ctx.quadraticCurveTo( - rectRef.current.x, - rectRef.current.y, - rectRef.current.x + rectRef.current.radius, - rectRef.current.y - ); - ctx.closePath(); - ctx.clip(); - - // === Object-Fit: Cover Logic === - const imageAspectRatio = image.width / image.height; - const rectAspectRatio = rectWidth / rectHeight; - - let sx, sy, sWidth, sHeight; - - if (imageAspectRatio > rectAspectRatio) { - // Image is wider than rect - sHeight = image.height; - sWidth = sHeight * rectAspectRatio; - sx = (image.width - sWidth) / 2; - sy = 0; - } else { - // Image is taller than rect - sWidth = image.width; - sHeight = sWidth / rectAspectRatio; - sx = 0; - sy = (image.height - sHeight) / 2; - } - - ctx.drawImage( - image, - sx, - sy, - sWidth, - sHeight, - rectRef.current.x, - rectRef.current.y, - rectWidth, - rectHeight - ); - // ============================== - - ctx.restore(); + // Fill outside the rectangle to create the masking effect + fillOutsideRect(ctx, rectRef.current, canvas.width, canvas.height); + // Store this state as the freeze frame freezeFrameRef.current = ctx.getImageData( 0, 0, @@ -467,42 +744,103 @@ const CameraCanvas = () => { canvas.height ); + // Create a separate canvas for the actual cropped image data const cropCanvas = document.createElement("canvas"); cropCanvas.width = rectWidth; cropCanvas.height = rectHeight; const cropCtx = cropCanvas.getContext("2d"); + // Draw the cropped portion from the main canvas to the crop canvas cropCtx.drawImage( - canvas, + canvas, // Source canvas rectRef.current.x, rectRef.current.y, rectWidth, rectHeight, - 0, - 0, + 0, // Destination x + 0, // Destination y rectWidth, rectHeight ); - setKTPdetected(true); + setKTPdetected(true); // Indicate that an image is ready for scan }; image.src = imageDataUrl; }; reader.readAsDataURL(file); }; + const goBackToSelection = () => { + setShowDocumentSelection(true); + setSelectedDocumentType(null); + setCameraInitialized(false); + setIsFreeze(false); + setCapturedImage(null); + setFileTemp(null); + setKTPdetected(false); + + // Stop camera stream + if (videoRef.current && videoRef.current.srcObject) { + const stream = videoRef.current.srcObject; + const tracks = stream.getTracks(); + tracks.forEach((track) => track.stop()); + videoRef.current.srcObject = null; + } + }; + + // Function to get document display info + const getDocumentDisplayInfo = (docType) => { + switch (docType) { + case "ktp": + return { icon: "๐Ÿ†”", name: "KTP", fullName: "Kartu Tanda Penduduk" }; + case "kk": + return { icon: "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", name: "KK", fullName: "Kartu Keluarga" }; + case "akta_kelahiran": + return { + icon: "๐Ÿ‘ถ", + name: "Akta Kelahiran", + fullName: "Akta Kelahiran", + }; + case "new": // For the "new" option itself + return { icon: "โœจ", name: "New Document", fullName: "Dokumen Baru" }; + default: + // For dynamically added document types, use the name itself + return { + icon: "๐Ÿ“„", + name: docType, + fullName: docType + .replace(/_/g, " ") + .replace(/\b\w/g, (l) => l.toUpperCase()), + }; + } + }; + + // Placeholder for `useNavigate` if `react-router-dom` is not used in this environment. + // If `react-router-dom` is available, uncomment the original `useNavigate`. + const navigate = (path) => { + console.log(`Navigating to: ${path}`); + // In a real browser environment with react-router-dom, this would be: + // useNavigate()(path); + }; + return (
-
-
- Bot Avatar -

Kawal PSI Dashboard

+
+
+ {/* Placeholder for image, ensure it's accessible */} + Bot Avatar +

SOLID

+

DATA

-
+
{isMenuOpen && ( -
+
@@ -537,7 +875,7 @@ const CameraCanvas = () => { navigate("/dashboard"); setIsMenuOpen(false); }} - className={styless.dropdownItem} + style={styles.dropdownItem} > Dashboard @@ -545,125 +883,240 @@ const CameraCanvas = () => { )}
-