From 9fba19bd36743cc61a22e81cfe8363a1a0c00873 Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 8 Sep 2023 14:58:30 +0300 Subject: [PATCH] add csv extract functionality --- package-lock.json | 30 +++++++++ package.json | 2 + .../activeMembers/ActiveMemberBreakdown.tsx | 59 ++++++++++++++++-- .../DisengagedMembersCompositionBreakdown.tsx | 59 ++++++++++++++++-- .../OnboardingMembersBreakdown.tsx | 59 ++++++++++++++++-- src/helpers/csvHelper.tsx | 62 +++++++++++++++++++ src/store/slices/breakdownsSlice.ts | 18 +++++- src/store/types/IBreakdown.ts | 9 ++- 8 files changed, 274 insertions(+), 24 deletions(-) create mode 100644 src/helpers/csvHelper.tsx diff --git a/package-lock.json b/package-lock.json index 45be1465..f20f5255 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "moment": "^2.29.4", "moment-timezone": "^0.5.38", "next": "13.0.2", + "papaparse": "^5.4.1", "prettier": "^2.8.2", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -61,6 +62,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/d3-force": "^3.0.4", + "@types/papaparse": "^5.3.8", "autoprefixer": "^10.4.13", "babel-jest": "^29.5.0", "identity-obj-proxy": "^3.0.0", @@ -3136,6 +3138,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" }, + "node_modules/@types/papaparse": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.8.tgz", + "integrity": "sha512-ArKIEOOWULbhi53wkAiRy1ze4wvrTfhpAj7Yfzva+EkmX2sV8PpFB+xqzJfzXNzK4me95FJH9QZt5NXFVGzOoQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -9941,6 +9952,11 @@ "node": ">=6" } }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -14380,6 +14396,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" }, + "@types/papaparse": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.8.tgz", + "integrity": "sha512-ArKIEOOWULbhi53wkAiRy1ze4wvrTfhpAj7Yfzva+EkmX2sV8PpFB+xqzJfzXNzK4me95FJH9QZt5NXFVGzOoQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -19344,6 +19369,11 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/package.json b/package.json index 947806a5..0f09a62c 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "moment": "^2.29.4", "moment-timezone": "^0.5.38", "next": "13.0.2", + "papaparse": "^5.4.1", "prettier": "^2.8.2", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -65,6 +66,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/d3-force": "^3.0.4", + "@types/papaparse": "^5.3.8", "autoprefixer": "^10.4.13", "babel-jest": "^29.5.0", "identity-obj-proxy": "^3.0.0", diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx index f8d23633..510d11bb 100644 --- a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -10,6 +10,12 @@ import { import CustomPagination from '../CustomPagination'; import CustomButton from '../../../../global/CustomButton'; import clsx from 'clsx'; +import { FaFileCsv } from 'react-icons/fa'; +import { Button } from '@mui/material'; +import { + convertToCSV, + downloadCSVFile, +} from '../../../../../helpers/csvHelper'; const columns: Column[] = [ { id: 'username', label: 'Name' }, @@ -109,15 +115,56 @@ export default function ActiveMemberBreakdown() { toggleExpanded(!isExpanded); }; + const handleDownloadCSV = async () => { + if (!guild) { + return; + } + + try { + const limit = fetchedData.totalResults; + + const { results } = await getActiveMemberCompositionTable( + guild.guildId, + activityComposition, + roles, + username, + sortBy, + page, + limit + ); + + if (results && Array.isArray(results)) { + const csv = convertToCSV(results); + downloadCSVFile(csv, 'activeMemberComposition.csv'); + } else { + console.error('Received data is not valid for CSV conversion.'); + } + } catch (error) { + console.error('Error while fetching data and downloading CSV:', error); + } + }; + return ( <>
-

- Members breakdown -

+
+

+ Members breakdown +

+ +
{ + if (!guild) { + return; + } + + try { + const limit = fetchedData.totalResults; + + const { results } = await getDisengagedMembersCompositionTable( + guild.guildId, + disengagedComposition, + roles, + username, + sortBy, + page, + limit + ); + + if (results && Array.isArray(results)) { + const csv = convertToCSV(results); + downloadCSVFile(csv, 'disengagedMemberComposition.csv'); + } else { + console.error('Received data is not valid for CSV conversion.'); + } + } catch (error) { + console.error('Error while fetching data and downloading CSV:', error); + } + }; + return ( <>
-

- Members breakdown -

+
+

+ Members breakdown +

+ +
{ + if (!guild) { + return; + } + + try { + const limit = fetchedData.totalResults; + + const { results } = await getOnboardingMemberCompositionTable( + guild.guildId, + onboardingComposition, + roles, + username, + sortBy, + page, + limit + ); + + if (results && Array.isArray(results)) { + const csv = convertToCSV(results); + downloadCSVFile(csv, 'onboardingMemberComposition.csv'); + } else { + console.error('Received data is not valid for CSV conversion.'); + } + } catch (error) { + console.error('Error while fetching data and downloading CSV:', error); + } + }; + return ( <>
-

- Members breakdown -

+
+

+ Members breakdown +

+ +
{ + let flattenedItem: { [key: string]: string | number } = {}; + + for (const key in item) { + if (item.hasOwnProperty(key)) { + // If the property is an array containing objects + if ( + Array.isArray(item[key]) && + item[key].length > 0 && + typeof item[key][0] === 'object' + ) { + flattenedItem[key] = item[key] + .map((subItem: any) => + Object.entries(subItem) + .map(([subKey, subValue]) => `${subKey}: ${subValue}`) + .join(', ') + ) + .join(' , '); + } else { + flattenedItem[key] = item[key]; + } + } + } + + return flattenedItem; + }); + + return Papa.unparse(flattenedData); +} + +/** + * Download the given CSV data as a file. + * @param {string} csvData The CSV data to download. + * @param {string} filename The name for the downloaded file. + */ +function downloadCSVFile(csvData: string, filename = 'data.csv'): void { + const blob = new Blob([csvData], { type: 'text/csv' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); +} + +export { convertToCSV, downloadCSVFile }; diff --git a/src/store/slices/breakdownsSlice.ts b/src/store/slices/breakdownsSlice.ts index 3c10583e..4c37ca5a 100644 --- a/src/store/slices/breakdownsSlice.ts +++ b/src/store/slices/breakdownsSlice.ts @@ -14,7 +14,8 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ roles: string[], username?: string, sortBy?: string, - page?: number + page?: number, + limit?: number ) => { try { set(() => ({ isActiveMembersBreakdownLoading: true })); @@ -29,6 +30,9 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ if (page) { params.append('page', page.toString()); } + if (limit) { + params.append('limit', limit.toString()); + } if (sortBy) { params.append('sortBy', `joinedAt:${sortBy}`); } @@ -62,7 +66,8 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ roles: string[], username?: string, sortBy?: string, - page?: number + page?: number, + limit?: number ) => { try { set(() => ({ isOnboardingMembersBreakdownLoading: true })); @@ -77,6 +82,9 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ if (page) { params.append('page', page.toString()); } + if (limit) { + params.append('limit', limit.toString()); + } if (sortBy) { params.append('sortBy', `joinedAt:${sortBy}`); } @@ -110,7 +118,8 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ roles: string[], username?: string, sortBy?: string, - page?: number + page?: number, + limit?: number ) => { try { set(() => ({ isDisengagedMembersCompositionBreakdownLoading: true })); @@ -125,6 +134,9 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ if (page) { params.append('page', page.toString()); } + if (limit) { + params.append('limit', limit.toString()); + } if (sortBy) { params.append('sortBy', `joinedAt:${sortBy}`); } diff --git a/src/store/types/IBreakdown.ts b/src/store/types/IBreakdown.ts index 2aa628d5..e008bfd5 100644 --- a/src/store/types/IBreakdown.ts +++ b/src/store/types/IBreakdown.ts @@ -10,7 +10,8 @@ export default interface IBreakdown { roles: string[], username?: string, sortBy?: string, - page?: number + page?: number, + limit?: number ) => any; getOnboardingMemberCompositionTable: ( guild_id: string, @@ -18,7 +19,8 @@ export default interface IBreakdown { roles: string[], username?: string, sortBy?: string, - page?: number + page?: number, + limit?: number ) => any; getDisengagedMembersCompositionTable: ( guild_id: string, @@ -26,7 +28,8 @@ export default interface IBreakdown { roles: string[], username?: string, sortBy?: string, - page?: number + page?: number, + limit?: number ) => any; getRoles: (guild_id: string) => any; }