diff --git a/package-lock.json b/package-lock.json index ec928a8535f141781db634ab1be55599b1549b81..92863e93075a05cf1617a82efcac2148f9495f7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1780,6 +1780,12 @@ "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==" }, + "@types/raf": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz", + "integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==", + "optional": true + }, "@types/react": { "version": "16.9.34", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.34.tgz", @@ -3148,6 +3154,12 @@ } } }, + "base64-arraybuffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", + "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", + "optional": true + }, "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", @@ -3392,6 +3404,11 @@ "node-int64": "^0.4.0" } }, + "btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" + }, "buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -3557,6 +3574,32 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001043.tgz", "integrity": "sha512-MrBDRPJPDBYwACtSQvxg9+fkna5jPXhJlKmuxenl/ml9uf8LHKlDmLpElu+zTW/bEz7lC1m0wTDD7jiIB+hgFg==" }, + "canvg": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.7.tgz", + "integrity": "sha512-4sq6iL5Q4VOXS3PL1BapiXIZItpxYyANVzsAKpTPS5oq4u3SKbGfUcbZh2gdLCQ3jWpG/y5wRkMlBBAJhXeiZA==", + "optional": true, + "requires": { + "@babel/runtime-corejs3": "^7.9.6", + "@types/raf": "^3.4.0", + "raf": "^3.4.1", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^5.0.5" + }, + "dependencies": { + "@babel/runtime-corejs3": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.13.10.tgz", + "integrity": "sha512-x/XYVQ1h684pp1mJwOV4CyvqZXqbc8CMsMGUnAbuc82ZCdv1U63w5RSUzgDSXQHG5Rps/kiksH6g2D5BuaKyXg==", + "optional": true, + "requires": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.4" + } + } + } + }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -4241,6 +4284,15 @@ } } }, + "css-line-break": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz", + "integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==", + "optional": true, + "requires": { + "base64-arraybuffer": "^0.2.0" + } + }, "css-loader": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz", @@ -4883,6 +4935,12 @@ "domelementtype": "1" } }, + "dompurify": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.7.tgz", + "integrity": "sha512-jdtDffdGNY+C76jvodNTu9jt5yYj59vuTUyx+wXdzcSwAGTYZDAQkQ7Iwx9zcGrA4ixC1syU4H3RZROqRxokxg==", + "optional": true + }, "domutils": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", @@ -6170,6 +6228,11 @@ "bser": "2.1.1" } }, + "fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" + }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -6998,6 +7061,15 @@ } } }, + "html2canvas": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz", + "integrity": "sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==", + "optional": true, + "requires": { + "css-line-break": "1.1.1" + } + }, "htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", @@ -8815,6 +8887,20 @@ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" }, + "jspdf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.3.1.tgz", + "integrity": "sha512-1vp0USP1mQi1h7NKpwxjFgQkJ5ncZvtH858aLpycUc/M+r/RpWJT8PixAU7Cw/3fPd4fpC8eB/Bj42LnsR21YQ==", + "requires": { + "atob": "^2.1.2", + "btoa": "^1.2.1", + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.2.0", + "fflate": "^0.4.8", + "html2canvas": "^1.0.0-rc.5" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -13538,6 +13624,12 @@ "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" }, + "rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha1-1lBezbMEplldom+ktDMHMGd1lF0=", + "optional": true + }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -14338,6 +14430,12 @@ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==" }, + "stackblur-canvas": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz", + "integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==", + "optional": true + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -14724,6 +14822,12 @@ "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" }, + "svg-pathdata": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-5.0.5.tgz", + "integrity": "sha512-TAAvLNSE3fEhyl/Da19JWfMAdhSXTYeviXsLSoDT1UM76ADj5ndwAPX1FKQEgB/gFMPavOy6tOqfalXKUiXrow==", + "optional": true + }, "svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", diff --git a/package.json b/package.json index 2f2f44d5f4b7acbfe4e5236ae50f870078a40fed..8fc7f4fce96a94000b2cc6952e4ea6bfb20f338e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "crypto-js": "^4.0.0", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", + "jspdf": "^2.3.1", "react": "^16.12.0", "react-date-picker": "^8.0.1", "react-dom": "^16.12.0", diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index d67936d6a53225b8be83bd677ef11e461aa3edc6..ff263af897cd50cce37ad09a7d032da108fbc6dc 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -16,6 +16,7 @@ interface TableProps { id?: string; header: Array; data?: Array>; + setData?: any; onChange?: (value: string, pageNumber: number) => Promise>>; searchPlaceholder?: string; maximumData?: number; @@ -86,12 +87,12 @@ export default function Table({ header, data = [], searchPlaceholder = 'Search something...', + setData = () => {}, onChange = async () => new Promise>>(() => []), maximumData = -1, rowOnClick, }: TableProps) { const { colors } = useContext(ThemeContext) || DEFAULT_THEME; - const [innerData, setInnerData] = useState(data); const [previousDataTotal, setPreviousDataTotal] = useState(0); const [isLoading, setIsLoading] = useState(false); const [pageNumber, setPageNumber] = useState(1); @@ -110,45 +111,46 @@ export default function Table({ const newPageNumber = pageNumber + sign; if ( maximumData > 0 && - previousDataTotal + ((1 + sign) / 2) * innerData.length >= maximumData + previousDataTotal + ((1 + sign) / 2) * data.length >= maximumData ) return; - setIsLoading(true) + setIsLoading(true); const newData: Array> = await onChange( searchValue, newPageNumber ); - setIsLoading(false) + setIsLoading(false); if (newData.length === 0 || newData[0].length === 0) return; setPageNumber(newPageNumber); - setPreviousDataTotal(previousDataTotal + (sign === 1? innerData.length: -newData.length)); - setInnerData([...newData]); + setPreviousDataTotal(previousDataTotal + (sign === 1? data.length: -newData.length)); + setData([...newData]); }; const [couldGoBack, couldGoNext] = [ pageNumber > 1, - maximumData <= 0 || previousDataTotal + innerData.length < maximumData, + maximumData <= 0 || previousDataTotal + data.length < maximumData, ]; useEffect(() => { - setInnerData(data); + setData(data); }, [JSON.stringify(data)]); useEffect(() => { const initializeData = async () => { - setIsLoading(true) - const data = await onChange(searchValue, pageNumber) + console.log("Initialize data"); + setIsLoading(true); + const data = await onChange(searchValue, pageNumber); if (data) { - setInnerData(data) + setData(data); } - setIsLoading(false) - } - initializeData() - }, []) + setIsLoading(false); + }; + initializeData(); + }, []); - if (innerData[0] && (header.length > innerData[0].length)) { + if (data[0] && (header.length > data[0].length)) { throw new Error('The shape of Header and data are not consistent'); } @@ -171,14 +173,14 @@ export default function Table({ data-test-id="search-button" onClick={async () => { const newPageNumber = 1; - setIsLoading(true) + setIsLoading(true); const newData = await onChange(searchValue, newPageNumber); - setIsLoading(false) + setIsLoading(false); if (newData.length === 0 || newData[0].length === 0) return; setPageNumber(newPageNumber); setPreviousDataTotal(0); - setInnerData([...newData]); + setData([...newData]); }} > Cari @@ -220,12 +222,12 @@ export default function Table({ {isLoading? ( - + ) : ( - {innerData.map((row: Array, index: number) => { + {data.map((row: Array, index: number) => { return ( )} - {!isLoading && innerData.length === 0 + {!isLoading && data.length === 0 ? ( - {`${previousDataTotal + innerData.length}${ + {`${previousDataTotal + data.length}${ maximumData > 0 ? ` dari ${maximumData}` : '' }`} diff --git a/src/constant/index.ts b/src/constant/index.ts index 91c2776e0a6623364f1621a84243f50245ca2e4e..358a2ad7bb311efd58942e8ff084b85cac56889e 100644 --- a/src/constant/index.ts +++ b/src/constant/index.ts @@ -1,83 +1,105 @@ const KECAMATAN_VALUES: {label: string; value: string}[] = [ - {label: 'Beji', value: 'Beji'}, - {label: 'Bojongsari', value: 'Bojongsari'}, - {label: 'Cilodong', value: 'Cilodong'}, - {label: 'Cimanggis', value: 'Cimanggis'}, - {label: 'Cinere', value: 'Cinere'}, - {label: 'Cipayung', value: 'Cipayung'}, - {label: 'Limo', value: 'Limo'}, - {label: 'Pancoran Mas', value: 'Pancoran Mas'}, - {label: 'Sawangan', value: 'Sawangan'}, - {label: 'Sukmajaya', value: 'Sukmajaya'}, - {label: 'Tapos', value: 'Tapos'}, -]; - -const KELURAHAN_VALUES: {label: string; value: string}[] = [ { label: 'Beji', value: 'Beji' }, - { label: 'Beji Timur', value: 'Beji Timur' }, - { label: 'Kemirimuka', value: 'Kemirimuka' }, - { label: 'Kukusan', value: 'Kukusan' }, - { label: 'Pondok Cina', value: 'Pondok Cina' }, - { label: 'Tanah Baru', value: 'Tanah Baru' }, - { label: 'Bojongsari Baru', value: 'Bojongsari Baru' }, - { label: 'Bojongsari Lama', value: 'Bojongsari Lama' }, - { label: 'Curug', value: 'Curug' }, - { label: 'Duren Mekar', value: 'Duren Mekar' }, - { label: 'Duren Seribu', value: 'Duren Seribu' }, - { label: 'Pondok Petir', value: 'Pondok Petir' }, - { label: 'Serua', value: 'Serua' }, + { label: 'Bojongsari', value: 'Bojongsari' }, { label: 'Cilodong', value: 'Cilodong' }, - { label: 'Jatimulya', value: 'Jatimulya' }, - { label: 'Kalibaru', value: 'Kalibaru' }, - { label: 'Kalimulya', value: 'Kalimulya' }, - { label: 'Sukamaju', value: 'Sukamaju' }, - { label: 'Cisalak Pasar', value: 'Cisalak Pasar' }, - { label: 'Curug', value: 'Curug' }, - { label: 'Harjamukti', value: 'Harjamukti' }, - { label: 'Mekarsari', value: 'Mekarsari' }, - { label: 'Pasir Gunung Selatan', value: 'Pasir Gunung Selatan' }, - { label: 'Tugu', value: 'Tugu' }, + { label: 'Cimanggis', value: 'Cimanggis' }, { label: 'Cinere', value: 'Cinere' }, - { label: 'Gandul', value: 'Gandul' }, - { label: 'Pangkalan Jati', value: 'Pangkalan Jati' }, - { label: 'Pangkalan Jati Baru', value: 'Pangkalan Jati Baru' }, - { label: 'Bojong Pondok Terong', value: 'Bojong Pondok Terong' }, { label: 'Cipayung', value: 'Cipayung' }, - { label: 'Cipayung Jaya', value: 'Cipayung Jaya' }, - { label: 'Pondok Jaya', value: 'Pondok Jaya' }, - { label: 'Ratujaya', value: 'Ratujaya' }, - { label: 'Grogol', value: 'Grogol' }, - { label: 'Krukut', value: 'Krukut' }, { label: 'Limo', value: 'Limo' }, - { label: 'Meruyung', value: 'Meruyung' }, - { label: 'Depok', value: 'Depok' }, - { label: 'Depok Jaya', value: 'Depok Jaya' }, - { label: 'Mampang', value: 'Mampang' }, { label: 'Pancoran Mas', value: 'Pancoran Mas' }, - { label: 'Rangkapan Jaya', value: 'Rangkapan Jaya' }, - { label: 'Rangkapan Jaya Baru', value: 'Rangkapan Jaya Baru' }, - { label: 'Bedahan', value: 'Bedahan' }, - { label: 'Cinangka', value: 'Cinangka' }, - { label: 'Kedaung', value: 'Kedaung' }, - { label: 'Pasir Putih', value: 'Pasir Putih' }, - { label: 'Pengasinan', value: 'Pengasinan' }, - { label: 'Sawangan Baru', value: 'Sawangan Baru' }, - { label: 'Sawangan Lama', value: 'Sawangan Lama' }, - { label: 'Abadijaya', value: 'Abadijaya' }, - { label: 'Bakti Jaya', value: 'Bakti Jaya' }, - { label: 'Cisalak', value: 'Cisalak' }, - { label: 'Mekar Jaya', value: 'Mekar Jaya' }, + { label: 'Sawangan', value: 'Sawangan' }, { label: 'Sukmajaya', value: 'Sukmajaya' }, - { label: 'Tirtajaya', value: 'Tirtajaya' }, - { label: 'Cilangkap', value: 'Cilangkap' }, - { label: 'Cimpaeun', value: 'Cimpaeun' }, - { label: 'Jatijajar', value: 'Jatijajar' }, - { label: 'Leuwinanggung', value: 'Leuwinanggung' }, - { label: 'Sukamaju Baru', value: 'Sukamaju Baru' }, - { label: 'Sukatani', value: 'Sukatani' }, { label: 'Tapos', value: 'Tapos' }, ]; +const KELURAHAN_VALUES: { [key: string]: {label: string; value: string}[] } = { + 'Beji': [ + { label: 'Beji', value: 'Beji' }, + { label: 'Beji Timur', value: 'Beji Timur' }, + { label: 'Kemirimuka', value: 'Kemirimuka' }, + { label: 'Kukusan', value: 'Kukusan' }, + { label: 'Pondok Cina', value: 'Pondok Cina' }, + { label: 'Tanah Baru', value: 'Tanah Baru' }, + ], + 'Bojongsari': [ + { label: 'Bojongsari Baru', value: 'Bojongsari Baru' }, + { label: 'Bojongsari Lama', value: 'Bojongsari Lama' }, + { label: 'Curug', value: 'Curug' }, + { label: 'Duren Mekar', value: 'Duren Mekar' }, + { label: 'Duren Seribu', value: 'Duren Seribu' }, + { label: 'Pondok Petir', value: 'Pondok Petir' }, + { label: 'Serua', value: 'Serua' }, + ], + 'Cilodong': [ + { label: 'Cilodong', value: 'Cilodong' }, + { label: 'Jatimulya', value: 'Jatimulya' }, + { label: 'Kalibaru', value: 'Kalibaru' }, + { label: 'Kalimulya', value: 'Kalimulya' }, + { label: 'Sukamaju', value: 'Sukamaju' }, + ], + 'Cimanggis': [ + { label: 'Cisalak Pasar', value: 'Cisalak Pasar' }, + { label: 'Curug', value: 'Curug' }, + { label: 'Harjamukti', value: 'Harjamukti' }, + { label: 'Mekarsari', value: 'Mekarsari' }, + { label: 'Pasir Gunung Selatan', value: 'Pasir Gunung Selatan' }, + { label: 'Tugu', value: 'Tugu' }, + ], + 'Cinere': [ + { label: 'Cinere', value: 'Cinere' }, + { label: 'Gandul', value: 'Gandul' }, + { label: 'Pangkalan Jati', value: 'Pangkalan Jati' }, + { label: 'Pangkalan Jati Baru', value: 'Pangkalan Jati Baru' }, + ], + 'Cipayung': [ + { label: 'Bojong Pondok Terong', value: 'Bojong Pondok Terong' }, + { label: 'Cipayung', value: 'Cipayung' }, + { label: 'Cipayung Jaya', value: 'Cipayung Jaya' }, + { label: 'Pondok Jaya', value: 'Pondok Jaya' }, + { label: 'Ratujaya', value: 'Ratujaya' }, + ], + 'Limo': [ + { label: 'Grogol', value: 'Grogol' }, + { label: 'Krukut', value: 'Krukut' }, + { label: 'Limo', value: 'Limo' }, + { label: 'Meruyung', value: 'Meruyung' }, + ], + 'Pancoran Mas': [ + { label: 'Depok', value: 'Depok' }, + { label: 'Depok Jaya', value: 'Depok Jaya' }, + { label: 'Mampang', value: 'Mampang' }, + { label: 'Pancoran Mas', value: 'Pancoran Mas' }, + { label: 'Rangkapan Jaya', value: 'Rangkapan Jaya' }, + { label: 'Rangkapan Jaya Baru', value: 'Rangkapan Jaya Baru' }, + ], + 'Sawangan': [ + { label: 'Bedahan', value: 'Bedahan' }, + { label: 'Cinangka', value: 'Cinangka' }, + { label: 'Kedaung', value: 'Kedaung' }, + { label: 'Pasir Putih', value: 'Pasir Putih' }, + { label: 'Pengasinan', value: 'Pengasinan' }, + { label: 'Sawangan Baru', value: 'Sawangan Baru' }, + { label: 'Sawangan Lama', value: 'Sawangan Lama' }, + ], + 'Sukmajaya': [ + { label: 'Abadijaya', value: 'Abadijaya' }, + { label: 'Bakti Jaya', value: 'Bakti Jaya' }, + { label: 'Cisalak', value: 'Cisalak' }, + { label: 'Mekar Jaya', value: 'Mekar Jaya' }, + { label: 'Sukmajaya', value: 'Sukmajaya' }, + { label: 'Tirtajaya', value: 'Tirtajaya' }, + ], + 'Tapos': [ + { label: 'Cilangkap', value: 'Cilangkap' }, + { label: 'Cimpaeun', value: 'Cimpaeun' }, + { label: 'Jatijajar', value: 'Jatijajar' }, + { label: 'Leuwinanggung', value: 'Leuwinanggung' }, + { label: 'Sukamaju Baru', value: 'Sukamaju Baru' }, + { label: 'Sukatani', value: 'Sukatani' }, + { label: 'Tapos', value: 'Tapos' }, + ], +}; + const SIGNUP_CATEGORY = [ { label: 'Kader', value: false }, { label: 'Admin', value: true }, diff --git a/src/contexts/AppContext/types.ts b/src/contexts/AppContext/types.ts index 2248f75fefb041db890d7f1cd10c09c4c77012f2..90780505a3b7f0d6886989897b2e99fd2783f940 100644 --- a/src/contexts/AppContext/types.ts +++ b/src/contexts/AppContext/types.ts @@ -78,8 +78,27 @@ export interface AgeKeyProps { maxAge: number; } +export interface DateProps { + start_date: Date; + end_date: Date; +} + +export interface SexKeyProps { + isMale: boolean; +} + +export interface DistrictKeyProps { + district: string; +} + export enum StatisticType { Age = 'Umur', District ='Kecamatan', - Sex = 'Jenis Kelamin' + Sex = 'Jenis Kelamin', + Date = 'Tanggal' +} + +export interface ExportCSVPayload { + filterType: StatisticType; + filters: AgeKeyProps | SexKeyProps | DistrictKeyProps; } \ No newline at end of file diff --git a/src/scenes/AccountManagement/components/AddAccountForm/index.test.tsx b/src/scenes/AccountManagement/components/AddAccountForm/index.test.tsx index 9a30fcc9c679ce70e03eb2d08c6613dfe1e08bbd..ea0155f014ba597a7ff41190d185205bff422b3e 100644 --- a/src/scenes/AccountManagement/components/AddAccountForm/index.test.tsx +++ b/src/scenes/AccountManagement/components/AddAccountForm/index.test.tsx @@ -8,6 +8,7 @@ import axios from 'axios'; import { useMainService } from 'services'; import { AppContext } from 'contexts'; import AddAccountForm from '.'; +import { KECAMATAN_VALUES, KELURAHAN_VALUES } from 'constant'; const testProps = { services: { @@ -113,8 +114,8 @@ describe('AddNewAccountForm', () => { expect(passwordField.props.value).toEqual(''); expect(emailField.props.value).toEqual(''); expect(phoneField.props.value).toEqual(''); - expect(districtField.props.value).toEqual(''); - expect(subdistrictField.props.value).toEqual(''); + expect(districtField.props.value).toEqual(KECAMATAN_VALUES[0].value); + expect(subdistrictField.props.value).toEqual(KELURAHAN_VALUES[KECAMATAN_VALUES[0].value][0].value); }); diff --git a/src/scenes/AccountManagement/components/AddAccountForm/index.tsx b/src/scenes/AccountManagement/components/AddAccountForm/index.tsx index 51513f674f0432ed43072d5c5c94f5bbab6e17f3..ba6838e116e993d21e682002b66a06f3585cd9e8 100644 --- a/src/scenes/AccountManagement/components/AddAccountForm/index.tsx +++ b/src/scenes/AccountManagement/components/AddAccountForm/index.tsx @@ -42,14 +42,19 @@ export default function AddAccountForm(props: AddAccountFormProps) { sub_district: { type: 'text' }, }); + const defaultDistrict = KECAMATAN_VALUES[0].value; + const defaultSubDistrict = KELURAHAN_VALUES[defaultDistrict][0].value; + const [subDistrictChoices, setSubDistrictChoices] = useState(KELURAHAN_VALUES[defaultDistrict]); + const resetFields = () => { setField('name', ''); setField('username', ''); setField('password', ''); setField('email', ''); setField('phone_number', ''); - setField('district', ''); - setField('sub_district', ''); + setField('district', defaultDistrict); + setField('sub_district', defaultSubDistrict); + setSubDistrictChoices(KELURAHAN_VALUES[defaultDistrict]); }; const getInitialFields = () => ({ @@ -124,6 +129,11 @@ export default function AddAccountForm(props: AddAccountFormProps) { } }; + const handleDistrictChange = (value: string) => { + setField('district', value); + setSubDistrictChoices(KELURAHAN_VALUES[value]); + }; + return ( @@ -206,7 +216,7 @@ export default function AddAccountForm(props: AddAccountFormProps) { placeholder='Masukan kecamatan akun' value={addAccountForm.fields.district.value} values={KECAMATAN_VALUES} - updateValue={(value) => setField('district', value)} + updateValue={handleDistrictChange} /> {errors.district} @@ -217,7 +227,7 @@ export default function AddAccountForm(props: AddAccountFormProps) { isRequired placeholder='Masukan kelurahan akun' value={addAccountForm.fields.sub_district.value} - values={KELURAHAN_VALUES} + values={subDistrictChoices} updateValue={(value) => setField('sub_district', value)} /> {errors.sub_district} diff --git a/src/scenes/AccountManagement/components/UserProfile/index.tsx b/src/scenes/AccountManagement/components/UserProfile/index.tsx index 35842c952b0d3ff44b018d9817cc21936b60d017..e0500814abd2d988c49754eee4ef247d9cdb3c98 100644 --- a/src/scenes/AccountManagement/components/UserProfile/index.tsx +++ b/src/scenes/AccountManagement/components/UserProfile/index.tsx @@ -66,6 +66,9 @@ export default function UserProfile({ const [canDelete, setCanDelete] = useState(true); const isAdmin = is_admin; + const defaultDistrict = KECAMATAN_VALUES[0].value; + const defaultSubDistrict = KELURAHAN_VALUES[defaultDistrict][0].value; + const getInitialFields = () => ({ name: '', email: '', @@ -74,6 +77,7 @@ export default function UserProfile({ sub_district: '', }); const [errors, setErrors] = useState(getInitialFields()); + const [subDistrictChoices, setSubDistrictChoices] = useState(KELURAHAN_VALUES[defaultDistrict]); const hanldeEditAccount = async () => { const editBody = { @@ -165,6 +169,11 @@ export default function UserProfile({ } }, []); + const handleDistrictChange = (value: string) => { + setUserField('district', value); + setSubDistrictChoices(KELURAHAN_VALUES[value]); + }; + return ( @@ -268,7 +277,7 @@ export default function UserProfile({ editable={shouldEdit} value={userForm.fields.district.value} values={KECAMATAN_VALUES} - updateValue={(value) => setUserField('district', value)} + updateValue={handleDistrictChange} information={errors.district} /> @@ -278,7 +287,7 @@ export default function UserProfile({ isRequired={shouldEdit} editable={shouldEdit} value={userForm.fields.sub_district.value} - values={KELURAHAN_VALUES} + values={subDistrictChoices} updateValue={(value) => setUserField('sub_district', value)} information={errors.sub_district} /> diff --git a/src/scenes/AccountManagement/index.test.tsx b/src/scenes/AccountManagement/index.test.tsx index f585cfac6c9cfc19087ce216607e2df34f3875f9..7636d29018c479f4012a1aa4073806e60067e70b 100644 --- a/src/scenes/AccountManagement/index.test.tsx +++ b/src/scenes/AccountManagement/index.test.tsx @@ -50,9 +50,7 @@ describe('AccountManagementPage', () => { }); it('fetches accounts and display accounts when pressed', async () => { - - jest.setTimeout(30000); - + jest.setTimeout(50000); mockedAxios.request.mockResolvedValue({ status: 200, data: accountData diff --git a/src/scenes/AccountManagement/index.tsx b/src/scenes/AccountManagement/index.tsx index 3ceb3851d22a567fcdcded4ffcfffae619964585..9a9fc68edb0996293dadbac73d8d7793daf31d82 100644 --- a/src/scenes/AccountManagement/index.tsx +++ b/src/scenes/AccountManagement/index.tsx @@ -17,14 +17,14 @@ import AddAccountForm from './components/AddAccountForm'; export default function AccountManagementPage() { const { setModal } = useContext(AppContext); const [accountCount, setAccountCount] = useState(0); - const [cleanedData, setCleanedData] = useState([['', '', '', '', '', '']]); + const [cleanedData, setCleanedData] = useState([['', '', '', '', '', '', '', '']]); const global = useContext(AppContext); const fetchAccounts = async ( pageNumber: number, query?: string ): Promise>> => { - const response = await global.services.main.searchAccount(query, pageNumber) + const response = await global.services.main.searchAccount(query, pageNumber); if (response.status === 200) { const { data } = response; @@ -38,9 +38,9 @@ export default function AccountManagementPage() { acc.phone_number, acc.district, acc.sub_district, - acc.is_admin ? "Admin" : "Kader", - acc.is_verified && acc.is_active ? "Aktif" : "Tidak Aktif", - "0", + acc.is_admin ? 'Admin' : 'Kader', + acc.is_verified && acc.is_active ? 'Aktif' : 'Tidak Aktif', + '0', acc.id, acc.is_admin, acc.is_verified, @@ -48,29 +48,12 @@ export default function AccountManagementPage() { ]; }); } - return [] - } + return []; + }; - const addAccount = (newAccount: Account) => { - const newAccountArray:Array = [ - String(newAccount.name), - String(newAccount.username), - String(newAccount.email), - String(newAccount.phone_number), - String(newAccount.district), - String(newAccount.sub_district), - String(newAccount.is_admin ? 'Admin' : 'Kader'), - String(newAccount.is_active && newAccount.is_verified ? 'Aktif' : 'Tidak Aktif'), - String(0), - String(newAccount.id), - String(newAccount.is_admin), - String(newAccount.is_verified), - String(newAccount.is_active), - ]; - const previousAccounts = cleanedData; - previousAccounts.push(newAccountArray); - setCleanedData(previousAccounts); - setAccountCount(accountCount + 1); + const addAccount = async (newAccount: Account) => { + const newAccountState = await fetchAccounts(1, ''); + setCleanedData(newAccountState); }; return ( @@ -142,6 +125,8 @@ export default function AccountManagementPage() { ); }} + data={cleanedData} + setData={setCleanedData} maximumData={accountCount} onChange={async (searchValue, pageNumber) => { return fetchAccounts(pageNumber, searchValue); diff --git a/src/scenes/Home/components/ExportCSVButton/index.test.tsx b/src/scenes/Home/components/ExportCSVButton/index.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8ad1ceef16dfe18e26802408bb478f35df3e6979 --- /dev/null +++ b/src/scenes/Home/components/ExportCSVButton/index.test.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { shallow } from "enzyme"; +import ExportCSVButton from "."; + +describe('', () => { + it('should render correctly', () => { + shallow( {}} + clickable={false} />); + }) + it('should call downloadAsPDF', () => { + const mock = jest.fn(); + const btn = shallow(); + btn.find('Button').simulate('click'); + expect(mock.mock.calls.length).toBeGreaterThan(0); + }) +}) \ No newline at end of file diff --git a/src/scenes/Home/components/ExportCSVButton/index.tsx b/src/scenes/Home/components/ExportCSVButton/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..50dcd96d0656d32743698db86e0ff57b49424f96 --- /dev/null +++ b/src/scenes/Home/components/ExportCSVButton/index.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Box, Button } from "components"; + +interface ExportCSVButtonProps { + onClick: () => void; + clickable: boolean; +} + +function ExportCSVButton({ + onClick, + clickable, +}: ExportCSVButtonProps) { + + return ( + + + + ) +} + +export default ExportCSVButton; \ No newline at end of file diff --git a/src/scenes/Home/components/ExportPDFButton/index.test.tsx b/src/scenes/Home/components/ExportPDFButton/index.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4df936bc864ccf939687bb4303d10a47162b9699 --- /dev/null +++ b/src/scenes/Home/components/ExportPDFButton/index.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { shallow } from "enzyme"; +import ExportPDFButton from "."; + +const dummyImage = "data:image/png;base64,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" + +describe('', () => { + it('should render correctly', () => { + shallow(); + }) + it('should call downloadAsPDF', () => { + const mod = require('../../utilities/utils'); + jest.spyOn(mod, 'downloadAsPDF'); + const btn = shallow(); + btn.find('Button').simulate('click'); + expect(mod.downloadAsPDF).toHaveBeenCalled(); + }) +}) \ No newline at end of file diff --git a/src/scenes/Home/components/ExportPDFButton/index.tsx b/src/scenes/Home/components/ExportPDFButton/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2e2a87a60566e476280f27c855093e70cee14164 --- /dev/null +++ b/src/scenes/Home/components/ExportPDFButton/index.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Box, Button } from 'components'; +import { downloadAsPDF } from 'scenes/Home/utilities/utils'; + +interface ExportPDFButtonProps { + type: string, + selectedKey: string, + clickable: boolean, +} + +function ExportPDFButton({ + type, selectedKey, clickable +}: ExportPDFButtonProps) { + const handleDownload = React.useCallback(async (type: string, selectedKey: string) => { + downloadAsPDF(type, selectedKey); + }, []); + return ( + + + + ); +} + +export default ExportPDFButton; \ No newline at end of file diff --git a/src/scenes/Home/components/Statistic/index.test.tsx b/src/scenes/Home/components/Statistic/index.test.tsx index e51151cf59e8035b3c4698368c0f26805b57be92..3fa6fa57893b20546315a1857c81fb628af2d544 100644 --- a/src/scenes/Home/components/Statistic/index.test.tsx +++ b/src/scenes/Home/components/Statistic/index.test.tsx @@ -5,6 +5,7 @@ import Statistic from '.'; import { useMainService } from 'services'; import { AppContext } from 'contexts'; +import { Checkbox } from 'components'; describe('', () => { const testProps = { @@ -94,4 +95,28 @@ describe('', () => { ); }); + + it('render kasus in time range stats chart successfully', () => { + mount( + + + + ); + }); + + it('sets ignoreDateRange state after clicking ignoreDateRange button', () => { + const wrapper = mount(); + + expect(wrapper.find(Checkbox).props().isChecked).toBe(false); + wrapper.find(Checkbox).simulate('click'); + expect(wrapper.find(Checkbox).props().isChecked).toBe(true); + }) }); \ No newline at end of file diff --git a/src/scenes/Home/components/Statistic/index.tsx b/src/scenes/Home/components/Statistic/index.tsx index e416b69ece9a2e7cb80f5b1509831f5f6d81ebda..512aa92f3adda84b92a9c11740f3c3ebc84f64c7 100644 --- a/src/scenes/Home/components/Statistic/index.tsx +++ b/src/scenes/Home/components/Statistic/index.tsx @@ -1,12 +1,15 @@ import React, { useContext, useState, useEffect } from 'react'; -import { Box, Text, Field, Gap, Button, Table, CategoryButton } from 'components'; +import { Box, Text, Field, Gap, Button, Table, CategoryButton, Checkbox } from 'components'; import { PieChart, Pie, Cell, BarChart, CartesianGrid, XAxis, Tooltip, Legend, YAxis, Bar } from 'recharts'; import { AppContext } from 'contexts'; import { KECAMATAN_VALUES, PIE_COLORS, GENDER_VALUES } from 'constant'; import { renderActiveShape, translate } from 'scenes/Home/utilities/utils'; import { - ExportableValue, StatisticType, AgeKeyProps + Exportables, ExportableValue, StatisticType, AgeKeyProps, DateProps } from 'contexts/AppContext/types'; +import ExportPDFButton from '../ExportPDFButton'; +import { StatisticData } from '../types'; +import ExportCSVButton from '../ExportCSVButton'; interface StatisticProps { isTable: boolean; @@ -30,25 +33,50 @@ function Statistic({ minAge: 0, maxAge: 0, }); - const [activeIndex, setActiveIndex] = useState(0); + const [activeIndex, setActiveIndex] = useState([0,1,2]); const [ selectedKeyData, setselectedKeyData - ] = useState([{}]); - const onPieEnter = (_pData: any, index: number) => { - setActiveIndex(index); - }; + ] = useState({ totalCount: 0, parts: [] }); + const [dateKey, setDateKey] = useState({ + start_date: new Date(), + end_date: new Date(), + }) + const [statisticsDateData, setstatisticsDateData] = useState({ + age:{}, + sex:{}, + district:{}, + total_count:0 + }); + const [isDateStatistic, setIsDateStatistic] = useState(false); + const [ignoreDateRange, setIgnoreDateRange] = useState(false); - const filterExports = async (filterType: StatisticType) => { - const response = await services.main.filterExportCSV( - filterType, - isTable ? translate(pagedData[0][0]) : selectedKey, - isTable ? - { - minAge: pagedData[0][0], - maxAge: pagedData[0][0] - } : ageKey - ); + const generateFilter = (filterType: StatisticType) => { + switch(filterType) { + case StatisticType.Age: + return { + filterType: filterType, + filters: isTable ? + { + minAge: pagedData[0][0], + maxAge: pagedData[0][0] + } : ageKey, + } + case StatisticType.District: + return { + filterType: filterType, + filters: { district: isTable ? translate(pagedData[0][0]) : selectedKey }, + } + case StatisticType.Sex: + return { + filterType: filterType, + filters: { isMale: isTable ? translate(pagedData[0][0]) : selectedKey } + } + } + } + + const filterExports = async (type: StatisticType) => { + const response = await services.main.filterExportCSV(generateFilter(type)); if (response.status === 200) { const url = window.URL.createObjectURL(new Blob([response.data])); @@ -71,6 +99,17 @@ function Statistic({ } }; + const fetchStatisticsByDate = async () => { + const start_date = (ignoreDateRange) ? "0010-01-01" : dateKey.start_date.toISOString().split('T')[0]; + const end_date = (ignoreDateRange) ? "9990-12-31" : dateKey.end_date.toISOString().split('T')[0]; + const response = await services.main.fetchStatisticsByDate(start_date, end_date); + + if (response.status === 200) { + const { data } = response; + setstatisticsDateData(data); + } + }; + useEffect(() => { const tempAgeObject = { positive: 0, @@ -90,24 +129,37 @@ function Statistic({ } } - const tempData = [ - { name: 'Kasus Positif', jumlah: tempAgeObject.positive }, - { name: 'Kasus Negatif', jumlah: tempAgeObject.negative }, - { name: 'Kasus Terduga', jumlah: tempAgeObject.undetermined }, - { total_count: tempAgeObject.total_count } - ]; + const tempData = { + totalCount: tempAgeObject.total_count, + parts: [ + { name: 'Kasus Positif', jumlah: tempAgeObject.positive }, + { name: 'Kasus Negatif', jumlah: tempAgeObject.negative }, + { name: 'Kasus Terduga', jumlah: tempAgeObject.undetermined }, + ] + }; setselectedKeyData(tempData); + setIsDateStatistic(false); }, [data, ageKey]); useEffect(() => { const selectedData = data[selectedKey]; - const tempData = [ - { name: 'Kasus Positif', jumlah: selectedData?.positive || 0 }, - { name: 'Kasus Negatif', jumlah: selectedData?.negative || 0 }, - { name: 'Kasus Terduga', jumlah: selectedData?.undetermined || 0 }, - { total_count: selectedData?.total_count || 0 } - ]; + const tempData = { + totalCount: selectedData?.total_count || 0, + parts: [ + { name: 'Kasus Positif', jumlah: selectedData?.positive || 0 }, + { name: 'Kasus Negatif', jumlah: selectedData?.negative || 0 }, + { name: 'Kasus Terduga', jumlah: selectedData?.undetermined || 0 }, + ], + }; + let res: Array = []; + tempData.parts.forEach((element, index) => { + if(element.jumlah > 0) { + res.push(index); + } + }); + setActiveIndex(res); setselectedKeyData(tempData); + setIsDateStatistic(false); }, [data, selectedKey]); useEffect(() => { @@ -122,33 +174,95 @@ function Statistic({ parsedData.push(currentData); } setTableData(parsedData); + setIsDateStatistic(false); }, [isTable, data]); useEffect(() => { setPagedData(tableData.slice(0, 10)); }, [tableData]); + useEffect(() => { + fetchStatisticsByDate(); + setIsDateStatistic(true); + }, [dateKey, ignoreDateRange]); + const renderChart = () => { + if(isDateStatistic) { + const values = Object.values(statisticsDateData['district']); + const keys = Object.keys(statisticsDateData['district']); + let res: Array = []; + let sum: number = 0; + keys.forEach((element, index) => { + res.push(Object.assign({"name": keys[index]}, values[index])); + sum += values[index]["total_count"]; + }); + res.sort(function(a: any, b: any){return a['total_count'] - b['total_count']}); + res.reverse(); + if(sum === 0) { + return ( + {`Belum ada kasus untuk ${type.toLowerCase()} ini`} + ) + } + return ( + + + + (Math.ceil(dataMax * 1.5))]} allowDataOverflow={true}/> + + + + + + + ); + } if (chartType) { return ( { - selectedKeyData.map((entry, index) => - + selectedKeyData.parts.map((entry, index) => + ) } + ); } @@ -156,7 +270,7 @@ function Statistic({ { - selectedKeyData.map((entry, index) => + selectedKeyData.parts.map((entry, index) => ) } @@ -179,108 +293,176 @@ function Statistic({ if (!isTable) { return ( <> - { - type === StatisticType.Age ? - <> - - { - setAgeKey({ ...ageKey, minAge: val }); - }} - /> - - { - setAgeKey({ ...ageKey, maxAge: val }); - }} - /> - - - - - - {`${type}: ${ageKey.minAge} s/d ${ageKey.maxAge} tahun`} - + + + { + type === StatisticType.Age ? + <> + + { + setAgeKey({ ...ageKey, minAge: val }); + }} + /> + + { + setAgeKey({ ...ageKey, maxAge: val }); + }} + /> - - - - - - : - - - - - - {`${type}: ${translate(selectedKey)}`} + filterExports(type)} + clickable={selectedKeyData.totalCount !== 0} + /> + + + - - + filterExports(type)} + clickable={selectedKeyData.totalCount !== 0} + /> + + + - - } - - { - (selectedKeyData[3]?.total_count) === 0 ? - {`Belum ada kasus untuk ${type.toLowerCase()} ini`} : + } + + + + { + (!isDateStatistic) ? + ((selectedKeyData.totalCount === 0) ? + {`Belum ada kasus untuk ${type.toLowerCase()} ini`} + : + <> + +
+ {renderChart()} +
+ + {`Total: ${selectedKeyData.totalCount} kasus`} + + + ) + : <> - - {renderChart()} +
+ {renderChart()} +
- {`Total: ${selectedKeyData[3]?.total_count} kasus`} } +
+ ); } @@ -328,17 +510,18 @@ function Statistic({ {`Filter satu ${type.toLowerCase()} untuk mengekspor.`} } - + } /> + ); } diff --git a/src/scenes/Home/components/types.tsx b/src/scenes/Home/components/types.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2b1c32bcedd86a30b7e352ea4a93207d70ed000d --- /dev/null +++ b/src/scenes/Home/components/types.tsx @@ -0,0 +1,9 @@ +export interface StatisticPartData { + name: string; + jumlah: number; +} + +export interface StatisticData { + totalCount: number; + parts: Array; +} \ No newline at end of file diff --git a/src/scenes/Home/index.test.tsx b/src/scenes/Home/index.test.tsx index 86af4cf5e777725f26ab4a9bda8018758fb29968..fe42a141814e4fb8a1b35d3c13bdff8d89c18dc3 100644 --- a/src/scenes/Home/index.test.tsx +++ b/src/scenes/Home/index.test.tsx @@ -1,7 +1,19 @@ import React from 'react'; import { shallow } from 'enzyme'; import Home from '.'; +import Statistic from './components/Statistic'; +import { StatisticType } from 'contexts/AppContext/types'; + +describe("", () => { + + it('render Home component without crashing', () => { + shallow(); + }); + + it('renders statistic kasus keseluruhan in timerange', () => { + const wrapper = shallow(); + expect(wrapper.find({type: StatisticType.Date})).toHaveLength(1); + }); -it('render Home component without crashing', () => { - shallow(); }); + diff --git a/src/scenes/Home/index.tsx b/src/scenes/Home/index.tsx index b0f2a0425b013b076c5b0040229176b42626696d..6200d8ddb452963f2682932067a170205f98b13d 100644 --- a/src/scenes/Home/index.tsx +++ b/src/scenes/Home/index.tsx @@ -47,6 +47,8 @@ function Home() { } }; + const fetchExportsPDF = async () => {} + useEffect(() => { fetchStatistics(); }, [fetchStatistics]); @@ -65,9 +67,9 @@ function Home() { borderRadius="3px" axis={Box.Axis.Horizontal} > - + - - - + {statisticType + ? + + + + : + <> + + } + {!statisticType ? + <> + + + Export diagram Keseluruhan Kasus + + + + + + + : + <> + + } @@ -109,11 +142,11 @@ function Home() { /> @@ -127,11 +160,11 @@ function Home() { /> diff --git a/src/scenes/Home/utilities/utils.test.tsx b/src/scenes/Home/utilities/utils.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..078eb8e52789632d4281ee76c5acf7226c19502a --- /dev/null +++ b/src/scenes/Home/utilities/utils.test.tsx @@ -0,0 +1,8 @@ +import { translate } from './utils' + +describe('utils', () => { + it('translates jenis kelamin', () => { + const ret_val = translate("male"); + expect(ret_val).toEqual("Pria"); + }) +}) \ No newline at end of file diff --git a/src/scenes/Home/utilities/utils.tsx b/src/scenes/Home/utilities/utils.tsx index c283f1042a93f5c397e41ba8c954e74c94baa406..3c1b390d10b5ecd280aa570cf757036625002018 100644 --- a/src/scenes/Home/utilities/utils.tsx +++ b/src/scenes/Home/utilities/utils.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { Sector } from 'recharts'; +import { jsPDF } from "jspdf"; +import { PIE_COLORS } from 'constant'; interface ActiveShapeProps { cx: number, cy: number, midAngle: number, innerRadius: number, @@ -42,7 +44,6 @@ const renderActiveShape = (props: ActiveShapeProps) => { return ( - {payload.name} { ); }; -export { translate, renderActiveShape }; \ No newline at end of file +const blobToImage = (blob: Blob) => { + return new Promise(resolve => { + const url = URL.createObjectURL(blob) + let img = new Image() + img.onload = () => { + URL.revokeObjectURL(url) + resolve(img) + } + img.src = url + }) +} + +const image2pdf = (imageType: string, type: string, key: string) => { + const imgWidth = 252; + const imgHeight = 156; + const pageWidth = 297; + const pageHeight = 210; + + const marginX = (pageWidth - imgWidth) / 2; + const marginY = (pageHeight - imgHeight) / 2; + + const doc = new jsPDF({orientation: "l", format:[pageWidth, pageHeight]}); + + doc.setFontSize(40); + doc.text(((key != "") ? type : "") + " " + key, 145, 25, {align:"center"}); + + let svg: Element = document.getElementById(type)?.children[0]?.children[0].cloneNode(true)! as Element; + svg.setAttribute("width", "952"); + svg.setAttribute("height", "589"); + svg.setAttribute("preserveAspectRatio", "xMidYMid meet"); + let svgURL = new XMLSerializer().serializeToString(svg); + let svgBlob = new Blob([svgURL], { type: "image/svg+xml;charset=utf-8" }); + blobToImage(svgBlob).then(img => { + let canvas = document.createElement('canvas'); + canvas.width = 952; + canvas.height = 589; + let context = canvas.getContext('2d')!; + context.drawImage(img, 0, 0, context.canvas.width, context.canvas.height); + let png = canvas.toDataURL('image/png', 1.0); + doc.addImage(png, imageType, marginX, marginY, 252, 156, undefined, "SLOW"); + doc.setFillColor(PIE_COLORS[0]); + doc.rect(30, 170, 5, 5, 'F'); + doc.setFillColor(PIE_COLORS[1]); + doc.rect(30, 180, 5, 5, 'F'); + doc.setFillColor(PIE_COLORS[2]); + doc.rect(30, 190, 5, 5, 'F'); + doc.setFontSize(14); + doc.text("Kasus Positif", 40, 174, {align:"left"}); + doc.text("Kasus Negatif", 40, 184, {align:"left"}); + doc.text("Kasus Terduga", 40, 194, {align:"left"}); + doc.save("bebas.pdf"); + }); +}; + +const downloadAsPDF = (type: string, selectedKey: string) => { + image2pdf("PNG", type, translate(selectedKey)); +} + +export { translate, renderActiveShape, image2pdf, downloadAsPDF }; \ No newline at end of file diff --git a/src/scenes/PositiveCaseInput/components/NewCaseForm/index.test.tsx b/src/scenes/PositiveCaseInput/components/NewCaseForm/index.test.tsx index 52f46640c4fd10ee34d90b31899b83bac45c5f1c..066d0303f2b4b8627ae3caf0792aa6cfc4d64f5b 100644 --- a/src/scenes/PositiveCaseInput/components/NewCaseForm/index.test.tsx +++ b/src/scenes/PositiveCaseInput/components/NewCaseForm/index.test.tsx @@ -16,32 +16,32 @@ const testProps = { jest.mock('axios'); it('should render properly with expected fields and default values', () => { - const form = renderer.create( - - ); + const form = renderer.create( + + ); expect(form).toBeTruthy(); - const nameField = form.root.find((elem: (typeof Field) ) => elem.props.name === 'Nama'); - expect(nameField).toBeTruthy(); - const ageField = form.root.find((elem: (typeof Field)) => elem.props.name === 'Usia'); - expect(ageField).toBeTruthy(); - const genderField = form.root.find((elem: (typeof Field) ) => elem.props.name === 'Jenis Kelamin'); - expect(genderField).toBeTruthy(); - expect(genderField.props.value).toBe('Laki-laki'); - const addressField = form.root.find((elem: (typeof Field)) => elem.props.name === 'Alamat'); - expect(addressField).toBeTruthy(); - const kecamatanField = form.root.find((elem: (typeof Field) ) => elem.props.name === 'Kecamatan'); - expect(kecamatanField).toBeTruthy(); - expect(kecamatanField.props.value).toBe(KECAMATAN_VALUES[0].value); - const kelurahanField = form.root.find((elem: (typeof Field)) => elem.props.name === 'Kelurahan'); - expect(kelurahanField).toBeTruthy(); - expect(kelurahanField.props.value).toBe(KELURAHAN_VALUES[0].value); - const dateField = form.root.find((elem: (typeof Field) ) => elem.props.name === 'Tanggal Pemeriksaan'); - expect(dateField).toBeTruthy(); - const resultField = form.root.find((elem: (typeof Field)) => elem.props.name === 'Hasil Pemeriksaan'); - expect(resultField).toBeTruthy(); - expect(resultField.props.value).toBe(HASIL_PEMERIKSAAN_VALUES[0].value); + const nameField = form.root.find((elem: (typeof Field) ) => elem.props.name === 'Nama'); + expect(nameField).toBeTruthy(); + const ageField = form.root.find((elem: (typeof Field)) => elem.props.name === 'Usia'); + expect(ageField).toBeTruthy(); + const genderField = form.root.find((elem: (typeof Field) ) => elem.props.name === 'Jenis Kelamin'); + expect(genderField).toBeTruthy(); + expect(genderField.props.value).toBe('Laki-laki'); + const addressField = form.root.find((elem: (typeof Field)) => elem.props.name === 'Alamat'); + expect(addressField).toBeTruthy(); + const kecamatanField = form.root.find((elem: (typeof Field) ) => elem.props.name === 'Kecamatan'); + expect(kecamatanField).toBeTruthy(); + expect(kecamatanField.props.value).toBe(KECAMATAN_VALUES[0].value); + const kelurahanField = form.root.find((elem: (typeof Field)) => elem.props.name === 'Kelurahan'); + expect(kelurahanField).toBeTruthy(); + expect(kelurahanField.props.value).toBe(KELURAHAN_VALUES[KECAMATAN_VALUES[0].value][0].value); + const dateField = form.root.find((elem: (typeof Field) ) => elem.props.name === 'Tanggal Pemeriksaan'); + expect(dateField).toBeTruthy(); + const resultField = form.root.find((elem: (typeof Field)) => elem.props.name === 'Hasil Pemeriksaan'); + expect(resultField).toBeTruthy(); + expect(resultField.props.value).toBe(HASIL_PEMERIKSAAN_VALUES[0].value); }); it('can input fields and submitted successfully', async () => { diff --git a/src/scenes/PositiveCaseInput/components/NewCaseForm/index.tsx b/src/scenes/PositiveCaseInput/components/NewCaseForm/index.tsx index 354b6b43d3f910428ea6ba18832c4fb1febb17c6..09844bb1197dd94822bcb4060a64877526410ab5 100644 --- a/src/scenes/PositiveCaseInput/components/NewCaseForm/index.tsx +++ b/src/scenes/PositiveCaseInput/components/NewCaseForm/index.tsx @@ -5,209 +5,220 @@ import { useFormState } from 'helper'; import { AppContext } from 'contexts'; export default function NewCaseForm() { - const { services, setModal } = useContext(AppContext); - const [caseForm, setCaseFormField] = useFormState({ - name: {type: 'text'}, - age: {type: 'number'}, - gender: {type: 'any'}, - address: {type: 'any'}, - district: {type: 'any'}, - subDistrict: {type: 'any'}, - checkResult: {type: 'any'}, - }); - const [checkDate, setCheckDate] = useState(); - const [isFirstTime, setIsFirstTime] = useState(true); - - const handleSubmit = async () => { - if (caseForm.isValid && checkDate && caseForm.fields.address.value) { - const caseSubjectData = { - name: caseForm.fields.name.value, - age: caseForm.fields.age.value, - is_male: caseForm.fields.gender.value === 'Laki-laki' ? true : false, - address: caseForm.fields.address.value, - district: caseForm.fields.district.value, - sub_district: caseForm.fields.subDistrict.value, - }; - - const caseSubjectResponse = await services.main.createCaseSubject(caseSubjectData); + const { services, setModal } = useContext(AppContext); + const [caseForm, setCaseFormField] = useFormState({ + name: { type: 'text' }, + age: { type: 'number' }, + gender: { type: 'any' }, + address: { type: 'any' }, + district: { type: 'any' }, + subDistrict: { type: 'any' }, + checkResult: { type: 'any' }, + }); + const [checkDate, setCheckDate] = useState(); + const [isFirstTime, setIsFirstTime] = useState(true); + + const handleSubmit = async () => { + if (caseForm.isValid && checkDate && caseForm.fields.address.value) { + const caseSubjectData = { + name: caseForm.fields.name.value, + age: caseForm.fields.age.value, + is_male: caseForm.fields.gender.value === 'Laki-laki', + address: caseForm.fields.address.value, + district: caseForm.fields.district.value, + sub_district: caseForm.fields.subDistrict.value, + }; + + const caseSubjectResponse = await services.main.createCaseSubject(caseSubjectData); - if (caseSubjectResponse.status === 201) { - const investigationCaseData = { - case_subject: caseSubjectResponse.data.id, - is_referral_needed: true, - outcome: caseForm.fields.checkResult.value, - }; + if (caseSubjectResponse.status === 201) { + const investigationCaseData = { + case_subject: caseSubjectResponse.data.id, + is_referral_needed: true, + outcome: caseForm.fields.checkResult.value, + }; - const investigationCaseResponse = await services.main.createInvestigationCase(investigationCaseData); + const investigationCaseResponse = await services.main.createInvestigationCase(investigationCaseData); - if (investigationCaseResponse.status === 201) { - const monitoringCaseData = { - investigation_case: investigationCaseResponse.data.id, - checking_date: checkDate.toISOString().slice(0, 10), - is_referred: true, - is_checked: true, - } + if (investigationCaseResponse.status === 201) { + const monitoringCaseData = { + investigation_case: investigationCaseResponse.data.id, + checking_date: checkDate.toISOString().slice(0, 10), + is_referred: true, + is_checked: true, + }; - const monitoringCaseResponse = await services.main.createMonitoringCase(monitoringCaseData); + const monitoringCaseResponse = await services.main.createMonitoringCase(monitoringCaseData); - if (monitoringCaseResponse.status === 201) { - setSubmitResultModal('Berhasil memasukkan kasus positif ' + caseForm.fields.name.value); - clearFields(); - } else { - setModal('Gagal memasukkan kasus positif ' + caseForm.fields.name.value); - } - } else { - setModal('Gagal memasukkan kasus positif ' + caseForm.fields.name.value); - } - } else { - setModal('Gagal memasukkan kasus positif ' + caseForm.fields.name.value); - } + if (monitoringCaseResponse.status === 201) { + setSubmitResultModal(`Berhasil memasukkan kasus positif ${ caseForm.fields.name.value}`); + clearFields(); + } else { + setModal(`Gagal memasukkan kasus positif ${ caseForm.fields.name.value}`); + } } else { - setIsFirstTime(false); + setModal(`Gagal memasukkan kasus positif ${ caseForm.fields.name.value}`); } - }; - - const setSubmitResultModal = (message: string) => { - setModal( - {message} - ); - }; - - const clearFields = () => { - setIsFirstTime(true); - setCaseFormField('name', ''); - setCaseFormField('age', ''); - setCaseFormField('gender', 'Laki-laki'); - setCaseFormField('address', ''); - setCaseFormField('district', KECAMATAN_VALUES[0].value); - setCaseFormField('subDistrict', KELURAHAN_VALUES[0].value); - setCaseFormField('checkResult', HASIL_PEMERIKSAAN_VALUES[0].value); - }; - - useEffect(() => { - setCaseFormField('subDistrict', KELURAHAN_VALUES[0].value); - setCaseFormField('district', KECAMATAN_VALUES[0].value); - setCaseFormField('checkResult', HASIL_PEMERIKSAAN_VALUES[0].value); - setCaseFormField('gender', 'Laki-laki') - }, []); - - return ( + } else { + setModal(`Gagal memasukkan kasus positif ${ caseForm.fields.name.value}`); + } + } else { + setIsFirstTime(false); + } + }; + + const setSubmitResultModal = (message: string) => { + setModal( + + {message} + + ); + }; + + const defaultDistrict = KECAMATAN_VALUES[0].value; + const defaultSubDistrict = KELURAHAN_VALUES[defaultDistrict][0].value; + const [subDistrictChoices, setSubDistrictChoices] = useState(KELURAHAN_VALUES[defaultDistrict]); + + const clearFields = () => { + setIsFirstTime(true); + setCaseFormField('name', ''); + setCaseFormField('age', ''); + setCaseFormField('gender', 'Laki-laki'); + setCaseFormField('address', ''); + setCaseFormField('district', defaultDistrict); + setCaseFormField('subDistrict', defaultSubDistrict); + setCaseFormField('checkResult', HASIL_PEMERIKSAAN_VALUES[0].value); + }; + + useEffect(() => { + setCaseFormField('subDistrict', defaultSubDistrict); + setCaseFormField('district', defaultDistrict); + setCaseFormField('checkResult', HASIL_PEMERIKSAAN_VALUES[0].value); + setCaseFormField('gender', 'Laki-laki'); + }, []); + + const handleDistrictChange = (value: string) => { + setCaseFormField('district', value); + setSubDistrictChoices(KELURAHAN_VALUES[value]); + }; + + return ( - setCaseFormField('name', value)} - information={isFirstTime || caseForm.fields.name.isValid ? '' : 'Nama hanya boleh mengandung alfabet dan tidak boleh kosong'} - /> - - - - - - setCaseFormField('age', value)} - information={isFirstTime || caseForm.fields.age.isValid ? '' : 'Usia hanya boleh mengandung angka dan tidak boleh kosong'} - /> - - - - setCaseFormField('gender', value)} - /> - - + setCaseFormField('name', value)} + information={isFirstTime || caseForm.fields.name.isValid ? '' : 'Nama hanya boleh mengandung alfabet dan tidak boleh kosong'} + /> - - + + + + setCaseFormField('address', value)} - information={isFirstTime || caseForm.fields.address.value ? '' : 'Alamat tidak boleh kosong'} - /> - - - - - setCaseFormField('subDistrict', value)} - /> - + placeholder='Usia' + value={caseForm.fields.age.value} + updateValue={(value) => setCaseFormField('age', value)} + information={isFirstTime || caseForm.fields.age.isValid ? '' : 'Usia hanya boleh mengandung angka dan tidak boleh kosong'} + /> + - - setCaseFormField('district', value)} - /> - + + setCaseFormField('gender', value)} + /> + + + + setCaseFormField('address', value)} + information={isFirstTime || caseForm.fields.address.value ? '' : 'Alamat tidak boleh kosong'} + /> - - + + + setCheckDate(value)} - information={isFirstTime || checkDate ? '' : 'Tanggal pemeriksaan tidak boleh kosong'} - /> - - - setCaseFormField('subDistrict', value)} + /> + + + + setCaseFormField('checkResult', value)} - /> - - - - + value={caseForm.fields.district.value || defaultDistrict} + values={KECAMATAN_VALUES} + updateValue={handleDistrictChange} + /> + + + + setCheckDate(value)} + information={isFirstTime || checkDate ? '' : 'Tanggal pemeriksaan tidak boleh kosong'} + /> + + + setCaseFormField('checkResult', value)} + /> + + + + + - ); + ); } \ No newline at end of file diff --git a/src/services/hooks/useMainService/index.test.tsx b/src/services/hooks/useMainService/index.test.tsx index 11ffa117d3e0e3326451f05f93b88eac86307218..51f48718d976f405d5dc26b78d9b40f75e353f82 100644 --- a/src/services/hooks/useMainService/index.test.tsx +++ b/src/services/hooks/useMainService/index.test.tsx @@ -324,5 +324,38 @@ describe('Test API that needs token', () => { }); expect(result.status).toBe(200); }); + + test('Fetch Statistics within specific timerange', async () => { + const response = { + "district": { + "Beji": { + "positive": 4, + "negative": 1, + "undetermined": 2, + "total": 7 + }, + "Bojongsari": { + "positive": 0, + "negative": 1, + "undetermined": 2, + "total": 3 + } + } + } + + mockedAxios.request.mockImplementationOnce( + () => + new Promise((resolve) => { + resolve({ + status: 200, + data: response + }); + }) + ); + + const result = await withAuth.fetchStatisticsByDate("start_date", "end_date"); + expect(result.status).toBe(200); + expect(result.data).toMatchObject(response); + }) }); }); diff --git a/src/services/hooks/useMainService/index.tsx b/src/services/hooks/useMainService/index.tsx index af1ea1ab8d6be42f265cc5317500cfe2e8fd341c..d35b7f5bc3276b25fa866e13413599a96e50247d 100644 --- a/src/services/hooks/useMainService/index.tsx +++ b/src/services/hooks/useMainService/index.tsx @@ -1,7 +1,7 @@ import { createEndpoint } from 'helper'; import axios from 'axios'; -import { FilterType, StatisticType, AgeKeyProps } from 'contexts/AppContext/types'; +import { FilterType, StatisticType, AgeKeyProps, ExportCSVPayload, SexKeyProps, DistrictKeyProps } from 'contexts/AppContext/types'; const API_MAIN_URL = process.env.REACT_APP_API_MAIN_URL; @@ -255,17 +255,20 @@ export default function useMainService(token: string) { } async function filterExportCSV( - filterType: StatisticType, key: string, ageKey: AgeKeyProps + filter: ExportCSVPayload ) { - let urlQuery = ''; - switch (filterType) { + let urlQuery = '?'; + switch (filter.filterType) { case StatisticType.Sex: - urlQuery = `?is_male=${key === 'male' ? 'true' : 'false'}`; + const sexKey = filter.filters as SexKeyProps; + urlQuery = `?is_male=${sexKey.isMale}`; break; case StatisticType.District: - urlQuery = `?district=${key}`; + const districtKey = filter.filters as DistrictKeyProps; + urlQuery = `?district=${districtKey.district}`; break; case StatisticType.Age: + const ageKey = filter.filters as AgeKeyProps; urlQuery= `?min_age=${ageKey.minAge}&max_age=${ageKey.maxAge}`; } const endpoint = END_POINTS.EXPORTABLES([`investigation-cases-csv${urlQuery}`]); @@ -288,6 +291,14 @@ export default function useMainService(token: string) { ); } + async function fetchStatisticsByDate( + start_date: string, end_date: string + ) { + let urlQuery = `?start-date=${start_date}&end-date=${end_date}`; + const endpoint = END_POINTS.EXPORTABLES([`${urlQuery}`]); + return fetchWithAuthentication(endpoint.slice(0, -1), Method.GET); + } + return { // Authentication login, @@ -318,6 +329,7 @@ export default function useMainService(token: string) { fetchStatistics, exportCSV, filterExportCSV, + fetchStatisticsByDate, // Log getLog, };