From 73fbe7d4b9acc02411438acf815768ac07698659 Mon Sep 17 00:00:00 2001 From: ter1203 Date: Tue, 8 Oct 2024 13:21:20 -0300 Subject: [PATCH 01/12] feat: adjust active account list table, activated contacts card --- .../CardActiveAccount.js | 72 ++++++++++ .../ProspectingActiveAccount/TimeLine.js | 58 ++++++++ client/src/pages/Prospecting/Prospecting.js | 132 ++++++++++++------ 3 files changed, 217 insertions(+), 45 deletions(-) create mode 100644 client/src/components/ProspectingActiveAccount/CardActiveAccount.js create mode 100644 client/src/components/ProspectingActiveAccount/TimeLine.js diff --git a/client/src/components/ProspectingActiveAccount/CardActiveAccount.js b/client/src/components/ProspectingActiveAccount/CardActiveAccount.js new file mode 100644 index 0000000..6ac6eba --- /dev/null +++ b/client/src/components/ProspectingActiveAccount/CardActiveAccount.js @@ -0,0 +1,72 @@ +import React from 'react'; +import { Card, CardContent, Typography, Divider, Box } from '@mui/material'; + +const ActivatedContacts = () => { + const contacts = [ + { name: 'Contact Name 1', status: 'Activated', activities: 4 }, + { name: 'Contact Name 2', status: 'Engaged', activities: 2 }, + { name: 'Contact Name 3', status: 'Activated', activities: 0 }, + { name: 'Contact Name 4', status: 'Activated', activities: 4 }, + { name: 'Contact Name 5', status: 'Engaged', activities: 2 }, + { name: 'Contact Name 6', status: 'Activated', activities: 0 }, + ]; + + return ( + + + + Activated Contacts + + + {contacts.map((contact, index) => ( + + + {contact.name} + + + + STATUS: + + {contact.status} + + + + TOTAL ACTIVITIES: + + {contact.activities} + + + + {index < contacts.length - 1 && } + + ))} + + + ); +}; + +export default ActivatedContacts; diff --git a/client/src/components/ProspectingActiveAccount/TimeLine.js b/client/src/components/ProspectingActiveAccount/TimeLine.js new file mode 100644 index 0000000..fd659c9 --- /dev/null +++ b/client/src/components/ProspectingActiveAccount/TimeLine.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { Box, Typography } from '@mui/material'; +import { Email, Phone, Handshake, BusinessCenter } from '@mui/icons-material'; + +// Example of event icons based on event type (message, call, etc.) +const iconMapper = { + message: , + call: , + meeting: , + opportunity: +}; + +const TimelineEvent = ({ date, time, icon, title }) => { + return ( + + {iconMapper[icon]} + {`${date}`} + {title} + {time} + + ); +}; + +const Timeline = () => { + const data = [ + { date: '11/02/23', time: '12:00', icon: 'message', title: 'Activated', style: 'A' }, + { date: '11/09/23', time: '14:00', icon: 'call', title: 'Engaged', style: 'B' }, + { date: '11/17/23', time: '16:00', icon: 'meeting', title: 'Meeting Set', style: 'C' }, + { date: '11/24/23', time: '18:00', icon: 'opportunity', title: 'Opportunity', style: 'D' } + ]; + + return ( + + {data.map((event, index) => ( + + + {index !== data.length - 1 && } + + ))} + + ); +}; + +// Sample data + +const TimeLine = () => ( +
+ Timeline + +
+); + +export default TimeLine; diff --git a/client/src/pages/Prospecting/Prospecting.js b/client/src/pages/Prospecting/Prospecting.js index 030f558..60826e4 100644 --- a/client/src/pages/Prospecting/Prospecting.js +++ b/client/src/pages/Prospecting/Prospecting.js @@ -18,6 +18,8 @@ import { Switch, styled, Stack, + Grid, + Card } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; import DataFilter from "../../components/DataFilter/DataFilter"; @@ -32,8 +34,9 @@ import { } from "src/components/Api/Api"; import CustomTable from "../../components/CustomTable/CustomTable"; import ProspectingMetadataOverview from "../../components/ProspectingMetadataOverview/ProspectingMetadataOverview"; -import ProspectingEffortTimeline from "../../components/ProspectingEffortTimeline/ProspectingEffortTimeline"; +// import ProspectingEffortTimeline from "../../components/ProspectingEffortTimeline/ProspectingEffortTimeline"; import CustomSelect from "src/components/CustomSelect/CustomSelect"; +import CardActiveAccount from "../../components/ProspectingActiveAccount/CardActiveAccount" /** * @typedef {import('types').Activation} Activation */ @@ -43,6 +46,7 @@ import ProspectingSummary from "./ProspectingSummary"; import LoadingComponent from "../../components/LoadingComponent/LoadingComponent"; import FreeTrialRibbon from "../../components/FreeTrialRibbon/FreeTrialRibbon"; import { debounce } from "lodash"; // Make sure to import lodash or use a custom debounce function +import TimeLine from "src/components/ProspectingActiveAccount/TimeLine"; const AntSwitch = styled(Switch)(({ theme }) => ({ width: 28, @@ -541,56 +545,94 @@ const Prospecting = () => { ) : ( <> - ({ - ...item, - "account.name": ( - - {item.account?.name || "N/A"} - - ), - "opportunity.name": item.opportunity ? ( - - {item.opportunity.name || "N/A"} - - ) : ( - "N/A" - ), - })), - selectedIds: new Set(), - availableColumns: tableColumns, - }} - paginationConfig={{ - type: "server-side", - totalItems: totalItems, - page: page, - rowsPerPage: rowsPerPage, - onPageChange: handlePageChange, - onRowsPerPageChange: handleRowsPerPageChange, - }} - onRowClick={handleRowClick} - onColumnsChange={handleColumnsChange} - isLoading={tableLoading} - onSearch={handleSearch} - /> + + + + + + Active Accounts List + + ({ + ...item, + "account.name": ( + + {item.account?.name || "N/A"} + + ), + "opportunity.name": item.opportunity ? ( + + {item.opportunity.name || "N/A"} + + ) : ( + "N/A" + ), + })), + selectedIds: new Set(), + availableColumns: tableColumns, + }} + paginationConfig={{ + type: "server-side", + totalItems: totalItems, + page: page, + rowsPerPage: rowsPerPage, + onPageChange: handlePageChange, + onRowsPerPageChange: handleRowsPerPageChange, + }} + onRowClick={handleRowClick} + onColumnsChange={handleColumnsChange} + isLoading={tableLoading} + onSearch={handleSearch} + /> + + + + + + + + +
+ + +
+ + {selectedActivation && ( - + {/* + /> */} Date: Tue, 8 Oct 2024 21:14:00 -0300 Subject: [PATCH 02/12] feat: timeline component --- .../ProspectingActiveAccount/TimeLine.js | 159 ++++++++++++------ client/src/pages/Prospecting/Prospecting.js | 29 ++-- 2 files changed, 129 insertions(+), 59 deletions(-) diff --git a/client/src/components/ProspectingActiveAccount/TimeLine.js b/client/src/components/ProspectingActiveAccount/TimeLine.js index fd659c9..c548ac0 100644 --- a/client/src/components/ProspectingActiveAccount/TimeLine.js +++ b/client/src/components/ProspectingActiveAccount/TimeLine.js @@ -1,58 +1,123 @@ import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { Email, Phone, Handshake, BusinessCenter } from '@mui/icons-material'; +import { Typography, Box } from '@mui/material'; +import { AccessTime, Call, Message, Event, BusinessCenter } from '@mui/icons-material'; -// Example of event icons based on event type (message, call, etc.) -const iconMapper = { - message: , - call: , - meeting: , - opportunity: +// Function to map icon names to actual icons +const getIcon = (iconName, color) => { + switch (iconName) { + case 'message': + return ; + case 'call': + return ; + case 'meeting': + return ; + case 'opportunity': + return ; + default: + return ; + } }; -const TimelineEvent = ({ date, time, icon, title }) => { - return ( - - {iconMapper[icon]} - {`${date}`} - {title} - {time} - - ); -}; - -const Timeline = () => { +const MyTimelineComponent = () => { + // Sample data const data = [ - { date: '11/02/23', time: '12:00', icon: 'message', title: 'Activated', style: 'A' }, - { date: '11/09/23', time: '14:00', icon: 'call', title: 'Engaged', style: 'B' }, - { date: '11/17/23', time: '16:00', icon: 'meeting', title: 'Meeting Set', style: 'C' }, - { date: '11/24/23', time: '18:00', icon: 'opportunity', title: 'Opportunity', style: 'D' } + { icon: "message", date: "11/02", color: "#DD4040", format: "top" }, + { icon: "call", date: "11/05", color: "#DD4040", format: "top" }, + { icon: "meeting", date: "11/07", color: "#DD4040", format: "top", line: '6px solid grey', label: "Activated" }, + { icon: "opportunity", date: "11/09", color: "#DD4040", format: "top" }, + { icon: "call", date: "11/13", color: "#7AAD67", format: "top", line: '6px solid grey', label: "Engaged" }, + { icon: "meeting", date: "11/14", color: "#7AAD67", format: "top" }, + { icon: "message", date: "11/17", color: "#7AAD67", format: "top" }, + { icon: "opportunity", date: "11/19", color: "#FF7D2F", format: "top", line: '6px solid grey', label: "Opportunity" }, + { icon: "", date: "11/24", color: "#533AF3", format: "top", }, + ]; return ( - - {data.map((event, index) => ( - - - {index !== data.length - 1 && } - - ))} - - ); -}; +
+ + Timeline + + + [Date] - [Date] + + + {/* Header */} + + + + + Previous
30 days
+
-// Sample data + {/* Timeline */} + + { + data.map((el, index) => ( + + { + el.format == "bottom" && index < data.length && ( + + ) + } + {getIcon(el.icon, el.color)} + { + el.format == "top" && index < data.length && ( + ( -
- Timeline - -
-); + }} + /> + ) + } + {/* Date Below Icon */} + {el.line ? {el.label}
: <>} {el.date}
+
+ )) + } +
+
+
+ ); +}; -export default TimeLine; +export default MyTimelineComponent; diff --git a/client/src/pages/Prospecting/Prospecting.js b/client/src/pages/Prospecting/Prospecting.js index 60826e4..b85a3c8 100644 --- a/client/src/pages/Prospecting/Prospecting.js +++ b/client/src/pages/Prospecting/Prospecting.js @@ -618,23 +618,28 @@ const Prospecting = () => {
-
- - -
- - - {selectedActivation && ( - - - - {/* + + + + {/* */} + - + Date: Thu, 10 Oct 2024 10:11:47 -0300 Subject: [PATCH 03/12] integrate data active contacts --- .../CardActiveAccount.js | 10 +-- client/src/pages/Prospecting/Prospecting.js | 81 +++++++++++++++++-- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/client/src/components/ProspectingActiveAccount/CardActiveAccount.js b/client/src/components/ProspectingActiveAccount/CardActiveAccount.js index 6ac6eba..c621768 100644 --- a/client/src/components/ProspectingActiveAccount/CardActiveAccount.js +++ b/client/src/components/ProspectingActiveAccount/CardActiveAccount.js @@ -1,7 +1,7 @@ import React from 'react'; import { Card, CardContent, Typography, Divider, Box } from '@mui/material'; -const ActivatedContacts = () => { +const ActivatedContacts = ({ data }) => { const contacts = [ { name: 'Contact Name 1', status: 'Activated', activities: 4 }, { name: 'Contact Name 2', status: 'Engaged', activities: 2 }, @@ -38,26 +38,26 @@ const ActivatedContacts = () => { Activated Contacts - {contacts.map((contact, index) => ( + {data.map(({ first_name, last_name }, index) => ( - {contact.name} + {first_name} {last_name} STATUS: - {contact.status} + Activated TOTAL ACTIVITIES: - {contact.activities} + 3 diff --git a/client/src/pages/Prospecting/Prospecting.js b/client/src/pages/Prospecting/Prospecting.js index b85a3c8..14b673f 100644 --- a/client/src/pages/Prospecting/Prospecting.js +++ b/client/src/pages/Prospecting/Prospecting.js @@ -33,14 +33,14 @@ import { getPaginatedProspectingActivities, } from "src/components/Api/Api"; import CustomTable from "../../components/CustomTable/CustomTable"; -import ProspectingMetadataOverview from "../../components/ProspectingMetadataOverview/ProspectingMetadataOverview"; +// import ProspectingMetadataOverview from "../../components/ProspectingMetadataOverview/ProspectingMetadataOverview"; // import ProspectingEffortTimeline from "../../components/ProspectingEffortTimeline/ProspectingEffortTimeline"; import CustomSelect from "src/components/CustomSelect/CustomSelect"; import CardActiveAccount from "../../components/ProspectingActiveAccount/CardActiveAccount" /** * @typedef {import('types').Activation} Activation */ - +import SummaryBarChartCard from 'src/components/SummaryCard/SummaryBarChartCard' import FreeTrialExpired from "../../components/FreeTrialExpired/FreeTrialExpired"; import ProspectingSummary from "./ProspectingSummary"; import LoadingComponent from "../../components/LoadingComponent/LoadingComponent"; @@ -131,6 +131,24 @@ const Prospecting = () => { const [detailedActivationData, setDetailedActivationData] = useState([]); const [tableLoading, setTableLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(""); + const mockData = [ + { + label: "User 1", + value: 6 + }, + { + label: "User 2", + value: 4 + }, + { + label: "User 3", + value: 7 + }, + { + label: "User 4", + value: 2 + } + ] useEffect(() => { async function fetchUserAndInstanceUrl() { @@ -363,6 +381,8 @@ const Prospecting = () => { const [selectedActivation, setSelectedActivation] = useState(null); const handleRowClick = (activation) => { + console.log("This log is for development only. To see your choosen account"); + console.log(activation); setSelectedActivation(activation); }; @@ -614,7 +634,7 @@ const Prospecting = () => { - + @@ -640,10 +660,61 @@ const Prospecting = () => { - + + + + + + + + + + + + + + + + + + + + + + + {/* + /> */} )} From a0e1158c0a7fb5381aee0fe16d934d919136081b Mon Sep 17 00:00:00 2001 From: ter1203 Date: Thu, 10 Oct 2024 13:20:34 -0300 Subject: [PATCH 04/12] feat: integrate data timeline component --- .../ProspectingActiveAccount/TimeLine.js | 100 +++++++++++++----- client/src/pages/Prospecting/Prospecting.js | 4 +- 2 files changed, 74 insertions(+), 30 deletions(-) diff --git a/client/src/components/ProspectingActiveAccount/TimeLine.js b/client/src/components/ProspectingActiveAccount/TimeLine.js index c548ac0..12b6b16 100644 --- a/client/src/components/ProspectingActiveAccount/TimeLine.js +++ b/client/src/components/ProspectingActiveAccount/TimeLine.js @@ -1,37 +1,49 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Typography, Box } from '@mui/material'; import { AccessTime, Call, Message, Event, BusinessCenter } from '@mui/icons-material'; // Function to map icon names to actual icons const getIcon = (iconName, color) => { switch (iconName) { - case 'message': + case 'Message': return ; - case 'call': + case 'Dial': return ; - case 'meeting': + case 'Meeting': return ; - case 'opportunity': + case 'Opportunity': return ; default: return ; } }; -const MyTimelineComponent = () => { +const MyTimelineComponent = ({ tasks }) => { // Sample data - const data = [ - { icon: "message", date: "11/02", color: "#DD4040", format: "top" }, - { icon: "call", date: "11/05", color: "#DD4040", format: "top" }, - { icon: "meeting", date: "11/07", color: "#DD4040", format: "top", line: '6px solid grey', label: "Activated" }, - { icon: "opportunity", date: "11/09", color: "#DD4040", format: "top" }, - { icon: "call", date: "11/13", color: "#7AAD67", format: "top", line: '6px solid grey', label: "Engaged" }, - { icon: "meeting", date: "11/14", color: "#7AAD67", format: "top" }, - { icon: "message", date: "11/17", color: "#7AAD67", format: "top" }, - { icon: "opportunity", date: "11/19", color: "#FF7D2F", format: "top", line: '6px solid grey', label: "Opportunity" }, - { icon: "", date: "11/24", color: "#533AF3", format: "top", }, + const [text, setText] = useState('') + const [data, setData] = useState([]) + const [page, setPage] = useState(1) + const [maxPage, setMaxPage] = useState(1) - ]; + useEffect(() => { + let array = tasks.map((e, index) => { + const date = new Date(e.CreatedDate); + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-indexed + const formattedDate = `${day}/${month}`; + let format = 'top' + let color = '#7AAD67' + let icon = e.Subject + if (e.Priority === "Priority") color = '#DD4040' + let obj = { id: index, icon, date: formattedDate, color, format } + return obj + }) + setText(array[0].date + ' - ' + array[array.length - 1].date) + let maxPage = Math.ceil(array.length / 10) + setPage(maxPage) + setMaxPage(maxPage) + setData(array) + }, []) return (
@@ -55,21 +67,45 @@ const MyTimelineComponent = () => { paddingBottom: 4, color: "#4C4C4C" }}> - [Date] - [Date] + {text} {/* Header */} - - - - - Previous
30 days
-
+ + { + page > 1 && ( + setPage(page - 1)} display="flex" alignItems="center" pb={6} mb={2} mx={2} height="80px"> + + + + Previous + + ) + } {/* Timeline */} + + + + { - data.map((el, index) => ( + data.filter((e, i) => i > (page - 1) * 10 && i <= page * 10).map((el, index) => ( { ) } {/* Date Below Icon */} - {el.line ? {el.label}
: <>} {el.date}
+ {el.line ? {el.label}
: <>} {el.date} {el.id}
)) }
+ { + page < maxPage && ( + setPage(page + 1)} display="flex" alignItems="center" pb={6} mb={2} mx={2} height="80px"> + + + + Next + + ) + }
-
+ ); }; diff --git a/client/src/pages/Prospecting/Prospecting.js b/client/src/pages/Prospecting/Prospecting.js index 14b673f..64818dc 100644 --- a/client/src/pages/Prospecting/Prospecting.js +++ b/client/src/pages/Prospecting/Prospecting.js @@ -381,8 +381,6 @@ const Prospecting = () => { const [selectedActivation, setSelectedActivation] = useState(null); const handleRowClick = (activation) => { - console.log("This log is for development only. To see your choosen account"); - console.log(activation); setSelectedActivation(activation); }; @@ -653,7 +651,7 @@ const Prospecting = () => { > - + {/* */} From 104133291341b66567bb30451306920ea00cb353 Mon Sep 17 00:00:00 2001 From: ter1203 Date: Fri, 11 Oct 2024 14:32:16 -0300 Subject: [PATCH 05/12] refactoring component and integrate data --- .../ProspectingActiveAccount/AccountDetail.js | 97 ++++++++++++ .../CardActiveAccount.js | 36 +++-- .../SelectedAccount.js | 97 ++++++++++++ .../ProspectingActiveAccount/TimeLine.js | 59 +++---- client/src/pages/Prospecting/Prospecting.js | 144 ++---------------- 5 files changed, 260 insertions(+), 173 deletions(-) create mode 100644 client/src/components/ProspectingActiveAccount/AccountDetail.js create mode 100644 client/src/components/ProspectingActiveAccount/SelectedAccount.js diff --git a/client/src/components/ProspectingActiveAccount/AccountDetail.js b/client/src/components/ProspectingActiveAccount/AccountDetail.js new file mode 100644 index 0000000..96955b8 --- /dev/null +++ b/client/src/components/ProspectingActiveAccount/AccountDetail.js @@ -0,0 +1,97 @@ +import { Box, Card, Grid, Typography } from "@mui/material" +import CustomTable from "../CustomTable/CustomTable" +import CardActiveAccount from "./CardActiveAccount" +import { Link } from "react-router-dom" +import { tableColumns } from "../../pages/Prospecting/tableColumns"; +import { useState } from "react"; + +const AccountDetail = ({ detailedActivationData, instanceUrl, totalItems, page, rowsPerPage, handlePageChange, handleRowsPerPageChange, handleRowClick, tableLoading, handleSearch, selectedActivation }) => { + const [columnShows, setColumnShows] = useState( + localStorage.getItem("activationColumnShow") + ? JSON.parse(localStorage.getItem("activationColumnShow")) + : tableColumns + ); + + const handleColumnsChange = (newColumns) => { + setColumnShows(newColumns); + localStorage.setItem("activationColumnShow", JSON.stringify(newColumns)); + }; + + return ( + + + + + + Active Accounts List + + ({ + ...item, + "account.name": ( + + {item.account?.name || "N/A"} + + ), + "opportunity.name": item.opportunity ? ( + + {item.opportunity.name || "N/A"} + + ) : ( + "N/A" + ), + })), + selectedIds: new Set(), + availableColumns: tableColumns, + }} + paginationConfig={{ + type: "server-side", + totalItems: totalItems, + page: page, + rowsPerPage: rowsPerPage, + onPageChange: handlePageChange, + onRowsPerPageChange: handleRowsPerPageChange, + }} + onRowClick={handleRowClick} + onColumnsChange={handleColumnsChange} + isLoading={tableLoading} + onSearch={handleSearch} + /> + + + + + + + + ) +} + + +export default AccountDetail \ No newline at end of file diff --git a/client/src/components/ProspectingActiveAccount/CardActiveAccount.js b/client/src/components/ProspectingActiveAccount/CardActiveAccount.js index c621768..c1ccd39 100644 --- a/client/src/components/ProspectingActiveAccount/CardActiveAccount.js +++ b/client/src/components/ProspectingActiveAccount/CardActiveAccount.js @@ -1,15 +1,25 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Card, CardContent, Typography, Divider, Box } from '@mui/material'; const ActivatedContacts = ({ data }) => { - const contacts = [ - { name: 'Contact Name 1', status: 'Activated', activities: 4 }, - { name: 'Contact Name 2', status: 'Engaged', activities: 2 }, - { name: 'Contact Name 3', status: 'Activated', activities: 0 }, - { name: 'Contact Name 4', status: 'Activated', activities: 4 }, - { name: 'Contact Name 5', status: 'Engaged', activities: 2 }, - { name: 'Contact Name 6', status: 'Activated', activities: 0 }, - ]; + const [contacts, setContacts] = useState([]) + useEffect(() => { + if (data) { + let raw = data.active_contacts.map(e => { + let { id, first_name, last_name } = e + let len = data.tasks.filter(el => el.Contact.id === id) + let obj = { + id, + name: first_name + " " + last_name, + total: len.length, + data: len, + status: len[len.length - 1].Status + } + return obj + }) + setContacts(raw) + } + }, [data]) return ( { Activated Contacts - {data.map(({ first_name, last_name }, index) => ( + {contacts.map((el, index) => ( - {first_name} {last_name} + {el.name} STATUS: - Activated + {el.status} TOTAL ACTIVITIES: - 3 + {el.total} diff --git a/client/src/components/ProspectingActiveAccount/SelectedAccount.js b/client/src/components/ProspectingActiveAccount/SelectedAccount.js new file mode 100644 index 0000000..d8d2d27 --- /dev/null +++ b/client/src/components/ProspectingActiveAccount/SelectedAccount.js @@ -0,0 +1,97 @@ +import Timeline from "./TimeLine" +import { Box, Card, Grid } from "@mui/material" +import SummaryBarChartCard from '../../components/SummaryCard/SummaryBarChartCard' + +function SelectedAccount({ selectedActivation }) { + const mockData = [ + { + label: "User 1", + value: 6 + }, + { + label: "User 2", + value: 4 + }, + { + label: "User 3", + value: 7 + }, + { + label: "User 4", + value: 2 + } + ] + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default SelectedAccount \ No newline at end of file diff --git a/client/src/components/ProspectingActiveAccount/TimeLine.js b/client/src/components/ProspectingActiveAccount/TimeLine.js index 12b6b16..03dcc79 100644 --- a/client/src/components/ProspectingActiveAccount/TimeLine.js +++ b/client/src/components/ProspectingActiveAccount/TimeLine.js @@ -39,7 +39,7 @@ const MyTimelineComponent = ({ tasks }) => { return obj }) setText(array[0].date + ' - ' + array[array.length - 1].date) - let maxPage = Math.ceil(array.length / 10) + let maxPage = Math.ceil(array.length / 8) setPage(maxPage) setMaxPage(maxPage) setData(array) @@ -71,20 +71,22 @@ const MyTimelineComponent = ({ tasks }) => { {/* Header */} + + { + page > 1 && ( + setPage(page - 1)} display="flex" alignItems="center" pb={6} mb={2} mx={2} height="80px"> + + + + Previous + + ) + } + - { - page > 1 && ( - setPage(page - 1)} display="flex" alignItems="center" pb={6} mb={2} mx={2} height="80px"> - - - - Previous - - ) - } {/* Timeline */} - + { marginTop: '34px', marginBottom: '8px', borderTop: '4px solid gray', - width: '100px' + width: '90px' }} /> { - data.filter((e, i) => i > (page - 1) * 10 && i <= page * 10).map((el, index) => ( + data.filter((e, i) => i > (page - 1) * 8 && i <= page * 8).map((el, index) => ( { marginTop: '64px', // Adjust spacing between icon and dashed line marginBottom: '8px', // Adjust spacing at the bottom borderTop: '4px solid gray', - width: '100px' + width: '90px' }} /> ) @@ -139,28 +141,31 @@ const MyTimelineComponent = ({ tasks }) => { marginTop: '8px', // Adjust spacing between icon and dashed line marginBottom: '98px', // Adjust spacing at the bottom borderBottom: '4px solid gray', - width: '100px' + width: '90px' }} /> ) } {/* Date Below Icon */} - {el.line ? {el.label}
: <>} {el.date} {el.id}
+ {el.line ? {el.label}
: <>} {el.date}
)) }
- { - page < maxPage && ( - setPage(page + 1)} display="flex" alignItems="center" pb={6} mb={2} mx={2} height="80px"> - - - - Next - - ) - } + + { + page < maxPage && ( + setPage(page + 1)} display="flex" alignItems="center" gap="10px" pb={6} mb={2} mx={2} height="80px"> + Next + + + + + ) + } + +
); diff --git a/client/src/pages/Prospecting/Prospecting.js b/client/src/pages/Prospecting/Prospecting.js index 5849940..9171bfe 100644 --- a/client/src/pages/Prospecting/Prospecting.js +++ b/client/src/pages/Prospecting/Prospecting.js @@ -13,7 +13,6 @@ import { FormControl, IconButton, Tooltip, - Link, Typography, Switch, styled, @@ -21,7 +20,6 @@ import { } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; import DataFilter from "../../components/DataFilter/DataFilter"; -import { tableColumns } from "./tableColumns"; import { fetchProspectingActivities, getInstanceUrl, @@ -30,21 +28,17 @@ import { getUserTimezone, getPaginatedProspectingActivities, } from "src/components/Api/Api"; -import CustomTable from "../../components/CustomTable/CustomTable"; -// import ProspectingMetadataOverview from "../../components/ProspectingMetadataOverview/ProspectingMetadataOverview"; -// import ProspectingEffortTimeline from "../../components/ProspectingEffortTimeline/ProspectingEffortTimeline"; +import AccountDetail from "../../components/ProspectingActiveAccount/AccountDetail" import CustomSelect from "src/components/CustomSelect/CustomSelect"; -import CardActiveAccount from "../../components/ProspectingActiveAccount/CardActiveAccount"; /** * @typedef {import('types').Activation} Activation */ -import SummaryBarChartCard from "src/components/SummaryCard/SummaryBarChartCard"; import FreeTrialExpired from "../../components/FreeTrialExpired/FreeTrialExpired"; import ProspectingSummary from "./ProspectingSummary"; import LoadingComponent from "../../components/LoadingComponent/LoadingComponent"; import FreeTrialRibbon from "../../components/FreeTrialRibbon/FreeTrialRibbon"; import { debounce } from "lodash"; // Make sure to import lodash or use a custom debounce function -import TimeLine from "src/components/ProspectingActiveAccount/TimeLine"; +import SelectedAccount from "src/components/ProspectingActiveAccount/SelectedAccount"; const AntSwitch = styled(Switch)(({ theme }) => ({ width: 28, @@ -104,34 +98,21 @@ const Prospecting = () => { const [rawData, setRawData] = useState([]); const inFlightRef = useRef(false); const navigate = useNavigate(); - const [dataFilter, setDataFilter] = useState(null); const [originalRawData, setOriginalRawData] = useState([]); - const [columnShows, setColumnShows] = useState( - localStorage.getItem("activationColumnShow") - ? JSON.parse(localStorage.getItem("activationColumnShow")) - : tableColumns - ); - const [loggedInUser, setLoggedInUser] = useState(null); const [userLoading, setUserLoading] = useState(true); const [userError, setUserError] = useState(null); - const [instanceUrl, setInstanceUrl] = useState(""); const [urlLoading, setUrlLoading] = useState(true); const [urlError, setUrlError] = useState(null); - const [userTimezone, setUserTimezone] = useState(""); - const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); const [totalItems, setTotalItems] = useState(0); const [detailedActivationData, setDetailedActivationData] = useState([]); const [tableLoading, setTableLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(""); - const [sortColumn, setSortColumn] = useState(""); - /** @type {["asc" | "desc" | undefined, Function]} */ - const [sortOrder, setSortOrder] = useState("asc"); useEffect(() => { async function fetchUserAndInstanceUrl() { @@ -287,13 +268,7 @@ const Prospecting = () => { }, [originalRawData, dataFilter]); const fetchPaginatedData = useCallback( - async ( - newPage, - newRowsPerPage, - newSearchTerm, - newSortColumn, - newSortOrder - ) => { + async (newPage, newRowsPerPage, newSearchTerm) => { setTableLoading(true); try { const filteredIds = filteredData.map((item) => item.id); @@ -301,9 +276,7 @@ const Prospecting = () => { filteredIds, newPage, newRowsPerPage, - newSearchTerm, - newSortColumn, - newSortOrder + newSearchTerm ); if (response.statusCode === 200 && response.success) { setDetailedActivationData(response.data[0].raw_data || []); @@ -316,7 +289,7 @@ const Prospecting = () => { } catch (err) { setError(`An error occurred while fetching data: ${err.message}`); console.error("Error details:", err); - throw err; + throw err; // Add this line to ensure the error is propagated } finally { setTableLoading(false); } @@ -337,13 +310,7 @@ const Prospecting = () => { useEffect(() => { if (filteredData.length > 0) { - debouncedFetchPaginatedData( - page, - rowsPerPage, - searchTerm, - sortColumn, - sortOrder - ); + debouncedFetchPaginatedData(page, rowsPerPage, searchTerm); } }, [ debouncedFetchPaginatedData, @@ -351,8 +318,6 @@ const Prospecting = () => { rowsPerPage, filteredData, searchTerm, - sortColumn, - sortOrder, ]); const handlePageChange = (newPage, newRowsPerPage) => { @@ -383,11 +348,6 @@ const Prospecting = () => { setSelectedActivation(activation); }; - const handleColumnsChange = (newColumns) => { - setColumnShows(newColumns); - localStorage.setItem("activationColumnShow", JSON.stringify(newColumns)); - }; - useEffect(() => { const fetchFilteredSummary = async () => { setSummaryLoading(true); @@ -424,14 +384,7 @@ const Prospecting = () => { const handleSearch = (newSearchTerm) => { setSearchTerm(newSearchTerm); setPage(0); // Reset to first page when searching - fetchPaginatedData(0, rowsPerPage, newSearchTerm, sortColumn, sortOrder); - }; - - const handleSort = (columnId, order) => { - setSortColumn(columnId); - setSortOrder(order); - setPage(0); // Reset to first page when sorting - fetchPaginatedData(0, rowsPerPage, searchTerm, columnId, order); + fetchPaginatedData(0, rowsPerPage, newSearchTerm); }; if (loading || summaryLoading || userLoading || urlLoading) { @@ -481,13 +434,7 @@ const Prospecting = () => { }} > - + { - {error ? ( {error} ) : isSummary && !summaryLoading ? ( ) : ( <> - ({ - ...item, - "account.name": ( - - {item.account?.name || "N/A"} - - ), - "opportunity.name": item.opportunity ? ( - - {item.opportunity.name || "N/A"} - - ) : ( - "N/A" - ), - })), - selectedIds: new Set(), - availableColumns: tableColumns, - }} - paginationConfig={{ - type: "server-side", - totalItems: totalItems, - page: page, - rowsPerPage: rowsPerPage, - onPageChange: handlePageChange, - onRowsPerPageChange: handleRowsPerPageChange, - }} - sortConfig={{ - columnId: sortColumn, - order: sortOrder, - onSort: handleSort, - }} - onRowClick={handleRowClick} - onColumnsChange={handleColumnsChange} - isLoading={tableLoading} - onSearch={handleSearch} - /> - - {selectedActivation && ( - - - - - - - - - )} + + {selectedActivation && ()} )}
- {loggedInUser?.status === "not paid" && freeTrialDaysLeft > 0 && ( )} @@ -655,4 +533,4 @@ const Prospecting = () => { ); }; -export default Prospecting; +export default Prospecting; \ No newline at end of file From bab5721fef7f7819d07c702656eeaeb7b544a4ae Mon Sep 17 00:00:00 2001 From: Nickz22 Date: Sat, 12 Oct 2024 18:15:02 -0500 Subject: [PATCH 06/12] effort and outcomes resources --- client/src/components/Api/Api.js | 40 +- client/src/pages/Prospecting/Prospecting.js | 1005 ++++++++++--------- server/app/data_models.py | 4 +- server/app/helpers/activation_helper.py | 64 +- server/app/routes.py | 32 +- server/app/utils.py | 2 +- 6 files changed, 647 insertions(+), 500 deletions(-) diff --git a/client/src/components/Api/Api.js b/client/src/components/Api/Api.js index 76eb061..d24842a 100644 --- a/client/src/components/Api/Api.js +++ b/client/src/components/Api/Api.js @@ -235,17 +235,6 @@ export const fetchEventFilterFields = async () => { return { ...response.data, statusCode: response.status }; }; -/** - * Fetches Salesforce users from the Salesforce API - * @returns {Promise} - */ -export const fetchSalesforceUsers = async () => { - const response = await api.get("/get_salesforce_users", { - validateStatus: () => true, - }); - return { ...response.data, statusCode: response.status }; -}; - /** * Fetches task query count from the Salesforce API * @param {Object} criteria - The criteria for the query @@ -286,6 +275,35 @@ export const fetchJwt = async () => { return { ...response.data, statusCode: response.status }; }; +/** + * Fetches Salesforce users from the Salesforce API + * @returns {Promise} + */ +export const fetchSalesforceUsers = async () => { + const response = await api.get("/get_salesforce_users", { + validateStatus: () => true, + }); + return { ...response.data, statusCode: response.status }; +}; + +/** + * Fetches the Salesforce users who have been designated as team members in settings + * @returns {Promise} + */ +export const fetchSalesforceTeam = async () => { + const response = await api.post( + "/get_salesforce_team", + { + headers: { + "Content-Type": "application/json", + "X-Session-Token": localStorage.getItem("sessionToken"), + }, + validateStatus: () => true, + } + ); + return { ...response.data, statusCode: response.status }; +}; + /** * Fetches Salesforce tasks from the Salesforce API * @param {string[]} userIds diff --git a/client/src/pages/Prospecting/Prospecting.js b/client/src/pages/Prospecting/Prospecting.js index 9171bfe..55513b3 100644 --- a/client/src/pages/Prospecting/Prospecting.js +++ b/client/src/pages/Prospecting/Prospecting.js @@ -1,34 +1,35 @@ import React, { - useState, - useEffect, - useRef, - useCallback, - useMemo, + useState, + useEffect, + useRef, + useCallback, + useMemo, } from "react"; import "./Prospecting.css"; import { useNavigate } from "react-router-dom"; import { - Box, - Alert, - FormControl, - IconButton, - Tooltip, - Typography, - Switch, - styled, - Stack, + Box, + Alert, + FormControl, + IconButton, + Tooltip, + Typography, + Switch, + styled, + Stack, } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; import DataFilter from "../../components/DataFilter/DataFilter"; import { - fetchProspectingActivities, - getInstanceUrl, - processNewProspectingActivity, - getLoggedInUser, - getUserTimezone, - getPaginatedProspectingActivities, + fetchProspectingActivities, + getInstanceUrl, + processNewProspectingActivity, + getLoggedInUser, + getUserTimezone, + getPaginatedProspectingActivities, + fetchSalesforceTeam, } from "src/components/Api/Api"; -import AccountDetail from "../../components/ProspectingActiveAccount/AccountDetail" +import AccountDetail from "../../components/ProspectingActiveAccount/AccountDetail"; import CustomSelect from "src/components/CustomSelect/CustomSelect"; /** * @typedef {import('types').Activation} Activation @@ -41,496 +42,540 @@ import { debounce } from "lodash"; // Make sure to import lodash or use a custom import SelectedAccount from "src/components/ProspectingActiveAccount/SelectedAccount"; const AntSwitch = styled(Switch)(({ theme }) => ({ - width: 28, - height: 16, - padding: 0, - display: "flex", - "&:active": { - "& .MuiSwitch-thumb": { - width: 15, + width: 28, + height: 16, + padding: 0, + display: "flex", + "&:active": { + "& .MuiSwitch-thumb": { + width: 15, + }, + "& .MuiSwitch-switchBase.Mui-checked": { + transform: "translateX(9px)", + }, + }, + "& .MuiSwitch-switchBase": { + padding: 2, + "&.Mui-checked": { + transform: "translateX(12px)", + color: "#fff", + "& + .MuiSwitch-track": { + opacity: 1, + backgroundColor: "#1890ff", + ...theme.applyStyles("dark", { + backgroundColor: "#177ddc", + }), + }, + }, }, - "& .MuiSwitch-switchBase.Mui-checked": { - transform: "translateX(9px)", + "& .MuiSwitch-thumb": { + boxShadow: "0 2px 4px 0 rgb(0 35 11 / 20%)", + width: 12, + height: 12, + borderRadius: 6, + transition: theme.transitions.create(["width"], { + duration: 200, + }), }, - }, - "& .MuiSwitch-switchBase": { - padding: 2, - "&.Mui-checked": { - transform: "translateX(12px)", - color: "#fff", - "& + .MuiSwitch-track": { + "& .MuiSwitch-track": { + borderRadius: 16 / 2, opacity: 1, - backgroundColor: "#1890ff", + backgroundColor: "rgba(0,0,0,.25)", + boxSizing: "border-box", ...theme.applyStyles("dark", { - backgroundColor: "#177ddc", + backgroundColor: "rgba(255,255,255,.35)", }), - }, }, - }, - "& .MuiSwitch-thumb": { - boxShadow: "0 2px 4px 0 rgb(0 35 11 / 20%)", - width: 12, - height: 12, - borderRadius: 6, - transition: theme.transitions.create(["width"], { - duration: 200, - }), - }, - "& .MuiSwitch-track": { - borderRadius: 16 / 2, - opacity: 1, - backgroundColor: "rgba(0,0,0,.25)", - boxSizing: "border-box", - ...theme.applyStyles("dark", { - backgroundColor: "rgba(255,255,255,.35)", - }), - }, })); const Prospecting = () => { - /** @type {[('This Week' | 'All' | 'Last Week' | 'This Month' | 'Last Month' | 'This Quarter' | 'Last Quarter'), Function]} */ - const [period, setPeriod] = useState("All"); - const [isSummary, setIsSummary] = useState(true); - const [loading, setLoading] = useState(true); - const [summaryLoading, setSummaryLoading] = useState(false); - const [error, setError] = useState(null); - const [summaryData, setSummaryData] = useState(null); - const [rawData, setRawData] = useState([]); - const inFlightRef = useRef(false); - const navigate = useNavigate(); - const [dataFilter, setDataFilter] = useState(null); - const [originalRawData, setOriginalRawData] = useState([]); - const [loggedInUser, setLoggedInUser] = useState(null); - const [userLoading, setUserLoading] = useState(true); - const [userError, setUserError] = useState(null); - const [instanceUrl, setInstanceUrl] = useState(""); - const [urlLoading, setUrlLoading] = useState(true); - const [urlError, setUrlError] = useState(null); - const [userTimezone, setUserTimezone] = useState(""); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const [totalItems, setTotalItems] = useState(0); - const [detailedActivationData, setDetailedActivationData] = useState([]); - const [tableLoading, setTableLoading] = useState(false); - const [searchTerm, setSearchTerm] = useState(""); - - useEffect(() => { - async function fetchUserAndInstanceUrl() { - try { - if (loggedInUser && instanceUrl && userTimezone) { - return; - } - const [userResponse, instanceUrlResponse, timezoneResponse] = - await Promise.all([ - getLoggedInUser(), - getInstanceUrl(), - getUserTimezone(), - ]); - - if (userResponse.success) { - setLoggedInUser(userResponse.data[0]); - } else { - setUserError("Failed to fetch user data"); + /** @type {[('This Week' | 'All' | 'Last Week' | 'This Month' | 'Last Month' | 'This Quarter' | 'Last Quarter'), Function]} */ + const [period, setPeriod] = useState("All"); + const [isSummary, setIsSummary] = useState(true); + const [loading, setLoading] = useState(true); + const [summaryLoading, setSummaryLoading] = useState(false); + const [error, setError] = useState(null); + const [summaryData, setSummaryData] = useState(null); + const [rawData, setRawData] = useState([]); + const inFlightRef = useRef(false); + const navigate = useNavigate(); + const [dataFilter, setDataFilter] = useState(null); + const [originalRawData, setOriginalRawData] = useState([]); + const [loggedInUser, setLoggedInUser] = useState(null); + const [userLoading, setUserLoading] = useState(true); + const [userError, setUserError] = useState(null); + const [instanceUrl, setInstanceUrl] = useState(""); + const [urlLoading, setUrlLoading] = useState(true); + const [urlError, setUrlError] = useState(null); + const [userTimezone, setUserTimezone] = useState(""); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [totalItems, setTotalItems] = useState(0); + const [detailedActivationData, setDetailedActivationData] = useState([]); + const [tableLoading, setTableLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + + useEffect(() => { + async function fetchUserAndInstanceUrl() { + try { + if (loggedInUser && instanceUrl && userTimezone) { + return; + } + + const [userResponse, instanceUrlResponse, timezoneResponse] = + await Promise.all([ + getLoggedInUser(), + getInstanceUrl(), + getUserTimezone(), + ]); + + if (userResponse.success) { + setLoggedInUser(userResponse.data[0]); + console.log("just a test, remove!"); + const result = await fetchSalesforceTeam(); + console.log(result); + } else { + setUserError("Failed to fetch user data"); + } + + if (instanceUrlResponse.success) { + setInstanceUrl(instanceUrlResponse.data[0]); + } else { + setUrlError("Failed to fetch instance URL"); + } + + if (timezoneResponse.success) { + setUserTimezone(timezoneResponse.data); + } else { + console.error("Failed to fetch user timezone"); + } + } catch (error) { + setUserError("An error occurred while fetching user data"); + setUrlError("An error occurred while fetching instance URL"); + console.error("An error occurred while fetching user timezone"); + throw error; // Add this line to ensure the error is propagated + } finally { + setUserLoading(false); + setUrlLoading(false); + } } - if (instanceUrlResponse.success) { - setInstanceUrl(instanceUrlResponse.data[0]); - } else { - setUrlError("Failed to fetch instance URL"); - } + fetchUserAndInstanceUrl(); + }, []); - if (timezoneResponse.success) { - setUserTimezone(timezoneResponse.data); - } else { - console.error("Failed to fetch user timezone"); + const freeTrialDaysLeft = useMemo(() => { + if (!loggedInUser) { + return 0; + } + if (loggedInUser?.created_at?.length === 0) { + return 0; // No creation date, no trial left } - } catch (error) { - setUserError("An error occurred while fetching user data"); - setUrlError("An error occurred while fetching instance URL"); - console.error("An error occurred while fetching user timezone"); - throw error; // Add this line to ensure the error is propagated - } finally { - setUserLoading(false); - setUrlLoading(false); - } - } - fetchUserAndInstanceUrl(); - }, []); + const createdAtDate = new Date(loggedInUser.created_at); // Convert to Date object + const currentDate = new Date(); // Current date - const freeTrialDaysLeft = useMemo(() => { - if (!loggedInUser) { - return 0; - } - if (loggedInUser?.created_at?.length === 0) { - return 0; // No creation date, no trial left - } + // Set both dates to midnight (00:00:00) to ignore time + createdAtDate.setHours(0, 0, 0, 0); + currentDate.setHours(0, 0, 0, 0); - const createdAtDate = new Date(loggedInUser.created_at); // Convert to Date object - const currentDate = new Date(); // Current date - - // Set both dates to midnight (00:00:00) to ignore time - createdAtDate.setHours(0, 0, 0, 0); - currentDate.setHours(0, 0, 0, 0); - - // Calculate difference in milliseconds - const timeDifference = currentDate.getTime() - createdAtDate.getTime(); - - // Convert milliseconds to days - const daysPassed = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); - - // Free trial is 3 days, so calculate days left - const trialDaysLeft = 3 - daysPassed; - - // If days left is less than 0, return 0 - return trialDaysLeft > 0 && trialDaysLeft < 4 ? trialDaysLeft : 0; - }, [loggedInUser]); - - const handleDataFilter = useCallback((filters) => { - setDataFilter(filters); - }, []); - - const handleRefresh = () => { - fetchData(true); - }; - - const fetchData = useCallback( - /** - * - * @param {boolean} isRefresh - * @param {'Today' | 'Yesterday' | 'This Week' | 'Last Week' | 'This Month' | 'Last Month' | 'This Quarter' | 'Last Quarter'} selectedPeriod - * @param {string[]} filteredIds - * @returns - */ - async (isRefresh = false, selectedPeriod = period, filteredIds = []) => { - if (inFlightRef.current) return; - inFlightRef.current = true; - setLoading(true); - setSummaryLoading(true); - try { - let response; - if (isRefresh) { - await processNewProspectingActivity(userTimezone); - } + // Calculate difference in milliseconds + const timeDifference = currentDate.getTime() - createdAtDate.getTime(); - response = await fetchProspectingActivities( - selectedPeriod, - filteredIds - ); - - if (response.statusCode === 200 && response.success) { - setSummaryData(response.data[0].summary); - setRawData(response.data[0].raw_data || []); - setOriginalRawData(response.data[0].raw_data || []); - } else if (response.statusCode === 401) { - navigate("/"); - } else { - setError(response.message); + // Convert milliseconds to days + const daysPassed = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + + // Free trial is 3 days, so calculate days left + const trialDaysLeft = 3 - daysPassed; + + // If days left is less than 0, return 0 + return trialDaysLeft > 0 && trialDaysLeft < 4 ? trialDaysLeft : 0; + }, [loggedInUser]); + + const handleDataFilter = useCallback((filters) => { + setDataFilter(filters); + }, []); + + const handleRefresh = () => { + fetchData(true); + }; + + const fetchData = useCallback( + /** + * + * @param {boolean} isRefresh + * @param {'Today' | 'Yesterday' | 'This Week' | 'Last Week' | 'This Month' | 'Last Month' | 'This Quarter' | 'Last Quarter'} selectedPeriod + * @param {string[]} filteredIds + * @returns + */ + async ( + isRefresh = false, + selectedPeriod = period, + filteredIds = [] + ) => { + if (inFlightRef.current) return; + inFlightRef.current = true; + setLoading(true); + setSummaryLoading(true); + try { + let response; + if (isRefresh) { + await processNewProspectingActivity(userTimezone); + } + + response = await fetchProspectingActivities( + selectedPeriod, + filteredIds + ); + + if (response.statusCode === 200 && response.success) { + setSummaryData(response.data[0].summary); + setRawData(response.data[0].raw_data || []); + setOriginalRawData(response.data[0].raw_data || []); + } else if (response.statusCode === 401) { + navigate("/"); + } else { + setError(response.message); + } + } catch (err) { + setError( + `An error occurred while fetching data: ${err.message}` + ); + console.error("Error details:", err); + throw err; + } finally { + setLoading(false); + setSummaryLoading(false); + inFlightRef.current = false; + } + }, + [navigate, period, userTimezone] + ); + + useEffect(() => { + fetchData(false, period); + }, [fetchData, period]); + + const filteredData = useMemo(() => { + if (!dataFilter) return originalRawData; + + return originalRawData.filter((item) => { + if ( + dataFilter.activatedBy.length > 0 && + !dataFilter.activatedBy.includes(item.activated_by_id) + ) + return false; + if ( + dataFilter.accountOwner.length > 0 && + !dataFilter.accountOwner.includes(item.account.owner.id) + ) + return false; + if ( + dataFilter.activatedByTeam.length > 0 && + !dataFilter.activatedByTeam.includes(item.activated_by.role) + ) + return false; + return true; + }); + }, [originalRawData, dataFilter]); + + const fetchPaginatedData = useCallback( + async (newPage, newRowsPerPage, newSearchTerm) => { + setTableLoading(true); + try { + const filteredIds = filteredData.map((item) => item.id); + const response = await getPaginatedProspectingActivities( + filteredIds, + newPage, + newRowsPerPage, + newSearchTerm + ); + if (response.statusCode === 200 && response.success) { + setDetailedActivationData(response.data[0].raw_data || []); + setTotalItems(response.data[0].total_items); + } else if (response.statusCode === 401) { + navigate("/"); + } else { + setError(response.message); + } + } catch (err) { + setError( + `An error occurred while fetching data: ${err.message}` + ); + console.error("Error details:", err); + throw err; // Add this line to ensure the error is propagated + } finally { + setTableLoading(false); + } + }, + [filteredData, navigate] + ); + + const debouncedFetchPaginatedData = useMemo( + () => debounce(fetchPaginatedData, 250), + [fetchPaginatedData] + ); + + useEffect(() => { + return () => { + debouncedFetchPaginatedData.cancel(); + }; + }, [debouncedFetchPaginatedData]); + + useEffect(() => { + if (filteredData.length > 0) { + debouncedFetchPaginatedData(page, rowsPerPage, searchTerm); } - } catch (err) { - setError(`An error occurred while fetching data: ${err.message}`); - console.error("Error details:", err); - throw err; - } finally { - setLoading(false); - setSummaryLoading(false); - inFlightRef.current = false; - } - }, - [navigate, period, userTimezone] - ); - - useEffect(() => { - fetchData(false, period); - }, [fetchData, period]); - - const filteredData = useMemo(() => { - if (!dataFilter) return originalRawData; - - return originalRawData.filter((item) => { - if ( - dataFilter.activatedBy.length > 0 && - !dataFilter.activatedBy.includes(item.activated_by_id) - ) - return false; - if ( - dataFilter.accountOwner.length > 0 && - !dataFilter.accountOwner.includes(item.account.owner.id) - ) - return false; - if ( - dataFilter.activatedByTeam.length > 0 && - !dataFilter.activatedByTeam.includes(item.activated_by.role) - ) - return false; - return true; - }); - }, [originalRawData, dataFilter]); - - const fetchPaginatedData = useCallback( - async (newPage, newRowsPerPage, newSearchTerm) => { - setTableLoading(true); - try { + }, [ + debouncedFetchPaginatedData, + page, + rowsPerPage, + filteredData, + searchTerm, + ]); + + const handlePageChange = (newPage, newRowsPerPage) => { + setPage(newPage); + setRowsPerPage(newRowsPerPage); + // The debounced function will be called in the useEffect + }; + + const handleRowsPerPageChange = (newRowsPerPage) => { + setRowsPerPage(newRowsPerPage); + setPage(0); + // The debounced function will be called in the useEffect + }; + + useEffect(() => { const filteredIds = filteredData.map((item) => item.id); - const response = await getPaginatedProspectingActivities( - filteredIds, - newPage, - newRowsPerPage, - newSearchTerm - ); - if (response.statusCode === 200 && response.success) { - setDetailedActivationData(response.data[0].raw_data || []); - setTotalItems(response.data[0].total_items); - } else if (response.statusCode === 401) { - navigate("/"); + fetchData(false, period, filteredIds); + }, [dataFilter, period]); + + const handlePeriodChange = (event) => { + const newPeriod = event.target.value; + setPeriod(newPeriod); + }; + + const [selectedActivation, setSelectedActivation] = useState(null); + + const handleRowClick = (activation) => { + setSelectedActivation(activation); + }; + + useEffect(() => { + const fetchFilteredSummary = async () => { + setSummaryLoading(true); + try { + const filteredIds = filteredData.map((item) => item.id); + await fetchData(false, period, filteredIds); + } catch (err) { + setError( + `An error occurred while generating the summary. ${err}` + ); + throw err; + } finally { + setSummaryLoading(false); + } + }; + + if (filteredData.length > 0) { + fetchFilteredSummary(); } else { - setError(response.message); + setSummaryData({ + total_activations: 0, + activations_today: 0, + avg_tasks_per_contact: 0, + avg_contacts_per_account: 0, + total_tasks: 0, + total_events: 0, + total_contacts: 0, + total_accounts: 0, + total_deals: 0, + total_pipeline_value: 0, + engaged_activations: 0, + }); } - } catch (err) { - setError(`An error occurred while fetching data: ${err.message}`); - console.error("Error details:", err); - throw err; // Add this line to ensure the error is propagated - } finally { - setTableLoading(false); - } - }, - [filteredData, navigate] - ); - - const debouncedFetchPaginatedData = useMemo( - () => debounce(fetchPaginatedData, 250), - [fetchPaginatedData] - ); + }, [dataFilter, period]); - useEffect(() => { - return () => { - debouncedFetchPaginatedData.cancel(); + const handleSearch = (newSearchTerm) => { + setSearchTerm(newSearchTerm); + setPage(0); // Reset to first page when searching + fetchPaginatedData(0, rowsPerPage, newSearchTerm); }; - }, [debouncedFetchPaginatedData]); - useEffect(() => { - if (filteredData.length > 0) { - debouncedFetchPaginatedData(page, rowsPerPage, searchTerm); + if (loading || summaryLoading || userLoading || urlLoading) { + return ; } - }, [ - debouncedFetchPaginatedData, - page, - rowsPerPage, - filteredData, - searchTerm, - ]); - - const handlePageChange = (newPage, newRowsPerPage) => { - setPage(newPage); - setRowsPerPage(newRowsPerPage); - // The debounced function will be called in the useEffect - }; - - const handleRowsPerPageChange = (newRowsPerPage) => { - setRowsPerPage(newRowsPerPage); - setPage(0); - // The debounced function will be called in the useEffect - }; - - useEffect(() => { - const filteredIds = filteredData.map((item) => item.id); - fetchData(false, period, filteredIds); - }, [dataFilter, period]); - - const handlePeriodChange = (event) => { - const newPeriod = event.target.value; - setPeriod(newPeriod); - }; - - const [selectedActivation, setSelectedActivation] = useState(null); - - const handleRowClick = (activation) => { - setSelectedActivation(activation); - }; - - useEffect(() => { - const fetchFilteredSummary = async () => { - setSummaryLoading(true); - try { - const filteredIds = filteredData.map((item) => item.id); - await fetchData(false, period, filteredIds); - } catch (err) { - setError(`An error occurred while generating the summary. ${err}`); - throw err; - } finally { - setSummaryLoading(false); - } - }; - if (filteredData.length > 0) { - fetchFilteredSummary(); - } else { - setSummaryData({ - total_activations: 0, - activations_today: 0, - avg_tasks_per_contact: 0, - avg_contacts_per_account: 0, - total_tasks: 0, - total_events: 0, - total_contacts: 0, - total_accounts: 0, - total_deals: 0, - total_pipeline_value: 0, - engaged_activations: 0, - }); + if (error || userError || urlError) { + return {error || userError || urlError}; } - }, [dataFilter, period]); - - const handleSearch = (newSearchTerm) => { - setSearchTerm(newSearchTerm); - setPage(0); // Reset to first page when searching - fetchPaginatedData(0, rowsPerPage, newSearchTerm); - }; - - if (loading || summaryLoading || userLoading || urlLoading) { - return ; - } - - if (error || userError || urlError) { - return {error || userError || urlError}; - } - - if (loggedInUser?.status !== "paid" && freeTrialDaysLeft === 0) { - return ; - } - - return ( - - + + if (loggedInUser?.status !== "paid" && freeTrialDaysLeft === 0) { + return ; + } + + return ( - - - - - - - - Detailed - { - setIsSummary((prev) => !prev); - }} - inputProps={{ "aria-label": "ant design" }} - /> - Summary - - - - { - if ( - loggedInUser?.status === "not paid" && - freeTrialDaysLeft === 0 - ) { - return; - } - handleRefresh(); - }} - color="primary" - size="small" - > - - - - + + + + + + + + + Detailed + { + setIsSummary((prev) => !prev); + }} + inputProps={{ "aria-label": "ant design" }} + /> + Summary + + + + { + if ( + loggedInUser?.status === "not paid" && + freeTrialDaysLeft === 0 + ) { + return; + } + handleRefresh(); + }} + color="primary" + size="small" + > + + + + + + {error ? ( + {error} + ) : isSummary && !summaryLoading ? ( + + ) : ( + <> + + {selectedActivation && ( + + )} + + )} + + {loggedInUser?.status === "not paid" && freeTrialDaysLeft > 0 && ( + + )} - {error ? ( - {error} - ) : isSummary && !summaryLoading ? ( - - ) : ( - <> - - {selectedActivation && ()} - - )} - - {loggedInUser?.status === "not paid" && freeTrialDaysLeft > 0 && ( - - )} -
- ); + ); }; -export default Prospecting; \ No newline at end of file +export default Prospecting; diff --git a/server/app/data_models.py b/server/app/data_models.py index 11cc848..8668dee 100644 --- a/server/app/data_models.py +++ b/server/app/data_models.py @@ -219,7 +219,9 @@ class Activation(SerializableModel): last_outbound_engagement: Optional[date] = None opportunity: Optional[Opportunity] = None status: StatusEnum = Field(default=StatusEnum.activated) - + outbound_prospecting_metadata_by_user: Optional[Dict[str, List[ProspectingMetadata]]] = None + inbound_prospecting_metadata_by_user: Optional[Dict[str, List[ProspectingMetadata]]] = None + def __init__(self, **data): if "activated_by" in data and isinstance(data["activated_by"], UserModel): data["activated_by_id"] = data["activated_by"].id diff --git a/server/app/helpers/activation_helper.py b/server/app/helpers/activation_helper.py index 13d0162..e8a50cb 100644 --- a/server/app/helpers/activation_helper.py +++ b/server/app/helpers/activation_helper.py @@ -1,7 +1,7 @@ from typing import Dict, List from datetime import datetime, date, timedelta from collections import defaultdict -from app.data_models import Activation +from app.data_models import Activation, Settings from app.utils import ( parse_datetime_string_with_timezone, add_days, @@ -169,8 +169,7 @@ def generate_summary(activations: list[Activation]) -> dict: ) summary["avg_outbound_activities_to_inbound_response"] = ( round( - total_prospecting_activities_engagement - / summary["engaged_activations"], + total_prospecting_activities_engagement / summary["engaged_activations"], 2, ) if summary["engaged_activations"] > 0 @@ -758,3 +757,62 @@ def get_inbound_tasks_within_period(inbound_tasks, start_date, period_days): for task in inbound_tasks if is_model_date_field_within_window(task, start_date, period_days) ] + + +def get_prospecting_metadata_direction( + task_id: str, activation: Activation, settings: Settings +) -> str: + for metadata in activation.prospecting_metadata: + if task_id in metadata.task_ids: + for criterion in settings.criteria: + if criterion.name == metadata.name: + return ( + criterion.direction or "outbound" + ) # Default to outbound if direction is not specified + return "matching_criteria_not_found" + +def set_effort_and_results_by_full_user_name( + activations: List[Activation], settings: Settings +) -> List[Activation]: + for activation in activations: + outbound_metadata = defaultdict(lambda: defaultdict(list)) + inbound_metadata = defaultdict(lambda: defaultdict(list)) + + for task in activation.tasks: + direction = get_prospecting_metadata_direction( + task["Id"], activation, settings + ) + metadata_dict = ( + outbound_metadata if direction.lower() == "outbound" else inbound_metadata + ) + + owner_id = task["OwnerId"] + criterion_name = next( + ( + m.name + for m in activation.prospecting_metadata + if task["Id"] in m.task_ids + ), + None, + ) + + if criterion_name: + metadata_dict[owner_id][criterion_name].append(task["Id"]) + + activation.outbound_prospecting_metadata_by_user = { + owner_id: [ + ProspectingMetadata(name=name, task_ids=set(ids), total=len(ids)) + for name, ids in criteria.items() + ] + for owner_id, criteria in outbound_metadata.items() + } + + activation.inbound_prospecting_metadata_by_user = { + owner_id: [ + ProspectingMetadata(name=name, task_ids=set(ids), total=len(ids)) + for name, ids in criteria.items() + ] + for owner_id, criteria in inbound_metadata.items() + } + + return activations diff --git a/server/app/routes.py b/server/app/routes.py index 452f96f..4bf1e92 100644 --- a/server/app/routes.py +++ b/server/app/routes.py @@ -2,7 +2,7 @@ from flask import Blueprint, jsonify, redirect, request from urllib.parse import unquote from app.middleware import authenticate -from app.utils import format_error_message, log_error +from app.utils import format_error_message, log_error, get_salesforce_team_ids from app.database.activation_selector import ( load_active_activations_minimal_by_ids, load_activations_by_period, @@ -23,7 +23,10 @@ convert_settings_model_to_settings, convert_settings_to_settings_model, ) -from app.helpers.activation_helper import generate_summary +from app.helpers.activation_helper import ( + generate_summary, + set_effort_and_results_by_full_user_name, +) from app.services.setting_service import define_criteria_from_events_or_tasks from app.engine.activation_engine import update_activation_states from app.salesforce_api import ( @@ -326,11 +329,14 @@ def get_paginated_prospecting_activities(): ) if result.success: + full_activations = result.data["activations"] + full_activations = set_effort_and_results_by_full_user_name( + full_activations, load_settings() + ) response.data = [ { "raw_data": [ - activation.to_dict() - for activation in result.data["activations"] + activation.to_dict() for activation in full_activations ], "total_items": result.data["total_count"], } @@ -454,6 +460,24 @@ def get_salesforce_users(): return jsonify(response.to_dict()), get_status_code(response) +@bp.route("/get_salesforce_team", methods=["POST"]) +@authenticate +def get_salesforce_team(): + from app.data_models import ApiResponse + + response = ApiResponse(data=[], message="", success=False) + try: + response = fetch_salesforce_users(get_salesforce_team_ids(load_settings())) + except Exception as e: + log_error(e) + error_message = format_error_message(e) + response.message = ( + f"Failed to retrieve Salesforce users by IDs: {error_message}" + ) + + return jsonify(response.to_dict()), get_status_code(response) + + @bp.route("/get_salesforce_tasks_by_user_ids", methods=["GET"]) @authenticate def get_salesforce_tasks_by_user_ids(): diff --git a/server/app/utils.py b/server/app/utils.py index e2ee7a1..4a90612 100644 --- a/server/app/utils.py +++ b/server/app/utils.py @@ -13,7 +13,7 @@ logger = setup_logger(__name__) -def get_salesforce_team_ids(settings: Settings): +def get_salesforce_team_ids(settings: Settings = None) -> list[str]: team_member_ids = [settings.salesforce_user_id] if settings.team_member_ids: team_member_ids.extend(settings.team_member_ids) From 5c0992a0c2a1163217dfc797be106e43d52bd8c5 Mon Sep 17 00:00:00 2001 From: ter1203 Date: Tue, 15 Oct 2024 17:11:05 -0300 Subject: [PATCH 07/12] fix: feature sort column with sortConfig --- .../src/components/CustomTable/CustomTable.js | 725 ++++++------ .../ProspectingActiveAccount/AccountDetail.js | 59 +- .../SelectedAccount.js | 52 +- .../ProspectingActiveAccount/TimeLine.js | 286 +++-- client/src/pages/Prospecting/Prospecting.js | 1030 ++++++++--------- 5 files changed, 1109 insertions(+), 1043 deletions(-) diff --git a/client/src/components/CustomTable/CustomTable.js b/client/src/components/CustomTable/CustomTable.js index 0ef59d9..f11114e 100644 --- a/client/src/components/CustomTable/CustomTable.js +++ b/client/src/components/CustomTable/CustomTable.js @@ -1,25 +1,25 @@ import React, { useState, useMemo, useEffect } from "react"; import { - Table, - Paper, - TableContainer, - TablePagination, - Box, - TextField, - InputAdornment, - IconButton, - Menu, - MenuItem, - Modal, - Typography, - List, - ListItem, - ListItemIcon, - ListItemText, - Checkbox, - Avatar, - Divider, - CircularProgress, + Table, + Paper, + TableContainer, + TablePagination, + Box, + TextField, + InputAdornment, + IconButton, + Menu, + MenuItem, + Modal, + Typography, + List, + ListItem, + ListItemIcon, + ListItemText, + Checkbox, + Avatar, + Divider, + CircularProgress, } from "@mui/material"; import SearchIcon from "@mui/icons-material/Search"; import ClearIcon from "@mui/icons-material/Clear"; @@ -63,377 +63,370 @@ import { debounce } from "lodash"; // Add this import at the top of the file * }} props */ const CustomTable = ({ - tableData, - onSelectionChange, - onColumnsChange, - paginationConfig, - sortConfig, - onRowClick, - isLoading, - onSearch, + tableData, + onSelectionChange, + onColumnsChange, + paginationConfig, + sortConfig, + onRowClick, + isLoading, + onSearch, }) => { - const [searchTerm, setSearchTerm] = useState(""); - const [localFilteredData, setLocalFilteredData] = useState(tableData.data); - const [page, setPage] = useState(paginationConfig?.page); - const [rowsPerPage, setRowsPerPage] = useState( - paginationConfig?.rowsPerPage || 10 - ); - const [orderBy, setOrderBy] = useState(sortConfig?.columnId || ""); - const [order, setOrder] = useState(sortConfig?.order || "asc"); - const [contextMenu, setContextMenu] = useState(null); - const [modalOpen, setModalOpen] = useState(false); - const [selectedRowId, setSelectedRowId] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [localFilteredData, setLocalFilteredData] = useState(tableData.data); + const [page, setPage] = useState(paginationConfig?.page); + const [rowsPerPage, setRowsPerPage] = useState( + paginationConfig?.rowsPerPage || 10 + ); + const [orderBy, setOrderBy] = useState(sortConfig?.columnId || ""); + const [order, setOrder] = useState(sortConfig?.order || "asc"); + const [contextMenu, setContextMenu] = useState(null); + const [modalOpen, setModalOpen] = useState(false); + const [selectedRowId, setSelectedRowId] = useState(null); - const isPaginated = !!paginationConfig; - const isServerSidePaginated = - isPaginated && paginationConfig.type === "server-side"; + const isPaginated = !!paginationConfig; + const isServerSidePaginated = + isPaginated && paginationConfig.type === "server-side"; - const filteredData = useMemo(() => { - return isServerSidePaginated ? tableData.data : localFilteredData; - }, [tableData.data, localFilteredData, isServerSidePaginated]); + const filteredData = useMemo(() => { + return isServerSidePaginated ? tableData.data : localFilteredData; + }, [tableData.data, localFilteredData, isServerSidePaginated]); - const sortedData = useMemo(() => { - return isServerSidePaginated - ? filteredData - : sortData(filteredData, orderBy, order); - }, [filteredData, orderBy, order, isServerSidePaginated]); + const sortedData = useMemo(() => { + return isServerSidePaginated + ? filteredData + : sortData(filteredData, orderBy, order); + }, [filteredData, orderBy, order, isServerSidePaginated]); - const paginatedData = useMemo(() => { - if (!isPaginated) return sortedData; - if (isServerSidePaginated) return sortedData; - const startIndex = page * rowsPerPage; - return sortedData.slice(startIndex, startIndex + rowsPerPage); - }, [sortedData, page, rowsPerPage, isPaginated, isServerSidePaginated]); + const paginatedData = useMemo(() => { + if (!isPaginated) return sortedData; + if (isServerSidePaginated) return sortedData; + const startIndex = page * rowsPerPage; + return sortedData.slice(startIndex, startIndex + rowsPerPage); + }, [sortedData, page, rowsPerPage, isPaginated, isServerSidePaginated]); - const handleContextMenu = (event) => { - event.preventDefault(); - setContextMenu( - contextMenu === null - ? { mouseX: event.clientX - 2, mouseY: event.clientY - 4 } - : null - ); - }; + const handleContextMenu = (event) => { + event.preventDefault(); + setContextMenu( + contextMenu === null + ? { mouseX: event.clientX - 2, mouseY: event.clientY - 4 } + : null + ); + }; - const handleCloseContextMenu = () => { - setContextMenu(null); - }; + const handleCloseContextMenu = () => { + setContextMenu(null); + }; - const handleOpenModal = () => { - setModalOpen(true); - handleCloseContextMenu(); - }; + const handleOpenModal = () => { + setModalOpen(true); + handleCloseContextMenu(); + }; - const handleCloseModal = () => { - setModalOpen(false); - }; + const handleCloseModal = () => { + setModalOpen(false); + }; - const handleColumnToggle = (column) => { - const newColumns = tableData.columns.includes(column) - ? tableData.columns.filter((c) => c.id !== column.id) - : [...tableData.columns, column]; - onColumnsChange(newColumns); - }; + const handleColumnToggle = (column) => { + const newColumns = tableData.columns.includes(column) + ? tableData.columns.filter((c) => c.id !== column.id) + : [...tableData.columns, column]; + onColumnsChange(newColumns); + }; - const handleChangePage = (event, newPage) => { - setPage(newPage); - if (isServerSidePaginated && paginationConfig.onPageChange) { - paginationConfig.onPageChange(newPage, rowsPerPage); - } - }; + const handleChangePage = (event, newPage) => { + setPage(newPage); + if (isServerSidePaginated && paginationConfig.onPageChange) { + paginationConfig.onPageChange(newPage, rowsPerPage); + } + }; - const handleChangeRowsPerPage = (event) => { - const newRowsPerPage = parseInt(event.target.value, 10); - setRowsPerPage(newRowsPerPage); - setPage(0); - if (isServerSidePaginated && paginationConfig.onRowsPerPageChange) { - paginationConfig.onRowsPerPageChange(newRowsPerPage); - } - }; + const handleChangeRowsPerPage = (event) => { + const newRowsPerPage = parseInt(event.target.value, 10); + setRowsPerPage(newRowsPerPage); + setPage(0); + if (isServerSidePaginated && paginationConfig.onRowsPerPageChange) { + paginationConfig.onRowsPerPageChange(newRowsPerPage); + } + }; - const handleSort = (columnId) => { - const isAsc = orderBy === columnId && order === "asc"; - const newOrder = isAsc ? "desc" : "asc"; - setOrder(newOrder); - setOrderBy(columnId); + const handleSort = (columnId) => { + const isAsc = orderBy === columnId && order === "asc"; + const newOrder = isAsc ? "desc" : "asc"; + setOrder(newOrder); + setOrderBy(columnId); - if (sortConfig?.onSort) { - sortConfig.onSort(columnId, newOrder); - } - }; + if (sortConfig?.onSort) { + sortConfig.onSort(columnId, newOrder); + } + }; - const handleToggle = (item) => { - const newSelectedIds = new Set(tableData.selectedIds); - if (newSelectedIds.has(item.id)) { - newSelectedIds.delete(item.id); - } else { - newSelectedIds.add(item.id); - } - onSelectionChange(newSelectedIds); - }; + const handleToggle = (item) => { + const newSelectedIds = new Set(tableData.selectedIds); + if (newSelectedIds.has(item.id)) { + newSelectedIds.delete(item.id); + } else { + newSelectedIds.add(item.id); + } + onSelectionChange(newSelectedIds); + }; - const handleRowClick = (item) => { - setSelectedRowId(item.id); - if (onRowClick) { - onRowClick(item); - } - }; + const handleRowClick = (item) => { + setSelectedRowId(item.id); + if (onRowClick) { + onRowClick(item); + } + }; - /** - * @param {TableColumn} column - * @param {Record} item - */ - const renderCell = (column, item) => { - let element; - switch (column.dataType) { - case "select": - element = ( - handleToggle(item)} - /> - ); - break; - case "image": - element = ( - - ); - break; - case "date": - if (item[column.id]) { - element = new Date(item[column.id]).toLocaleDateString(); - break; - } - element = null; - break; - case "datetime": - if (item[column.id]) { - element = new Date(item[column.id]).toLocaleString(); - break; - } - element = null; - break; - case "number": - element = Number(item[column.id]).toLocaleString(); - break; - default: - element = item[column.id]; - break; + /** + * @param {TableColumn} column + * @param {Record} item + */ + const renderCell = (column, item) => { + let element; + switch (column.dataType) { + case "select": + element = ( + handleToggle(item)} + /> + ); + break; + case "image": + element = ( + + ); + break; + case "date": + if (item[column.id]) { + element = new Date(item[column.id]).toLocaleDateString(); + break; } + element = null; + break; + case "datetime": + if (item[column.id]) { + element = new Date(item[column.id]).toLocaleString(); + break; + } + element = null; + break; + case "number": + element = Number(item[column.id]).toLocaleString(); + break; + default: + element = item[column.id]; + break; + } - return element; - }; + return element; + }; - const tableContent = useMemo( - () => ( - - - - {isLoading ? ( - - - - - - ) : ( - - )} -
- -
-
- ), - [ - paginatedData, - tableData.columns, - tableData.selectedIds, - orderBy, - order, - selectedRowId, - isLoading, - ] - ); + const tableContent = useMemo( + () => ( + + + + {isLoading ? ( + + + + + + ) : ( + + )} +
+ +
+
+ ), + [ + paginatedData, + tableData.columns, + tableData.selectedIds, + orderBy, + order, + selectedRowId, + isLoading, + ] + ); - // Create a debounced version of the search function - const debouncedSearch = useMemo( - () => - debounce((value) => { - if (paginationConfig?.type === "server-side") { - if (value.length > 3 || value.length === 0) { - onSearch(value); - } - } else { - // Perform local wildcard search on all columns - const filtered = tableData.data.filter((item) => - Object.values(item).some((field) => - String(field) - .toLowerCase() - .includes(value.toLowerCase()) - ) - ); - setLocalFilteredData(filtered); - setPage(0); // Reset to first page when filtering - } - }, 2000), - [paginationConfig, onSearch, tableData.data] - ); + // Create a debounced version of the search function + const debouncedSearch = useMemo( + () => + debounce((value) => { + if (paginationConfig?.type === "server-side") { + if (value.length > 3 || value.length === 0) { + onSearch(value); + } + } else { + // Perform local wildcard search on all columns + const filtered = tableData.data.filter((item) => + Object.values(item).some((field) => + String(field).toLowerCase().includes(value.toLowerCase()) + ) + ); + setLocalFilteredData(filtered); + setPage(0); // Reset to first page when filtering + } + }, 2000), + [paginationConfig, onSearch, tableData.data] + ); - // Update the handleSearch function - const handleSearch = (value) => { - setSearchTerm(value); - debouncedSearch(value); - }; + // Update the handleSearch function + const handleSearch = (value) => { + setSearchTerm(value); + debouncedSearch(value); + }; - // force a re-render when the sort order changes - useEffect(() => { - setPage(0); - }, [orderBy, order]); + // force a re-render when the sort order changes + useEffect(() => { + setPage(0); + }, [orderBy, order]); - return ( - - handleSearch(e.target.value)} - margin="normal" - InputProps={{ - startAdornment: ( - - - - ), - endAdornment: searchTerm && ( - - handleSearch("")} - edge="end" - > - - - - ), - }} - /> - {tableContent} - {isPaginated && ( - - )} - - Manage Columns - - - - - Manage Columns - - - - {tableData.availableColumns?.map((column) => ( - handleColumnToggle(column)} - > - - c.id === column.id - )} - tabIndex={-1} - disableRipple - /> - - - - ))} - - - + return ( + + handleSearch(e.target.value)} + margin="normal" + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: searchTerm && ( + + handleSearch("")} edge="end"> + + + + ), + }} + /> + {tableContent} + {isPaginated && ( + + )} + + Manage Columns + + + + + Manage Columns + + + + {tableData.availableColumns?.map((column) => ( + handleColumnToggle(column)} + > + + c.id === column.id)} + tabIndex={-1} + disableRipple + /> + + + + ))} + - ); + + + ); }; export default CustomTable; diff --git a/client/src/components/ProspectingActiveAccount/AccountDetail.js b/client/src/components/ProspectingActiveAccount/AccountDetail.js index 96955b8..8c11511 100644 --- a/client/src/components/ProspectingActiveAccount/AccountDetail.js +++ b/client/src/components/ProspectingActiveAccount/AccountDetail.js @@ -1,11 +1,24 @@ -import { Box, Card, Grid, Typography } from "@mui/material" -import CustomTable from "../CustomTable/CustomTable" -import CardActiveAccount from "./CardActiveAccount" -import { Link } from "react-router-dom" +import { Box, Card, Grid, Typography } from "@mui/material"; +import CustomTable from "../CustomTable/CustomTable"; +import CardActiveAccount from "./CardActiveAccount"; +import { Link } from "react-router-dom"; import { tableColumns } from "../../pages/Prospecting/tableColumns"; import { useState } from "react"; -const AccountDetail = ({ detailedActivationData, instanceUrl, totalItems, page, rowsPerPage, handlePageChange, handleRowsPerPageChange, handleRowClick, tableLoading, handleSearch, selectedActivation }) => { +const AccountDetail = ({ + sortConfig, + detailedActivationData, + instanceUrl, + totalItems, + page, + rowsPerPage, + handlePageChange, + handleRowsPerPageChange, + handleRowClick, + tableLoading, + handleSearch, + selectedActivation, +}) => { const [columnShows, setColumnShows] = useState( localStorage.getItem("activationColumnShow") ? JSON.parse(localStorage.getItem("activationColumnShow")) @@ -23,22 +36,26 @@ const AccountDetail = ({ detailedActivationData, instanceUrl, totalItems, page, - + Active Accounts List - ) -} - + ); +}; -export default AccountDetail \ No newline at end of file +export default AccountDetail; diff --git a/client/src/components/ProspectingActiveAccount/SelectedAccount.js b/client/src/components/ProspectingActiveAccount/SelectedAccount.js index d8d2d27..924a0d3 100644 --- a/client/src/components/ProspectingActiveAccount/SelectedAccount.js +++ b/client/src/components/ProspectingActiveAccount/SelectedAccount.js @@ -1,37 +1,37 @@ -import Timeline from "./TimeLine" -import { Box, Card, Grid } from "@mui/material" -import SummaryBarChartCard from '../../components/SummaryCard/SummaryBarChartCard' +import Timeline from "./TimeLine"; +import { Box, Card, Grid } from "@mui/material"; +import SummaryBarChartCard from "../../components/SummaryCard/SummaryBarChartCard"; function SelectedAccount({ selectedActivation }) { const mockData = [ { label: "User 1", - value: 6 + value: 6, }, { label: "User 2", - value: 4 + value: 4, }, { label: "User 3", - value: 7 + value: 7, }, { label: "User 4", - value: 2 - } - ] + value: 2, + }, + ]; return ( @@ -44,54 +44,52 @@ function SelectedAccount({ selectedActivation }) { - - - ) + ); } -export default SelectedAccount \ No newline at end of file +export default SelectedAccount; diff --git a/client/src/components/ProspectingActiveAccount/TimeLine.js b/client/src/components/ProspectingActiveAccount/TimeLine.js index 03dcc79..a2eb9ef 100644 --- a/client/src/components/ProspectingActiveAccount/TimeLine.js +++ b/client/src/components/ProspectingActiveAccount/TimeLine.js @@ -1,17 +1,23 @@ -import React, { useEffect, useState } from 'react'; -import { Typography, Box } from '@mui/material'; -import { AccessTime, Call, Message, Event, BusinessCenter } from '@mui/icons-material'; +import React, { useEffect, useState } from "react"; +import { Typography, Box } from "@mui/material"; +import { + AccessTime, + Call, + Message, + Event, + BusinessCenter, +} from "@mui/icons-material"; // Function to map icon names to actual icons const getIcon = (iconName, color) => { switch (iconName) { - case 'Message': + case "Message": return ; - case 'Dial': + case "Dial": return ; - case 'Meeting': + case "Meeting": return ; - case 'Opportunity': + case "Opportunity": return ; default: return ; @@ -20,73 +26,103 @@ const getIcon = (iconName, color) => { const MyTimelineComponent = ({ tasks }) => { // Sample data - const [text, setText] = useState('') - const [data, setData] = useState([]) - const [page, setPage] = useState(1) - const [maxPage, setMaxPage] = useState(1) + const [text, setText] = useState(""); + const [data, setData] = useState([]); + const [page, setPage] = useState(1); + const [maxPage, setMaxPage] = useState(1); useEffect(() => { let array = tasks.map((e, index) => { const date = new Date(e.CreatedDate); - const day = String(date.getDate()).padStart(2, '0'); - const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-indexed + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-indexed const formattedDate = `${day}/${month}`; - let format = 'top' - let color = '#7AAD67' - let icon = e.Subject - if (e.Priority === "Priority") color = '#DD4040' - let obj = { id: index, icon, date: formattedDate, color, format } - return obj - }) - setText(array[0].date + ' - ' + array[array.length - 1].date) - let maxPage = Math.ceil(array.length / 8) - setPage(maxPage) - setMaxPage(maxPage) - setData(array) - }, []) + let format = "top"; + let color = "#7AAD67"; + let icon = e.Subject; + if (e.Priority === "Priority") color = "#DD4040"; + let obj = { id: index, icon, date: formattedDate, color, format }; + return obj; + }); + setText(array[0].date + " - " + array[array.length - 1].date); + let maxPage = Math.ceil(array.length / 8); + setPage(maxPage); + setMaxPage(maxPage); + setData(array); + }, [tasks]); return (
- + Timeline - + {text} {/* Header */} - { - page > 1 && ( - setPage(page - 1)} display="flex" alignItems="center" pb={6} mb={2} mx={2} height="80px"> - - - - Previous - - ) - } + {page > 1 && ( + setPage(page - 1)} + display="flex" + alignItems="center" + pb={6} + mb={2} + mx={2} + height="80px" + > + + + + + Previous + + + )} - {/* Timeline */} - + { > - { - data.filter((e, i) => i > (page - 1) * 8 && i <= page * 8).map((el, index) => ( + {data + .filter((e, i) => i > (page - 1) * 8 && i <= page * 8) + .map((el, index) => ( { justifyContent="center" sx={{ height: 200 }} > - { - el.format == "bottom" && index < data.length && ( - - ) - } + {el.format == "bottom" && index < data.length && ( + + )} {getIcon(el.icon, el.color)} - { - el.format == "top" && index < data.length && ( - - ) - } + {el.format == "top" && index < data.length && ( + + )} {/* Date Below Icon */} - {el.line ? {el.label}
: <>} {el.date}
+ + {el.line ? ( + + {el.label} +
+
+ ) : ( + <> + )}{" "} + {el.date} +
- )) - } + ))}
- { - page < maxPage && ( - setPage(page + 1)} display="flex" alignItems="center" gap="10px" pb={6} mb={2} mx={2} height="80px"> - Next - - - - - ) - } + {page < maxPage && ( + setPage(page + 1)} + display="flex" + alignItems="center" + gap="10px" + pb={6} + mb={2} + mx={2} + height="80px" + > + + Next + + + + + + )} -
-
+ ); }; diff --git a/client/src/pages/Prospecting/Prospecting.js b/client/src/pages/Prospecting/Prospecting.js index 55513b3..b40199d 100644 --- a/client/src/pages/Prospecting/Prospecting.js +++ b/client/src/pages/Prospecting/Prospecting.js @@ -1,33 +1,32 @@ import React, { - useState, - useEffect, - useRef, - useCallback, - useMemo, + useState, + useEffect, + useRef, + useCallback, + useMemo, } from "react"; import "./Prospecting.css"; import { useNavigate } from "react-router-dom"; import { - Box, - Alert, - FormControl, - IconButton, - Tooltip, - Typography, - Switch, - styled, - Stack, + Box, + Alert, + FormControl, + IconButton, + Tooltip, + Typography, + Switch, + styled, + Stack, } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; import DataFilter from "../../components/DataFilter/DataFilter"; import { - fetchProspectingActivities, - getInstanceUrl, - processNewProspectingActivity, - getLoggedInUser, - getUserTimezone, - getPaginatedProspectingActivities, - fetchSalesforceTeam, + fetchProspectingActivities, + getInstanceUrl, + processNewProspectingActivity, + getLoggedInUser, + getUserTimezone, + getPaginatedProspectingActivities, } from "src/components/Api/Api"; import AccountDetail from "../../components/ProspectingActiveAccount/AccountDetail"; import CustomSelect from "src/components/CustomSelect/CustomSelect"; @@ -42,540 +41,527 @@ import { debounce } from "lodash"; // Make sure to import lodash or use a custom import SelectedAccount from "src/components/ProspectingActiveAccount/SelectedAccount"; const AntSwitch = styled(Switch)(({ theme }) => ({ - width: 28, - height: 16, - padding: 0, - display: "flex", - "&:active": { - "& .MuiSwitch-thumb": { - width: 15, - }, - "& .MuiSwitch-switchBase.Mui-checked": { - transform: "translateX(9px)", - }, - }, - "& .MuiSwitch-switchBase": { - padding: 2, - "&.Mui-checked": { - transform: "translateX(12px)", - color: "#fff", - "& + .MuiSwitch-track": { - opacity: 1, - backgroundColor: "#1890ff", - ...theme.applyStyles("dark", { - backgroundColor: "#177ddc", - }), - }, - }, - }, + width: 28, + height: 16, + padding: 0, + display: "flex", + "&:active": { "& .MuiSwitch-thumb": { - boxShadow: "0 2px 4px 0 rgb(0 35 11 / 20%)", - width: 12, - height: 12, - borderRadius: 6, - transition: theme.transitions.create(["width"], { - duration: 200, - }), + width: 15, + }, + "& .MuiSwitch-switchBase.Mui-checked": { + transform: "translateX(9px)", }, - "& .MuiSwitch-track": { - borderRadius: 16 / 2, + }, + "& .MuiSwitch-switchBase": { + padding: 2, + "&.Mui-checked": { + transform: "translateX(12px)", + color: "#fff", + "& + .MuiSwitch-track": { opacity: 1, - backgroundColor: "rgba(0,0,0,.25)", - boxSizing: "border-box", + backgroundColor: "#1890ff", ...theme.applyStyles("dark", { - backgroundColor: "rgba(255,255,255,.35)", + backgroundColor: "#177ddc", }), + }, }, + }, + "& .MuiSwitch-thumb": { + boxShadow: "0 2px 4px 0 rgb(0 35 11 / 20%)", + width: 12, + height: 12, + borderRadius: 6, + transition: theme.transitions.create(["width"], { + duration: 200, + }), + }, + "& .MuiSwitch-track": { + borderRadius: 16 / 2, + opacity: 1, + backgroundColor: "rgba(0,0,0,.25)", + boxSizing: "border-box", + ...theme.applyStyles("dark", { + backgroundColor: "rgba(255,255,255,.35)", + }), + }, })); const Prospecting = () => { - /** @type {[('This Week' | 'All' | 'Last Week' | 'This Month' | 'Last Month' | 'This Quarter' | 'Last Quarter'), Function]} */ - const [period, setPeriod] = useState("All"); - const [isSummary, setIsSummary] = useState(true); - const [loading, setLoading] = useState(true); - const [summaryLoading, setSummaryLoading] = useState(false); - const [error, setError] = useState(null); - const [summaryData, setSummaryData] = useState(null); - const [rawData, setRawData] = useState([]); - const inFlightRef = useRef(false); - const navigate = useNavigate(); - const [dataFilter, setDataFilter] = useState(null); - const [originalRawData, setOriginalRawData] = useState([]); - const [loggedInUser, setLoggedInUser] = useState(null); - const [userLoading, setUserLoading] = useState(true); - const [userError, setUserError] = useState(null); - const [instanceUrl, setInstanceUrl] = useState(""); - const [urlLoading, setUrlLoading] = useState(true); - const [urlError, setUrlError] = useState(null); - const [userTimezone, setUserTimezone] = useState(""); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const [totalItems, setTotalItems] = useState(0); - const [detailedActivationData, setDetailedActivationData] = useState([]); - const [tableLoading, setTableLoading] = useState(false); - const [searchTerm, setSearchTerm] = useState(""); - - useEffect(() => { - async function fetchUserAndInstanceUrl() { - try { - if (loggedInUser && instanceUrl && userTimezone) { - return; - } - - const [userResponse, instanceUrlResponse, timezoneResponse] = - await Promise.all([ - getLoggedInUser(), - getInstanceUrl(), - getUserTimezone(), - ]); - - if (userResponse.success) { - setLoggedInUser(userResponse.data[0]); - console.log("just a test, remove!"); - const result = await fetchSalesforceTeam(); - console.log(result); - } else { - setUserError("Failed to fetch user data"); - } - - if (instanceUrlResponse.success) { - setInstanceUrl(instanceUrlResponse.data[0]); - } else { - setUrlError("Failed to fetch instance URL"); - } - - if (timezoneResponse.success) { - setUserTimezone(timezoneResponse.data); - } else { - console.error("Failed to fetch user timezone"); - } - } catch (error) { - setUserError("An error occurred while fetching user data"); - setUrlError("An error occurred while fetching instance URL"); - console.error("An error occurred while fetching user timezone"); - throw error; // Add this line to ensure the error is propagated - } finally { - setUserLoading(false); - setUrlLoading(false); - } - } - - fetchUserAndInstanceUrl(); - }, []); - - const freeTrialDaysLeft = useMemo(() => { - if (!loggedInUser) { - return 0; + /** @type {[('This Week' | 'All' | 'Last Week' | 'This Month' | 'Last Month' | 'This Quarter' | 'Last Quarter'), Function]} */ + const [period, setPeriod] = useState("All"); + const [isSummary, setIsSummary] = useState(true); + const [loading, setLoading] = useState(true); + const [summaryLoading, setSummaryLoading] = useState(false); + const [error, setError] = useState(null); + const [summaryData, setSummaryData] = useState(null); + const [rawData, setRawData] = useState([]); + const inFlightRef = useRef(false); + const navigate = useNavigate(); + const [dataFilter, setDataFilter] = useState(null); + const [originalRawData, setOriginalRawData] = useState([]); + const [loggedInUser, setLoggedInUser] = useState(null); + const [userLoading, setUserLoading] = useState(true); + const [userError, setUserError] = useState(null); + const [instanceUrl, setInstanceUrl] = useState(""); + const [urlLoading, setUrlLoading] = useState(true); + const [urlError, setUrlError] = useState(null); + const [userTimezone, setUserTimezone] = useState(""); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [totalItems, setTotalItems] = useState(0); + const [detailedActivationData, setDetailedActivationData] = useState([]); + const [tableLoading, setTableLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [sortConfig, setSortConfig] = useState({ + columnId: "", + order: "asc", + }); + const onSort = (columnId, order = "asc") => { + setSortConfig({ columnId, order }); + }; + useEffect(() => { + async function fetchUserAndInstanceUrl() { + try { + if (loggedInUser && instanceUrl && userTimezone) { + return; } - if (loggedInUser?.created_at?.length === 0) { - return 0; // No creation date, no trial left + const [userResponse, instanceUrlResponse, timezoneResponse] = + await Promise.all([ + getLoggedInUser(), + getInstanceUrl(), + getUserTimezone(), + ]); + + if (userResponse.success) { + setLoggedInUser(userResponse.data[0]); + } else { + setUserError("Failed to fetch user data"); } - const createdAtDate = new Date(loggedInUser.created_at); // Convert to Date object - const currentDate = new Date(); // Current date + if (instanceUrlResponse.success) { + setInstanceUrl(instanceUrlResponse.data[0]); + } else { + setUrlError("Failed to fetch instance URL"); + } - // Set both dates to midnight (00:00:00) to ignore time - createdAtDate.setHours(0, 0, 0, 0); - currentDate.setHours(0, 0, 0, 0); + if (timezoneResponse.success) { + setUserTimezone(timezoneResponse.data); + } else { + console.error("Failed to fetch user timezone"); + } + } catch (error) { + setUserError("An error occurred while fetching user data"); + setUrlError("An error occurred while fetching instance URL"); + console.error("An error occurred while fetching user timezone"); + throw error; // Add this line to ensure the error is propagated + } finally { + setUserLoading(false); + setUrlLoading(false); + } + } - // Calculate difference in milliseconds - const timeDifference = currentDate.getTime() - createdAtDate.getTime(); + fetchUserAndInstanceUrl(); + }, []); - // Convert milliseconds to days - const daysPassed = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + const freeTrialDaysLeft = useMemo(() => { + if (!loggedInUser) { + return 0; + } + if (loggedInUser?.created_at?.length === 0) { + return 0; // No creation date, no trial left + } - // Free trial is 3 days, so calculate days left - const trialDaysLeft = 3 - daysPassed; + const createdAtDate = new Date(loggedInUser.created_at); // Convert to Date object + const currentDate = new Date(); // Current date + + // Set both dates to midnight (00:00:00) to ignore time + createdAtDate.setHours(0, 0, 0, 0); + currentDate.setHours(0, 0, 0, 0); + + // Calculate difference in milliseconds + const timeDifference = currentDate.getTime() - createdAtDate.getTime(); + + // Convert milliseconds to days + const daysPassed = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + + // Free trial is 3 days, so calculate days left + const trialDaysLeft = 3 - daysPassed; + + // If days left is less than 0, return 0 + return trialDaysLeft > 0 && trialDaysLeft < 4 ? trialDaysLeft : 0; + }, [loggedInUser]); + + const handleDataFilter = useCallback((filters) => { + setDataFilter(filters); + }, []); + + const handleRefresh = () => { + fetchData(true); + }; + + const fetchData = useCallback( + /** + * + * @param {boolean} isRefresh + * @param {'Today' | 'Yesterday' | 'This Week' | 'Last Week' | 'This Month' | 'Last Month' | 'This Quarter' | 'Last Quarter'} selectedPeriod + * @param {string[]} filteredIds + * @returns + */ + async (isRefresh = false, selectedPeriod = period, filteredIds = []) => { + if (inFlightRef.current) return; + inFlightRef.current = true; + setLoading(true); + setSummaryLoading(true); + try { + let response; + if (isRefresh) { + await processNewProspectingActivity(userTimezone); + } - // If days left is less than 0, return 0 - return trialDaysLeft > 0 && trialDaysLeft < 4 ? trialDaysLeft : 0; - }, [loggedInUser]); + response = await fetchProspectingActivities( + selectedPeriod, + filteredIds + ); + + if (response.statusCode === 200 && response.success) { + setSummaryData(response.data[0].summary); + setRawData(response.data[0].raw_data || []); + setOriginalRawData(response.data[0].raw_data || []); + } else if (response.statusCode === 401) { + navigate("/"); + } else { + setError(response.message); + } + } catch (err) { + setError(`An error occurred while fetching data: ${err.message}`); + console.error("Error details:", err); + throw err; + } finally { + setLoading(false); + setSummaryLoading(false); + inFlightRef.current = false; + } + }, + [navigate, period, userTimezone] + ); + + useEffect(() => { + fetchData(false, period); + }, [fetchData, period]); + + const filteredData = useMemo(() => { + if (!dataFilter) return originalRawData; + + return originalRawData.filter((item) => { + if ( + dataFilter.activatedBy.length > 0 && + !dataFilter.activatedBy.includes(item.activated_by_id) + ) + return false; + if ( + dataFilter.accountOwner.length > 0 && + !dataFilter.accountOwner.includes(item.account.owner.id) + ) + return false; + if ( + dataFilter.activatedByTeam.length > 0 && + !dataFilter.activatedByTeam.includes(item.activated_by.role) + ) + return false; + return true; + }); + }, [originalRawData, dataFilter]); + + const fetchPaginatedData = useCallback( + async (newPage, newRowsPerPage, newSearchTerm, newColumnId, newOrder) => { + setTableLoading(true); + try { + const filteredIds = filteredData.map((item) => item.id); + const response = await getPaginatedProspectingActivities( + filteredIds, + newPage, + newRowsPerPage, + newSearchTerm, + newColumnId, + newOrder + ); + if (response.statusCode === 200 && response.success) { + setDetailedActivationData(response.data[0].raw_data || []); + setTotalItems(response.data[0].total_items); + } else if (response.statusCode === 401) { + navigate("/"); + } else { + setError(response.message); + } + } catch (err) { + setError(`An error occurred while fetching data: ${err.message}`); + console.error("Error details:", err); + throw err; // Add this line to ensure the error is propagated + } finally { + setTableLoading(false); + } + }, + [filteredData, navigate] + ); - const handleDataFilter = useCallback((filters) => { - setDataFilter(filters); - }, []); + const debouncedFetchPaginatedData = useMemo( + () => debounce(fetchPaginatedData, 250), + [fetchPaginatedData] + ); - const handleRefresh = () => { - fetchData(true); + useEffect(() => { + return () => { + debouncedFetchPaginatedData.cancel(); }; + }, [debouncedFetchPaginatedData]); - const fetchData = useCallback( - /** - * - * @param {boolean} isRefresh - * @param {'Today' | 'Yesterday' | 'This Week' | 'Last Week' | 'This Month' | 'Last Month' | 'This Quarter' | 'Last Quarter'} selectedPeriod - * @param {string[]} filteredIds - * @returns - */ - async ( - isRefresh = false, - selectedPeriod = period, - filteredIds = [] - ) => { - if (inFlightRef.current) return; - inFlightRef.current = true; - setLoading(true); - setSummaryLoading(true); - try { - let response; - if (isRefresh) { - await processNewProspectingActivity(userTimezone); - } - - response = await fetchProspectingActivities( - selectedPeriod, - filteredIds - ); - - if (response.statusCode === 200 && response.success) { - setSummaryData(response.data[0].summary); - setRawData(response.data[0].raw_data || []); - setOriginalRawData(response.data[0].raw_data || []); - } else if (response.statusCode === 401) { - navigate("/"); - } else { - setError(response.message); - } - } catch (err) { - setError( - `An error occurred while fetching data: ${err.message}` - ); - console.error("Error details:", err); - throw err; - } finally { - setLoading(false); - setSummaryLoading(false); - inFlightRef.current = false; - } - }, - [navigate, period, userTimezone] - ); - - useEffect(() => { - fetchData(false, period); - }, [fetchData, period]); - - const filteredData = useMemo(() => { - if (!dataFilter) return originalRawData; - - return originalRawData.filter((item) => { - if ( - dataFilter.activatedBy.length > 0 && - !dataFilter.activatedBy.includes(item.activated_by_id) - ) - return false; - if ( - dataFilter.accountOwner.length > 0 && - !dataFilter.accountOwner.includes(item.account.owner.id) - ) - return false; - if ( - dataFilter.activatedByTeam.length > 0 && - !dataFilter.activatedByTeam.includes(item.activated_by.role) - ) - return false; - return true; - }); - }, [originalRawData, dataFilter]); - - const fetchPaginatedData = useCallback( - async (newPage, newRowsPerPage, newSearchTerm) => { - setTableLoading(true); - try { - const filteredIds = filteredData.map((item) => item.id); - const response = await getPaginatedProspectingActivities( - filteredIds, - newPage, - newRowsPerPage, - newSearchTerm - ); - if (response.statusCode === 200 && response.success) { - setDetailedActivationData(response.data[0].raw_data || []); - setTotalItems(response.data[0].total_items); - } else if (response.statusCode === 401) { - navigate("/"); - } else { - setError(response.message); - } - } catch (err) { - setError( - `An error occurred while fetching data: ${err.message}` - ); - console.error("Error details:", err); - throw err; // Add this line to ensure the error is propagated - } finally { - setTableLoading(false); - } - }, - [filteredData, navigate] - ); - - const debouncedFetchPaginatedData = useMemo( - () => debounce(fetchPaginatedData, 250), - [fetchPaginatedData] - ); - - useEffect(() => { - return () => { - debouncedFetchPaginatedData.cancel(); - }; - }, [debouncedFetchPaginatedData]); - - useEffect(() => { - if (filteredData.length > 0) { - debouncedFetchPaginatedData(page, rowsPerPage, searchTerm); - } - }, [ - debouncedFetchPaginatedData, + useEffect(() => { + if (filteredData.length > 0) { + debouncedFetchPaginatedData( page, rowsPerPage, - filteredData, searchTerm, - ]); - - const handlePageChange = (newPage, newRowsPerPage) => { - setPage(newPage); - setRowsPerPage(newRowsPerPage); - // The debounced function will be called in the useEffect - }; - - const handleRowsPerPageChange = (newRowsPerPage) => { - setRowsPerPage(newRowsPerPage); - setPage(0); - // The debounced function will be called in the useEffect - }; - - useEffect(() => { + sortConfig.columnId, + sortConfig.order + ); + } + }, [ + debouncedFetchPaginatedData, + page, + rowsPerPage, + filteredData, + searchTerm, + sortConfig.columnId, + sortConfig.order, + ]); + + const handlePageChange = (newPage, newRowsPerPage) => { + setPage(newPage); + setRowsPerPage(newRowsPerPage); + // The debounced function will be called in the useEffect + }; + + const handleRowsPerPageChange = (newRowsPerPage) => { + setRowsPerPage(newRowsPerPage); + setPage(0); + // The debounced function will be called in the useEffect + }; + + useEffect(() => { + const filteredIds = filteredData.map((item) => item.id); + fetchData(false, period, filteredIds); + }, [dataFilter, period]); + + const handlePeriodChange = (event) => { + const newPeriod = event.target.value; + setPeriod(newPeriod); + }; + + const [selectedActivation, setSelectedActivation] = useState(null); + + const handleRowClick = (activation) => { + setSelectedActivation(activation); + }; + + useEffect(() => { + const fetchFilteredSummary = async () => { + setSummaryLoading(true); + try { const filteredIds = filteredData.map((item) => item.id); - fetchData(false, period, filteredIds); - }, [dataFilter, period]); - - const handlePeriodChange = (event) => { - const newPeriod = event.target.value; - setPeriod(newPeriod); - }; - - const [selectedActivation, setSelectedActivation] = useState(null); - - const handleRowClick = (activation) => { - setSelectedActivation(activation); - }; - - useEffect(() => { - const fetchFilteredSummary = async () => { - setSummaryLoading(true); - try { - const filteredIds = filteredData.map((item) => item.id); - await fetchData(false, period, filteredIds); - } catch (err) { - setError( - `An error occurred while generating the summary. ${err}` - ); - throw err; - } finally { - setSummaryLoading(false); - } - }; - - if (filteredData.length > 0) { - fetchFilteredSummary(); - } else { - setSummaryData({ - total_activations: 0, - activations_today: 0, - avg_tasks_per_contact: 0, - avg_contacts_per_account: 0, - total_tasks: 0, - total_events: 0, - total_contacts: 0, - total_accounts: 0, - total_deals: 0, - total_pipeline_value: 0, - engaged_activations: 0, - }); - } - }, [dataFilter, period]); - - const handleSearch = (newSearchTerm) => { - setSearchTerm(newSearchTerm); - setPage(0); // Reset to first page when searching - fetchPaginatedData(0, rowsPerPage, newSearchTerm); + await fetchData(false, period, filteredIds); + } catch (err) { + setError(`An error occurred while generating the summary. ${err}`); + throw err; + } finally { + setSummaryLoading(false); + } }; - if (loading || summaryLoading || userLoading || urlLoading) { - return ; + if (filteredData.length > 0) { + fetchFilteredSummary(); + } else { + setSummaryData({ + total_activations: 0, + activations_today: 0, + avg_tasks_per_contact: 0, + avg_contacts_per_account: 0, + total_tasks: 0, + total_events: 0, + total_contacts: 0, + total_accounts: 0, + total_deals: 0, + total_pipeline_value: 0, + engaged_activations: 0, + }); } - - if (error || userError || urlError) { - return {error || userError || urlError}; - } - - if (loggedInUser?.status !== "paid" && freeTrialDaysLeft === 0) { - return ; - } - - return ( + }, [dataFilter, period]); + + const handleSearch = (newSearchTerm) => { + setSearchTerm(newSearchTerm); + setPage(0); // Reset to first page when searching + fetchPaginatedData(0, rowsPerPage, newSearchTerm); + }; + + if (loading || summaryLoading || userLoading || urlLoading) { + return ; + } + + if (error || userError || urlError) { + return {error || userError || urlError}; + } + + if (loggedInUser?.status !== "paid" && freeTrialDaysLeft === 0) { + return ; + } + + return ( + + - + + + + + - - - - - - - - - Detailed - { - setIsSummary((prev) => !prev); - }} - inputProps={{ "aria-label": "ant design" }} - /> - Summary - - - - { - if ( - loggedInUser?.status === "not paid" && - freeTrialDaysLeft === 0 - ) { - return; - } - handleRefresh(); - }} - color="primary" - size="small" - > - - - - - - {error ? ( - {error} - ) : isSummary && !summaryLoading ? ( - - ) : ( - <> - - {selectedActivation && ( - - )} - - )} - - {loggedInUser?.status === "not paid" && freeTrialDaysLeft > 0 && ( - - )} + + Detailed + { + setIsSummary((prev) => !prev); + }} + inputProps={{ "aria-label": "ant design" }} + /> + Summary + + + + { + if ( + loggedInUser?.status === "not paid" && + freeTrialDaysLeft === 0 + ) { + return; + } + handleRefresh(); + }} + color="primary" + size="small" + > + + + + - ); + {error ? ( + {error} + ) : isSummary && !summaryLoading ? ( + + ) : ( + <> + + {selectedActivation && ( + + )} + + )} + + {loggedInUser?.status === "not paid" && freeTrialDaysLeft > 0 && ( + + )} + + ); }; export default Prospecting; From caa57c0475a0834d1a3c155a5e778e421514dec2 Mon Sep 17 00:00:00 2001 From: ter1203 Date: Wed, 16 Oct 2024 17:22:29 -0300 Subject: [PATCH 08/12] fix: link account redirect new tab --- .../src/components/CustomTable/CustomTable.js | 1 - .../ProspectingActiveAccount/AccountDetail.js | 49 ++++++++++--------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/client/src/components/CustomTable/CustomTable.js b/client/src/components/CustomTable/CustomTable.js index f11114e..f574257 100644 --- a/client/src/components/CustomTable/CustomTable.js +++ b/client/src/components/CustomTable/CustomTable.js @@ -155,7 +155,6 @@ const CustomTable = ({ const newOrder = isAsc ? "desc" : "asc"; setOrder(newOrder); setOrderBy(columnId); - if (sortConfig?.onSort) { sortConfig.onSort(columnId, newOrder); } diff --git a/client/src/components/ProspectingActiveAccount/AccountDetail.js b/client/src/components/ProspectingActiveAccount/AccountDetail.js index 8c11511..0be4a07 100644 --- a/client/src/components/ProspectingActiveAccount/AccountDetail.js +++ b/client/src/components/ProspectingActiveAccount/AccountDetail.js @@ -1,7 +1,6 @@ import { Box, Card, Grid, Typography } from "@mui/material"; import CustomTable from "../CustomTable/CustomTable"; import CardActiveAccount from "./CardActiveAccount"; -import { Link } from "react-router-dom"; import { tableColumns } from "../../pages/Prospecting/tableColumns"; import { useState } from "react"; @@ -61,29 +60,31 @@ const AccountDetail = ({ ({ - ...item, - "account.name": ( - - {item.account?.name || "N/A"} - - ), - "opportunity.name": item.opportunity ? ( - - {item.opportunity.name || "N/A"} - - ) : ( - "N/A" - ), - })), + data: detailedActivationData.map((item) => { + return { + ...item, + "account.name": ( + + {item.account?.name || "N/A"} + + ), + "opportunity.name": item.opportunity ? ( + + {item.opportunity.name || "N/A"} + + ) : ( + "N/A" + ), + }; + }), selectedIds: new Set(), availableColumns: tableColumns, }} From c6910d5c6d0e0c3d6c02921739318982e0cb75f8 Mon Sep 17 00:00:00 2001 From: ter1203 Date: Wed, 16 Oct 2024 19:01:55 -0300 Subject: [PATCH 09/12] implement timeline part --- .../ProspectingActiveAccount/TimeLine.js | 165 +++++++++++++----- 1 file changed, 126 insertions(+), 39 deletions(-) diff --git a/client/src/components/ProspectingActiveAccount/TimeLine.js b/client/src/components/ProspectingActiveAccount/TimeLine.js index a2eb9ef..9c00a7d 100644 --- a/client/src/components/ProspectingActiveAccount/TimeLine.js +++ b/client/src/components/ProspectingActiveAccount/TimeLine.js @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from "react"; -import { Typography, Box } from "@mui/material"; +import { useEffect, useState } from "react"; +import { Typography, Box, Tooltip } from "@mui/material"; import { AccessTime, Call, @@ -9,46 +9,136 @@ import { } from "@mui/icons-material"; // Function to map icon names to actual icons -const getIcon = (iconName, color) => { +const getIcon = (iconName, color, total) => { + let title = `${total} ${iconName}`; switch (iconName) { case "Message": - return ; + return ( + + + + ); case "Dial": - return ; + return ( + + + + ); case "Meeting": - return ; + return ( + + + + ); case "Opportunity": - return ; + return ( + + + + ); default: - return ; + return ( + + + + ); } }; +// Helper function to format the date +const formatDate = (dateStr) => { + const date = new Date(dateStr); + const year = date.getFullYear(); + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-indexed + return `${day}/${month}/${year}`; +}; + +// Helper function to generate all dates for the past month +function generateDatesForLastMonth() { + const now = new Date(); + const oneMonthAgo = new Date(); + oneMonthAgo.setDate(now.getDate() - 30); // Set the date 30 days ago + + const dates = []; + let currentDate = new Date(oneMonthAgo); + + while (currentDate <= now) { + dates.push(formatDate(currentDate)); + currentDate.setDate(currentDate.getDate() + 1); // Move to the next day + } + + return dates; +} + const MyTimelineComponent = ({ tasks }) => { - // Sample data const [text, setText] = useState(""); const [data, setData] = useState([]); const [page, setPage] = useState(1); const [maxPage, setMaxPage] = useState(1); useEffect(() => { - let array = tasks.map((e, index) => { - const date = new Date(e.CreatedDate); - const day = String(date.getDate()).padStart(2, "0"); - const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-indexed - const formattedDate = `${day}/${month}`; - let format = "top"; - let color = "#7AAD67"; - let icon = e.Subject; - if (e.Priority === "Priority") color = "#DD4040"; - let obj = { id: index, icon, date: formattedDate, color, format }; - return obj; + const allDates = generateDatesForLastMonth(); + + const result = allDates.map((formattedDate) => { + // Find tasks for the current date + const tasksForDate = tasks.filter( + (task) => formatDate(new Date(task.CreatedDate)) === formattedDate + ); + + if (tasksForDate.length > 0) { + // There are tasks for this date, process them as before + const detail = []; + let total = 0; + let icons = []; + let color = "#7AAD67"; // Default color + + tasksForDate.forEach((task) => { + const icon = task.Subject; + total++; + color = task.Priority === "Priority" ? "#DD4040" : "#7AAD67"; // Set color based on Priority + + const detailEntry = detail.find((d) => d[0] === icon); + if (detailEntry) { + detailEntry[1]++; + } else { + detail.push([icon, 1]); + icons.push(icon); + } + }); + + return { + total, + detail, + icons, + date: formattedDate, + color, + format: "top", + }; + } else { + // No tasks for this date + return { + total: 0, + detail: [], + icons: [], + date: formattedDate, + color: "#7AAD67", // Default color + format: "top", + }; + } }); - setText(array[0].date + " - " + array[array.length - 1].date); - let maxPage = Math.ceil(array.length / 8); + + // Update the state + setData(result); + setText(`${result[0].date} - ${result[result.length - 1].date}`); + + const maxPage = Math.ceil(result.length / 8); setPage(maxPage); setMaxPage(maxPage); - setData(array); }, [tasks]); return ( @@ -121,7 +211,7 @@ const MyTimelineComponent = ({ tasks }) => { display="flex" flexGrow="1" alignItems="center" - justifyContent="start" + justifyContent="center" > { {data - .filter((e, i) => i > (page - 1) * 8 && i <= page * 8) + .filter((e, i) => i >= (page - 1) * 8 && i <= page * 8) .map((el, index) => ( { justifyContent="center" sx={{ height: 200 }} > - {el.format == "bottom" && index < data.length && ( - - )} - {getIcon(el.icon, el.color)} + + {el.detail.map((e) => getIcon(e[0], el.color, e[1]))} + {el.format == "top" && index < data.length && ( Date: Fri, 18 Oct 2024 08:31:49 -0500 Subject: [PATCH 10/12] get ordered list of prospecting metadata --- client/src/components/Api/Api.js | 13 +++++ client/src/pages/Prospecting/Prospecting.js | 3 + server/app/database/activation_selector.py | 24 ++++++++ server/app/helpers/activation_helper.py | 62 +++------------------ server/app/routes.py | 24 ++++++-- 5 files changed, 67 insertions(+), 59 deletions(-) diff --git a/client/src/components/Api/Api.js b/client/src/components/Api/Api.js index d24842a..09d5d24 100644 --- a/client/src/components/Api/Api.js +++ b/client/src/components/Api/Api.js @@ -317,6 +317,19 @@ export const fetchSalesforceTasksByUserIds = async (userIds) => { return { ...response.data, statusCode: response.status }; }; +/** + * Fetches the prospecting activity type groupings for a given activation + * @param {string} activationId + * @returns {Promise} + */ +export const fetchProspectingActivityTypeGroupings = async (activationId) => { + const response = await api.get("/get_prospecting_activity_type_groupings", { + params: { activation_id: activationId }, + validateStatus: () => true, + }); + return { ...response.data, statusCode: response.status }; +}; + /** * Fetches Salesforce events from the Salesforce API * @param {string[]} userIds diff --git a/client/src/pages/Prospecting/Prospecting.js b/client/src/pages/Prospecting/Prospecting.js index b40199d..c423ff6 100644 --- a/client/src/pages/Prospecting/Prospecting.js +++ b/client/src/pages/Prospecting/Prospecting.js @@ -27,6 +27,7 @@ import { getLoggedInUser, getUserTimezone, getPaginatedProspectingActivities, + fetchProspectingActivityTypeGroupings } from "src/components/Api/Api"; import AccountDetail from "../../components/ProspectingActiveAccount/AccountDetail"; import CustomSelect from "src/components/CustomSelect/CustomSelect"; @@ -227,6 +228,8 @@ const Prospecting = () => { if (response.statusCode === 200 && response.success) { setSummaryData(response.data[0].summary); setRawData(response.data[0].raw_data || []); + const testPmGroup = await fetchProspectingActivityTypeGroupings(response.data[0].raw_data[0].id); + console.log(testPmGroup); setOriginalRawData(response.data[0].raw_data || []); } else if (response.statusCode === 401) { navigate("/"); diff --git a/server/app/database/activation_selector.py b/server/app/database/activation_selector.py index 56d7636..8673a76 100644 --- a/server/app/database/activation_selector.py +++ b/server/app/database/activation_selector.py @@ -346,3 +346,27 @@ def load_active_activations_paginated_with_search( return ApiResponse( success=False, message=f"Failed to load activations: {str(error_msg)}" ) + +def load_single_activation_by_id(activation_id: str) -> ApiResponse: + supabase_client = get_supabase_admin_client() + + try: + response = ( + supabase_client.table("Activations") + .select("*") + .eq("id", activation_id) + .limit(1) + .execute() + ) + if response.data: + activation = supabase_dict_to_python_activation(response.data[0]) + return ApiResponse(data=[activation], success=True) + else: + return ApiResponse(data=[], success=False, message="Activation not found") + except Exception as e: + error_msg = format_error_message(e) + logging.error(f"Error in load_single_activation_by_id: {error_msg}") + return ApiResponse( + success=False, message=f"Failed to load activation: {str(error_msg)}" + ) + \ No newline at end of file diff --git a/server/app/helpers/activation_helper.py b/server/app/helpers/activation_helper.py index e8a50cb..93c2f2b 100644 --- a/server/app/helpers/activation_helper.py +++ b/server/app/helpers/activation_helper.py @@ -759,60 +759,12 @@ def get_inbound_tasks_within_period(inbound_tasks, start_date, period_days): ] -def get_prospecting_metadata_direction( - task_id: str, activation: Activation, settings: Settings -) -> str: - for metadata in activation.prospecting_metadata: - if task_id in metadata.task_ids: - for criterion in settings.criteria: - if criterion.name == metadata.name: - return ( - criterion.direction or "outbound" - ) # Default to outbound if direction is not specified - return "matching_criteria_not_found" - -def set_effort_and_results_by_full_user_name( - activations: List[Activation], settings: Settings -) -> List[Activation]: - for activation in activations: - outbound_metadata = defaultdict(lambda: defaultdict(list)) - inbound_metadata = defaultdict(lambda: defaultdict(list)) - - for task in activation.tasks: - direction = get_prospecting_metadata_direction( - task["Id"], activation, settings - ) - metadata_dict = ( - outbound_metadata if direction.lower() == "outbound" else inbound_metadata - ) - - owner_id = task["OwnerId"] - criterion_name = next( - ( - m.name - for m in activation.prospecting_metadata - if task["Id"] in m.task_ids - ), - None, - ) - - if criterion_name: - metadata_dict[owner_id][criterion_name].append(task["Id"]) - - activation.outbound_prospecting_metadata_by_user = { - owner_id: [ - ProspectingMetadata(name=name, task_ids=set(ids), total=len(ids)) - for name, ids in criteria.items() - ] - for owner_id, criteria in outbound_metadata.items() - } +def fetch_prospecting_activity_type_groupings( + activation: Activation +) -> List[ProspectingMetadata]: + if not activation.prospecting_metadata: + return [] - activation.inbound_prospecting_metadata_by_user = { - owner_id: [ - ProspectingMetadata(name=name, task_ids=set(ids), total=len(ids)) - for name, ids in criteria.items() - ] - for owner_id, criteria in inbound_metadata.items() - } + sorted_metadata = sorted(activation.prospecting_metadata, key=lambda x: x.total) - return activations + return sorted_metadata diff --git a/server/app/routes.py b/server/app/routes.py index 4bf1e92..a778600 100644 --- a/server/app/routes.py +++ b/server/app/routes.py @@ -5,6 +5,7 @@ from app.utils import format_error_message, log_error, get_salesforce_team_ids from app.database.activation_selector import ( load_active_activations_minimal_by_ids, + load_single_activation_by_id, load_activations_by_period, load_active_activations_paginated_by_ids, load_active_activations_paginated_with_search, @@ -25,7 +26,7 @@ ) from app.helpers.activation_helper import ( generate_summary, - set_effort_and_results_by_full_user_name, + fetch_prospecting_activity_type_groupings ) from app.services.setting_service import define_criteria_from_events_or_tasks from app.engine.activation_engine import update_activation_states @@ -330,9 +331,6 @@ def get_paginated_prospecting_activities(): if result.success: full_activations = result.data["activations"] - full_activations = set_effort_and_results_by_full_user_name( - full_activations, load_settings() - ) response.data = [ { "raw_data": [ @@ -642,6 +640,24 @@ def get_event_fields(): return jsonify(response.__dict__), get_status_code(response) +@bp.route("/get_prospecting_activity_type_groupings", methods=["GET"]) +@authenticate +def get_prospecting_activity_type_groupings(): + from app.data_models import ApiResponse + activation_id = request.args.get("activation_id") + response = ApiResponse(data=[], message="", success=False) + try: + activation = load_single_activation_by_id(activation_id) + if not activation.success: + raise Exception(activation.message) + response.data = fetch_prospecting_activity_type_groupings(activation.data[0]) + response.success = True + except Exception as e: + log_error(e) + response.message = f"Failed to retrieve prospecting activity type groupings: {format_error_message(e)}" + + return jsonify(response.to_dict()), get_status_code(response) + @bp.route("/get_task_query_count", methods=["POST"]) @authenticate From 7d8492e599e23171e6524d501cd9291f16ba18fc Mon Sep 17 00:00:00 2001 From: ter1203 Date: Fri, 18 Oct 2024 15:03:01 -0300 Subject: [PATCH 11/12] display CardActiveAccount when its needed --- .../ProspectingActiveAccount/AccountDetail.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/src/components/ProspectingActiveAccount/AccountDetail.js b/client/src/components/ProspectingActiveAccount/AccountDetail.js index 0be4a07..d7ed7bf 100644 --- a/client/src/components/ProspectingActiveAccount/AccountDetail.js +++ b/client/src/components/ProspectingActiveAccount/AccountDetail.js @@ -32,7 +32,7 @@ const AccountDetail = ({ return ( - + - - - + {selectedActivation && ( + + + + )} ); From c320c20511de8a6f7dce96e2b1d9b8759609551c Mon Sep 17 00:00:00 2001 From: ter1203 Date: Sat, 19 Oct 2024 13:28:18 -0300 Subject: [PATCH 12/12] feat: prospecting metadata chart --- .../SelectedAccount.js | 53 +--- .../SummaryCard/SummaryBarChartCard.js | 291 +++++++++--------- client/src/pages/Prospecting/Prospecting.js | 7 +- 3 files changed, 161 insertions(+), 190 deletions(-) diff --git a/client/src/components/ProspectingActiveAccount/SelectedAccount.js b/client/src/components/ProspectingActiveAccount/SelectedAccount.js index 924a0d3..744ca63 100644 --- a/client/src/components/ProspectingActiveAccount/SelectedAccount.js +++ b/client/src/components/ProspectingActiveAccount/SelectedAccount.js @@ -3,24 +3,6 @@ import { Box, Card, Grid } from "@mui/material"; import SummaryBarChartCard from "../../components/SummaryCard/SummaryBarChartCard"; function SelectedAccount({ selectedActivation }) { - const mockData = [ - { - label: "User 1", - value: 6, - }, - { - label: "User 2", - value: 4, - }, - { - label: "User 3", - value: 7, - }, - { - label: "User 4", - value: 2, - }, - ]; return ( - + - - -
- - - - - - ({ + label: item.name, + value: item.total, + }))} /> diff --git a/client/src/components/SummaryCard/SummaryBarChartCard.js b/client/src/components/SummaryCard/SummaryBarChartCard.js index dc1b1cb..ffac66a 100644 --- a/client/src/components/SummaryCard/SummaryBarChartCard.js +++ b/client/src/components/SummaryCard/SummaryBarChartCard.js @@ -1,157 +1,166 @@ -import React, { useEffect, useRef, useState } from 'react' -import { Box, Tooltip, Typography } from '@mui/material' -import { BarChart } from '@mui/x-charts/BarChart'; - +import React, { useEffect, useRef, useState } from "react"; +import { Box, Tooltip, Typography } from "@mui/material"; +import { BarChart } from "@mui/x-charts/BarChart"; const OrangeBlueGradientColorDefs = () => { - return ( - <> - - - - - - - - ); + return ( + <> + + + + + + + + ); }; /** * @param {object} props - * @param {string} [props.tooltipTitle = ''] - * @param {any[]} props.data - * @param {number} [props.target] - * @param {string} props.title - * @param {'horizontal' | 'vertical'} props.direction + * @param {string} [props.tooltipTitle = ''] + * @param {any[]} props.data + * @param {number} [props.target] + * @param {string} props.title + * @param {'horizontal' | 'vertical'} props.direction */ -const SummaryBarChartCard = ({ tooltipTitle = '', data, target, title, direction }) => { - const textRef = useRef(null) - const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); - - useEffect(() => { - if (textRef.current) { - const { offsetWidth, offsetHeight } = textRef.current; - setDimensions({ width: offsetWidth, height: offsetHeight }); - } - }, [textRef]); +const SummaryBarChartCard = ({ + tooltipTitle = "", + data, + target, + title, + direction, +}) => { + const textRef = useRef(null); + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); - return ( - - - - {title} - - {target && ( - - TARGET = {target} - - )} - { + useEffect(() => { + if (textRef.current) { + const { offsetWidth, offsetHeight } = textRef.current; + setDimensions({ width: offsetWidth, height: offsetHeight }); + } + }, [textRef]); - if (!target) { - return 'url(#paint0_linear_bar1)' - } - - if (val.value >= target) { - return 'url(#paint0_linear_bar1)' - } else { - return 'rgba(217, 217, 217, 1)' - } - }) - } : undefined, - categoryGapRatio: 0.5, + return ( + + + + {title} + + {target && ( + + TARGET = {target} + + )} + { + if (!target) { + return "url(#paint0_linear_bar1)"; } - ]} // Keep band for categorical labels - grid={{ - vertical: direction === "vertical" ? true : false, - horizontal: direction === "horizontal" ? true : false, - }} - series={[{ dataKey: 'value' }]} - layout={direction === "vertical" ? "horizontal" : "vertical"} - xAxis={[ - { - dataKey: direction === "vertical" ? "value" : "label", - scaleType: direction === "vertical" ? 'linear' : 'band', - tickMinStep: 2, - colorMap: direction === "horizontal" ? { - type: 'ordinal', - colors: data.map((val) => { - if (val.label === "5. Unresponsive") { - return 'rgba(221, 64, 64, 1)' - } + if (val.value >= target) { + return "url(#paint0_linear_bar1)"; + } else { + return "rgba(217, 217, 217, 1)"; + } + }), + } + : undefined, + categoryGapRatio: 0.5, + }, + ]} // Keep band for categorical labels + grid={{ + vertical: direction === "vertical" ? true : false, + horizontal: direction === "horizontal" ? true : false, + }} + series={[{ dataKey: "value" }]} + layout={direction === "vertical" ? "horizontal" : "vertical"} + xAxis={[ + { + dataKey: direction === "vertical" ? "value" : "label", + scaleType: direction === "vertical" ? "linear" : "band", + tickMinStep: 2, + colorMap: + direction === "horizontal" + ? { + type: "ordinal", + colors: data.map((val) => { + if (val.label === "5. Unresponsive") { + return "rgba(221, 64, 64, 1)"; + } - if (!target) { - return 'url(#paint0_linear_bar1)' - } + if (!target) { + return "url(#paint0_linear_bar1)"; + } - if (val.value >= target) { - return 'url(#paint0_linear_bar1)' - } else { - return 'rgba(217, 217, 217, 1)' - } - }) - } : undefined, - categoryGapRatio: 0.5, - }, - ]} - > - - - - - ) + if (val.value >= target) { + return "url(#paint0_linear_bar1)"; + } else { + return "rgba(217, 217, 217, 1)"; + } + }), + } + : undefined, + categoryGapRatio: 0.5, + }, + ]} + > + + + + + ); }; export default SummaryBarChartCard; diff --git a/client/src/pages/Prospecting/Prospecting.js b/client/src/pages/Prospecting/Prospecting.js index c423ff6..fd15ba6 100644 --- a/client/src/pages/Prospecting/Prospecting.js +++ b/client/src/pages/Prospecting/Prospecting.js @@ -27,7 +27,7 @@ import { getLoggedInUser, getUserTimezone, getPaginatedProspectingActivities, - fetchProspectingActivityTypeGroupings + // fetchProspectingActivityTypeGroupings, } from "src/components/Api/Api"; import AccountDetail from "../../components/ProspectingActiveAccount/AccountDetail"; import CustomSelect from "src/components/CustomSelect/CustomSelect"; @@ -228,8 +228,9 @@ const Prospecting = () => { if (response.statusCode === 200 && response.success) { setSummaryData(response.data[0].summary); setRawData(response.data[0].raw_data || []); - const testPmGroup = await fetchProspectingActivityTypeGroupings(response.data[0].raw_data[0].id); - console.log(testPmGroup); + // const pmDataGrouping = await fetchProspectingActivityTypeGroupings( + // response.data[0].raw_data[0].id + // ); setOriginalRawData(response.data[0].raw_data || []); } else if (response.statusCode === 401) { navigate("/");