From 48f5981e131666a366a27cc686dee9a47c93a025 Mon Sep 17 00:00:00 2001 From: ter1203 Date: Thu, 29 Aug 2024 14:42:54 -0300 Subject: [PATCH 1/5] feat: step 1 --- client/src/App.js | 2 +- client/src/components/Logo/Logo.js | 16 + .../ProgressTracker/ProgressTracker.js | 73 ++- client/src/index.css | 19 +- client/src/pages/Onboard.js | 192 +++++--- client/src/pages/Onboard/Onboard.js | 445 ++++++++++++++++++ client/src/pages/Onboard/RoleStep.js | 201 ++++++++ client/src/utils/c.js | 8 +- 8 files changed, 878 insertions(+), 78 deletions(-) create mode 100644 client/src/components/Logo/Logo.js create mode 100644 client/src/pages/Onboard/Onboard.js create mode 100644 client/src/pages/Onboard/RoleStep.js diff --git a/client/src/App.js b/client/src/App.js index b89f691..a7d0530 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -10,7 +10,7 @@ import { Box } from "@mui/material"; import Sidebar from "./components/Sidebar/Sidebar"; import MainContent from "./components/MainContent"; import Login from "./pages/Login"; -import Onboard from "./pages/Onboard"; +import Onboard from "./pages/Onboard/Onboard"; import "./App.css"; const AppRoutes = () => { diff --git a/client/src/components/Logo/Logo.js b/client/src/components/Logo/Logo.js new file mode 100644 index 0000000..ff04777 --- /dev/null +++ b/client/src/components/Logo/Logo.js @@ -0,0 +1,16 @@ +const Logo = () => { + return ( + + + + + + + + + + + ) +} + +export default Logo \ No newline at end of file diff --git a/client/src/components/ProgressTracker/ProgressTracker.js b/client/src/components/ProgressTracker/ProgressTracker.js index 043eb11..2950cf0 100644 --- a/client/src/components/ProgressTracker/ProgressTracker.js +++ b/client/src/components/ProgressTracker/ProgressTracker.js @@ -1,6 +1,40 @@ import React from "react"; -import { Box, Stepper, Step, StepButton, Typography } from "@mui/material"; +import { Box, Stepper, Step, StepLabel } from "@mui/material"; +/** @typedef {import("types/OnboardWizardStep").OnboardWizardStep} OnboardWizardStep */ +/** @typedef {{steps: (OnboardWizardStep | {title: string;})[]; currentStep: number; onStepClick: (stepIndex: number) => void; orientation: import("@mui/material").Orientation }} Props */ + +function CheckIcon() { + return ( + + + + ) +} + +/** @param {import("@mui/material").StepIconProps} props */ +function ColorlibStepIcon(props) { + const { active, completed } = props; + + return ( +
+ {completed ? : props.icon} +
+ ) +} + +/** @param {Props} props */ const ProgressTracker = ({ steps, currentStep, @@ -9,15 +43,38 @@ const ProgressTracker = ({ }) => { return ( - + {steps.map((step, index) => ( - - onStepClick(index + 1)} - disabled={index + 1 > currentStep} + onStepClick(index + 1)} + disabled={index + 1 > currentStep} + sx={{ + cursor: "pointer" + }} + > + - {step.title} - + {step.title} + ))} diff --git a/client/src/index.css b/client/src/index.css index ec2585e..7aa0ba9 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,13 +1,26 @@ +@import url("https://fonts.googleapis.com/css2?family=Albert+Sans:ital,wght@0,100..900;1,100..900&display=swap"); + body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +p, +h1, +h2, +h3, +h4, +h5, +h6, +div { + font-family: "Albert Sans", sans-serif; +} + code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } diff --git a/client/src/pages/Onboard.js b/client/src/pages/Onboard.js index 03b50a3..af3520c 100644 --- a/client/src/pages/Onboard.js +++ b/client/src/pages/Onboard.js @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from "react"; -import { Dialog, DialogContent, Box, Paper, Typography } from "@mui/material"; +import { Dialog, DialogContent, Box, Paper, Typography, Divider } from "@mui/material"; import { useNavigate } from "react-router-dom"; import ProspectingCriteriaSelector from "../components/ProspectingCriteriaSelector/ProspectingCriteriaSelector"; import InfoGatheringStep from "../components/InfoGatheringStep/InfoGatheringStep"; @@ -25,6 +25,7 @@ import { */ import { ONBOARD_WIZARD_STEPS } from "../utils/c"; +import Logo from "src/components/Logo/Logo"; const REQUIRED_PROSPECTING_CATEGORIES = [ "Inbound Call", @@ -69,40 +70,40 @@ const Onboard = () => { taskSObjectFields.current.length > 0 ? taskSObjectFields.current : (await fetchTaskFields()).data.map( - /** @param {SObjectField} field */ - (field) => ({ - id: field.name, - label: field.label, - dataType: field.type, - }) - ); + /** @param {SObjectField} field */ + (field) => ({ + id: field.name, + label: field.label, + dataType: field.type, + }) + ); setCategoryFormTableData({ availableColumns: taskSObjectFields.current, columns: categoryFormTableData.columns.length > 0 ? categoryFormTableData.columns : [ - { - id: "select", - label: "Select", - dataType: "select", - }, - { - id: "Subject", - label: "Subject", - dataType: "string", - }, - { - id: "Status", - label: "Status", - dataType: "string", - }, - { - id: "TaskSubtype", - label: "TaskSubtype", - dataType: "string", - }, - ], + { + id: "select", + label: "Select", + dataType: "select", + }, + { + id: "Subject", + label: "Subject", + dataType: "string", + }, + { + id: "Status", + label: "Status", + dataType: "string", + }, + { + id: "TaskSubtype", + label: "TaskSubtype", + dataType: "string", + }, + ], data: tasks, selectedIds: new Set(), }); @@ -190,8 +191,8 @@ const Onboard = () => { settings[key] === "Yes" ? true : settings[key] === "No" - ? false - : settings[key]; + ? false + : settings[key]; return acc; }, {}); @@ -367,17 +368,17 @@ const Onboard = () => { border: "1px solid #e0e0e0", ...(isLargeDialogStep() ? { - maxWidth: "60vw", - width: "60vw", - maxHeight: "90vh", - height: "90vh", - } + maxWidth: "60vw", + width: "60vw", + maxHeight: "90vh", + height: "90vh", + } : { - maxWidth: "600px", // Adjust as needed for small dialog - width: "100%", - maxHeight: "80vh", - height: "auto", - }), + maxWidth: "600px", // Adjust as needed for small dialog + width: "100%", + maxHeight: "80vh", + height: "auto", + }), }; /** @@ -389,22 +390,29 @@ const Onboard = () => { }; return ( - + + {/* Sidebar */} +
+ +
+ + { orientation="vertical" />
- - { - console.log("closing"); - }} - PaperProps={{ - style: dialogStyle, + +
- + YOUR DATA VISUALIZED +

+

+ Welcome to your +

+

+ Onboarding +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Quis ipsum suspendisse ultrices gravida. +

+
{renderStep()} - -
+ + +
); diff --git a/client/src/pages/Onboard/Onboard.js b/client/src/pages/Onboard/Onboard.js new file mode 100644 index 0000000..0c4c5a8 --- /dev/null +++ b/client/src/pages/Onboard/Onboard.js @@ -0,0 +1,445 @@ +import React, { useState, useEffect, useRef } from "react"; +import { Box, Paper, Typography, Divider } from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import ProspectingCriteriaSelector from "../../components/ProspectingCriteriaSelector/ProspectingCriteriaSelector"; +import InfoGatheringStep from "../../components/InfoGatheringStep/InfoGatheringStep"; +import ProgressTracker from "../../components/ProgressTracker/ProgressTracker"; +import { + fetchLoggedInSalesforceUser, + fetchSalesforceTasksByUserIds, + fetchTaskFields, + fetchTaskFilterFields, + generateCriteria, + saveSettings as saveSettingsToSupabase, +} from "../../components/Api/Api"; + +/** + * @typedef {import('types').SObject} SObject + * @typedef {import('types').SObjectField} SObjectField + * @typedef {import('types').Settings} Settings + * @typedef {import('types').FilterContainer} FilterContainer + * @typedef {import('types').ApiResponse} ApiResponse + * @typedef {import('types').TableData} TableData + * @typedef {import('types').TableColumn} TableColumn + * @typedef {import('types').SalesforceUser} SalesforceUser + */ + +import { ONBOARD_WIZARD_STEPS } from "../../utils/c"; +import Logo from "src/components/Logo/Logo"; +import RoleStep from "./RoleStep"; + +const REQUIRED_PROSPECTING_CATEGORIES = [ + "Inbound Call", + "Outbound Call", + "Inbound Email", + "Outbound Email", +]; + +const Onboard = () => { + const navigate = useNavigate(); + const [step, setStep] = useState(1); // Start from step 1 + /** @type {[FilterContainer[], Function]} */ + const [filters, setFilters] = useState( + REQUIRED_PROSPECTING_CATEGORIES.map((category) => ({ + name: category, + filters: [], + filterLogic: "", + direction: category.toLowerCase().includes("inbound") + ? "Inbound" + : "Outbound", + })) + ); + /** + * @typedef {{ [key: string]: any }} GatheringResponses + */ + + /** @type {[GatheringResponses, React.Dispatch>]} */ + const [gatheringResponses, setGatheringResponses] = useState({}); + const [isLargeDialog, setIsLargeDialog] = useState(false); + const [isTransitioning, setIsTransitioning] = useState(false); + /** @type {[TableData, Function]} */ + const [categoryFormTableData, setCategoryFormTableData] = useState({ + availableColumns: [], + columns: [], + data: [], + selectedIds: new Set(), + }); + /** @type {[SObject[], Function]} */ + const [tasks, setTasks] = useState([]); + const taskSObjectFields = useRef([]); + const taskFilterFields = useRef([]); + + useEffect(() => { + const setInitialCategoryFormTableData = async () => { + taskSObjectFields.current = + taskSObjectFields.current.length > 0 + ? taskSObjectFields.current + : (await fetchTaskFields()).data.map( + /** @param {SObjectField} field */ + (field) => ({ + id: field.name, + label: field.label, + dataType: field.type, + }) + ); + setCategoryFormTableData({ + availableColumns: taskSObjectFields.current, + columns: + categoryFormTableData.columns.length > 0 + ? categoryFormTableData.columns + : [ + { + id: "select", + label: "Select", + dataType: "select", + }, + { + id: "Subject", + label: "Subject", + dataType: "string", + }, + { + id: "Status", + label: "Status", + dataType: "string", + }, + { + id: "TaskSubtype", + label: "TaskSubtype", + dataType: "string", + }, + ], + data: tasks, + selectedIds: new Set(), + }); + }; + setInitialCategoryFormTableData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tasks]); + + useEffect(() => { + const setTaskFilterFields = async () => { + taskFilterFields.current = + taskFilterFields.current.length > 0 + ? taskFilterFields.current + : (await fetchTaskFilterFields()).data; + }; + setTaskFilterFields(); + }, []); + + useEffect(() => { + const setSalesforceTasks = async () => { + try { + if (tasks.length > 0) return; + + const settings = getSettingsFromResponses(); + + /** @type {(string)[]} */ + const salesforceUserIds = [ + ...(settings.teamMemberIds || []), + ]; + + if (settings.salesforceUserId) { + salesforceUserIds.push(settings.salesforceUserId) + } + + if (salesforceUserIds.length === 0 || !salesforceUserIds[0]) return; + const response = await fetchSalesforceTasksByUserIds(salesforceUserIds); + if (!response.success) { + console.error(`Error fetching Salesforce tasks ${response.message}`); + return; + } + setTasks( + response.data.map( + /** @param {SObject} task */ + (task) => ({ ...task, id: task.Id }) + ) + ); + } catch (error) { + console.error("Error fetching Salesforce tasks", error); + } + }; + setSalesforceTasks(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [gatheringResponses]); + + useEffect(() => { + const setLoggedInSalesforceUser = async () => { + const sfResponse = await fetchLoggedInSalesforceUser(); + if (!sfResponse.success) { + console.error(`Error fetching Salesforce User: ${sfResponse.message}`); + return; + } + /** @type {SalesforceUser} */ + const salesforceUser = sfResponse.data[0]; + + // Update gatheringResponses with Salesforce user ID + setGatheringResponses((prev) => ({ + ...prev, + salesforceUserId: { value: salesforceUser.id }, + })); + }; + setLoggedInSalesforceUser(); + }, []); + + useEffect(() => { + if (isTransitioning) { + const timer = setTimeout(() => setIsTransitioning(false), 300); // Match this with transition duration + return () => clearTimeout(timer); + } + }, [isTransitioning]); + + /** + * @param {number} clickedStep + */ + const handleStepClick = (clickedStep) => { + if (clickedStep < step) { + setStep(clickedStep); + } + }; + + const saveSettings = async () => { + try { + /** @type {Settings} */ + let settings = getSettingsFromResponses(); + + settings = Object.keys(settings).reduce((acc, key) => { + acc[key] = + settings[key] === "Yes" + ? true + : settings[key] === "No" + ? false + : settings[key]; + return acc; + }, /** @type {Settings} */({})); + + const result = await saveSettingsToSupabase(settings); + + if (!result.success) { + throw new Error(result.message); + } + + console.log("Settings saved successfully"); + navigate("/app/settings"); + } catch (error) { + console.error("Error saving settings:", error); + } + }; + + /** + * Formats the settings data from the form responses. + * @returns {Settings} The formatted settings data matching the Supabase Settings table structure. + */ + const getSettingsFromResponses = () => { + const now = new Date(); + const threeMonthsAgo = new Date(now.setMonth(now.getMonth() - 3)); + return { + inactivityThreshold: parseInt( + gatheringResponses["inactivityThreshold"]?.value, + 10 + ), // Tracking Period + criteria: filters, + meetingObject: gatheringResponses["meetingObject"]?.value + .toLowerCase() + .includes("task") + ? "Task" + : "Event", + meetingsCriteria: gatheringResponses["meetingsCriteria"]?.value, + activitiesPerContact: parseInt( + gatheringResponses["activitiesPerContact"]?.value, + 10 + ), + contactsPerAccount: parseInt( + gatheringResponses["contactsPerAccount"]?.value, + 10 + ), + trackingPeriod: parseInt(gatheringResponses["trackingPeriod"]?.value, 10), + activateByMeeting: gatheringResponses["activateByMeeting"]?.value, + activateByOpportunity: gatheringResponses["activateByOpportunity"]?.value, + teamMemberIds: gatheringResponses["teamMemberIds"]?.value?.map( + (salesforceUser) => salesforceUser.id + ), + salesforceUserId: gatheringResponses["salesforceUserId"]?.value, + latestDateQueried: threeMonthsAgo.toISOString(), + }; + }; + + const handleNext = () => { + setStep(step + 1); + }; + + /** + * Corresponds to the onboarding wizard step question, if the question is composed of an array of questions, + * `responses` will be an array of responses, else it will be a single response + * @param {[{label: string, value: string}]} response + * @returns {void} + */ + const handleInfoGatheringComplete = (response) => { + setGatheringResponses( + /** + * @param {{ [key: string]: any }} prev + */ + (prev) => { + const newResponses = { ...prev }; + // Handle the case where we have multiple responses + response.forEach((res) => { + newResponses[res.label] = { value: res.value }; + }); + return newResponses; + } + ); + handleNext(); + }; + + /** + * @param {FilterContainer} filterContainer + */ + const handleProspectingFilterChanged = (updatedFilter) => { + setFilters((prev) => + prev.map((filter) => + filter.name === updatedFilter.name ? updatedFilter : filter + ) + ); + + setGatheringResponses((prev) => { + const newResponses = { ...prev }; + if (!newResponses.criteria) { + newResponses.criteria = { value: [] }; + } + const criteriaIndex = newResponses.criteria.value.findIndex( + (criteria) => criteria.name === updatedFilter.name + ); + if (criteriaIndex !== -1) { + newResponses.criteria.value[criteriaIndex] = updatedFilter; + } else { + newResponses.criteria.value.push(updatedFilter); + } + return newResponses; + }); + }; + + const handleTaskSelection = async (selectedTaskIds) => { + try { + const selectedTasks = tasks.filter((task) => + selectedTaskIds.includes(task.id) + ); + const response = await generateCriteria( + selectedTasks, + categoryFormTableData.columns + ); + return response.data[0]; + } catch (error) { + console.error("Error generating criteria:", error); + } + }; + + const renderStep = () => { + if (step <= ONBOARD_WIZARD_STEPS.length) { + return ( + + ); + } else if (step === ONBOARD_WIZARD_STEPS.length + 1) { + return ( + <> + + Define criteria by which we will recognize an Inbound Call, Outbound + Call, Inbound Email and Outbound Email. + + + + ); + } else { + return
Invalid step
; + } + }; + + const getProgressSteps = () => { + return [ + ...ONBOARD_WIZARD_STEPS, + { title: "Prospecting Categories" }, + { title: "Review" }, + ]; + }; + + const isLargeDialogStep = () => { + return isLargeDialog || step > ONBOARD_WIZARD_STEPS.length; + }; + + const dialogStyle = { + transition: "all 0.3s ease-in-out", + boxShadow: "0 4px 20px rgba(0,0,0,0.1)", + border: "1px solid #e0e0e0", + ...(isLargeDialogStep() + ? { + maxWidth: "60vw", + width: "60vw", + maxHeight: "90vh", + height: "90vh", + } + : { + maxWidth: "600px", // Adjust as needed for small dialog + width: "100%", + maxHeight: "80vh", + height: "auto", + }), + }; + + /** + * @param {boolean} isDisplayed + */ + const handleTableDisplay = (isDisplayed) => { + setIsTransitioning(true); + setIsLargeDialog(isDisplayed); + }; + + return ( + + {/* Sidebar */} + +
+ +
+ + + +
+ {step === 1 && ()} +
+ ); +}; + +export default Onboard; diff --git a/client/src/pages/Onboard/RoleStep.js b/client/src/pages/Onboard/RoleStep.js new file mode 100644 index 0000000..55d125f --- /dev/null +++ b/client/src/pages/Onboard/RoleStep.js @@ -0,0 +1,201 @@ +import React, { useState } from 'react'; +import { Box, Button, FormControl, MenuItem, Select } from '@mui/material'; + +/** + * @typedef {import('types').Settings} Settings + */ + +/** + * @param {object} props + * @param {() => void} props.handleNext + */ +const RoleStep = ({ handleNext }) => { + /** @type {[Settings, React.Dispatch>]} */ + const [inputValues, setInputValues] = useState({ + userRole: "placeholder" + }); + + const handleInputChange = (event, setting) => { + setInputValues((prev) => ({ + ...prev, + [setting]: event.target.value, + })); + }; + + return ( + +
+

+ YOUR DATA VISUALIZED +

+

+ Welcome to your +

+

+ Onboarding +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Quis ipsum suspendisse ultrices gravida. +

+ +

+ Tell Us About Yourself +

+ + {/* User Role */} + + + + + +
+
+ +
+ ); +}; + +export default RoleStep; \ No newline at end of file diff --git a/client/src/utils/c.js b/client/src/utils/c.js index a9d6a85..bb9cdef 100644 --- a/client/src/utils/c.js +++ b/client/src/utils/c.js @@ -51,7 +51,7 @@ export const PROSPECTING_ACTIVITY_FILTER_TITLE_PLACEHOLDERS = [ export const ONBOARD_WIZARD_STEPS = [ { description: "", - title: "Tell us about yourself", + title: "Your Role", inputs: [ { setting: "userRole", @@ -119,7 +119,7 @@ export const ONBOARD_WIZARD_STEPS = [

An "approach" is defined as when a rep attempts to engage with _ people at a target/prospect company within a _ day period. `, - title: "Welcome to InsideOutbound", + title: "Welcome", inputs: [ { setting: "contactsPerAccount", @@ -274,7 +274,7 @@ export const ONBOARD_WIZARD_STEPS = [ ? await fetchSalesforceTasksByUserIds(salesforceUserIds) : await fetchSalesforceEventsByUserIds(salesforceUserIds); tableData.data = tableDataResponse.data.map( - /** @param {SObject} item */ (item) => ({ + /** @param {SObject} item */(item) => ({ ...item, id: item.Id, }) @@ -284,7 +284,7 @@ export const ONBOARD_WIZARD_STEPS = [ ? await fetchTaskFilterFields() : await fetchEventFilterFields(); tableData.availableColumns = fieldsResponse.data.map( - /**@param {SObjectField} field*/ (field) => ({ + /**@param {SObjectField} field*/(field) => ({ id: field.name, label: field.label, dataType: field.type, From a3cd567452a6aac3c6869f6595cbc2596b9e2986 Mon Sep 17 00:00:00 2001 From: ter1203 Date: Thu, 29 Aug 2024 15:04:17 -0300 Subject: [PATCH 2/5] fix: comment out for future use --- client/src/pages/Onboard/Onboard.js | 207 +++++++--------------------- 1 file changed, 49 insertions(+), 158 deletions(-) diff --git a/client/src/pages/Onboard/Onboard.js b/client/src/pages/Onboard/Onboard.js index 0c4c5a8..e8162b7 100644 --- a/client/src/pages/Onboard/Onboard.js +++ b/client/src/pages/Onboard/Onboard.js @@ -1,16 +1,14 @@ import React, { useState, useEffect, useRef } from "react"; -import { Box, Paper, Typography, Divider } from "@mui/material"; -import { useNavigate } from "react-router-dom"; -import ProspectingCriteriaSelector from "../../components/ProspectingCriteriaSelector/ProspectingCriteriaSelector"; -import InfoGatheringStep from "../../components/InfoGatheringStep/InfoGatheringStep"; +import { Box, Paper, Divider } from "@mui/material"; +// import { useNavigate } from "react-router-dom"; import ProgressTracker from "../../components/ProgressTracker/ProgressTracker"; import { fetchLoggedInSalesforceUser, fetchSalesforceTasksByUserIds, fetchTaskFields, fetchTaskFilterFields, - generateCriteria, - saveSettings as saveSettingsToSupabase, + // generateCriteria, + // saveSettings as saveSettingsToSupabase, } from "../../components/Api/Api"; /** @@ -36,10 +34,10 @@ const REQUIRED_PROSPECTING_CATEGORIES = [ ]; const Onboard = () => { - const navigate = useNavigate(); + // const navigate = useNavigate(); const [step, setStep] = useState(1); // Start from step 1 /** @type {[FilterContainer[], Function]} */ - const [filters, setFilters] = useState( + const [filters] = useState( REQUIRED_PROSPECTING_CATEGORIES.map((category) => ({ name: category, filters: [], @@ -55,7 +53,6 @@ const Onboard = () => { /** @type {[GatheringResponses, React.Dispatch>]} */ const [gatheringResponses, setGatheringResponses] = useState({}); - const [isLargeDialog, setIsLargeDialog] = useState(false); const [isTransitioning, setIsTransitioning] = useState(false); /** @type {[TableData, Function]} */ const [categoryFormTableData, setCategoryFormTableData] = useState({ @@ -198,33 +195,33 @@ const Onboard = () => { } }; - const saveSettings = async () => { - try { - /** @type {Settings} */ - let settings = getSettingsFromResponses(); - - settings = Object.keys(settings).reduce((acc, key) => { - acc[key] = - settings[key] === "Yes" - ? true - : settings[key] === "No" - ? false - : settings[key]; - return acc; - }, /** @type {Settings} */({})); - - const result = await saveSettingsToSupabase(settings); - - if (!result.success) { - throw new Error(result.message); - } - - console.log("Settings saved successfully"); - navigate("/app/settings"); - } catch (error) { - console.error("Error saving settings:", error); - } - }; + // const saveSettings = async () => { + // try { + // /** @type {Settings} */ + // let settings = getSettingsFromResponses(); + + // settings = Object.keys(settings).reduce((acc, key) => { + // acc[key] = + // settings[key] === "Yes" + // ? true + // : settings[key] === "No" + // ? false + // : settings[key]; + // return acc; + // }, /** @type {Settings} */({})); + + // const result = await saveSettingsToSupabase(settings); + + // if (!result.success) { + // throw new Error(result.message); + // } + + // console.log("Settings saved successfully"); + // navigate("/app/settings"); + // } catch (error) { + // console.error("Error saving settings:", error); + // } + // }; /** * Formats the settings data from the form responses. @@ -274,98 +271,22 @@ const Onboard = () => { * @param {[{label: string, value: string}]} response * @returns {void} */ - const handleInfoGatheringComplete = (response) => { - setGatheringResponses( - /** - * @param {{ [key: string]: any }} prev - */ - (prev) => { - const newResponses = { ...prev }; - // Handle the case where we have multiple responses - response.forEach((res) => { - newResponses[res.label] = { value: res.value }; - }); - return newResponses; - } - ); - handleNext(); - }; - - /** - * @param {FilterContainer} filterContainer - */ - const handleProspectingFilterChanged = (updatedFilter) => { - setFilters((prev) => - prev.map((filter) => - filter.name === updatedFilter.name ? updatedFilter : filter - ) - ); - - setGatheringResponses((prev) => { - const newResponses = { ...prev }; - if (!newResponses.criteria) { - newResponses.criteria = { value: [] }; - } - const criteriaIndex = newResponses.criteria.value.findIndex( - (criteria) => criteria.name === updatedFilter.name - ); - if (criteriaIndex !== -1) { - newResponses.criteria.value[criteriaIndex] = updatedFilter; - } else { - newResponses.criteria.value.push(updatedFilter); - } - return newResponses; - }); - }; - - const handleTaskSelection = async (selectedTaskIds) => { - try { - const selectedTasks = tasks.filter((task) => - selectedTaskIds.includes(task.id) - ); - const response = await generateCriteria( - selectedTasks, - categoryFormTableData.columns - ); - return response.data[0]; - } catch (error) { - console.error("Error generating criteria:", error); - } - }; - - const renderStep = () => { - if (step <= ONBOARD_WIZARD_STEPS.length) { - return ( - - ); - } else if (step === ONBOARD_WIZARD_STEPS.length + 1) { - return ( - <> - - Define criteria by which we will recognize an Inbound Call, Outbound - Call, Inbound Email and Outbound Email. - - - - ); - } else { - return
Invalid step
; - } - }; + // const handleInfoGatheringComplete = (response) => { + // setGatheringResponses( + // /** + // * @param {{ [key: string]: any }} prev + // */ + // (prev) => { + // const newResponses = { ...prev }; + // // Handle the case where we have multiple responses + // response.forEach((res) => { + // newResponses[res.label] = { value: res.value }; + // }); + // return newResponses; + // } + // ); + // handleNext(); + // }; const getProgressSteps = () => { return [ @@ -375,36 +296,6 @@ const Onboard = () => { ]; }; - const isLargeDialogStep = () => { - return isLargeDialog || step > ONBOARD_WIZARD_STEPS.length; - }; - - const dialogStyle = { - transition: "all 0.3s ease-in-out", - boxShadow: "0 4px 20px rgba(0,0,0,0.1)", - border: "1px solid #e0e0e0", - ...(isLargeDialogStep() - ? { - maxWidth: "60vw", - width: "60vw", - maxHeight: "90vh", - height: "90vh", - } - : { - maxWidth: "600px", // Adjust as needed for small dialog - width: "100%", - maxHeight: "80vh", - height: "auto", - }), - }; - - /** - * @param {boolean} isDisplayed - */ - const handleTableDisplay = (isDisplayed) => { - setIsTransitioning(true); - setIsLargeDialog(isDisplayed); - }; return ( From cd53cbb23d5fd4dd41a0bcf55cf6adec37f7cc7c Mon Sep 17 00:00:00 2001 From: ter1203 Date: Thu, 29 Aug 2024 15:14:57 -0300 Subject: [PATCH 3/5] revert onBoard.js for reference new implementation --- client/src/pages/Onboard.js | 114 ++++++++---------------------------- 1 file changed, 23 insertions(+), 91 deletions(-) diff --git a/client/src/pages/Onboard.js b/client/src/pages/Onboard.js index af3520c..6e111c7 100644 --- a/client/src/pages/Onboard.js +++ b/client/src/pages/Onboard.js @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from "react"; -import { Dialog, DialogContent, Box, Paper, Typography, Divider } from "@mui/material"; +import { Dialog, DialogContent, Box, Paper, Typography } from "@mui/material"; import { useNavigate } from "react-router-dom"; import ProspectingCriteriaSelector from "../components/ProspectingCriteriaSelector/ProspectingCriteriaSelector"; import InfoGatheringStep from "../components/InfoGatheringStep/InfoGatheringStep"; @@ -25,7 +25,6 @@ import { */ import { ONBOARD_WIZARD_STEPS } from "../utils/c"; -import Logo from "src/components/Logo/Logo"; const REQUIRED_PROSPECTING_CATEGORIES = [ "Inbound Call", @@ -390,29 +389,22 @@ const Onboard = () => { }; return ( - - {/* Sidebar */} + -
- -
- - { orientation="vertical" />
- -
+ { + console.log("closing"); + }} + PaperProps={{ + style: dialogStyle, }} + fullWidth + maxWidth={false} > -

- YOUR DATA VISUALIZED -

-

- Welcome to your -

-

- Onboarding -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Quis ipsum suspendisse ultrices gravida. -

-
{renderStep()} -
-
- + +
); }; -export default Onboard; +export default Onboard; \ No newline at end of file From c891a3e7941a14ac404a683ceb841df751ae5a9c Mon Sep 17 00:00:00 2001 From: ter1203 Date: Mon, 2 Sep 2024 14:57:05 -0300 Subject: [PATCH 4/5] fix: display teams table when role is 'manage a team' --- client/src/App.js | 3 +- client/src/pages/Onboard/Onboard.js | 138 +++------------ client/src/pages/Onboard/OnboardConstant.js | 15 ++ client/src/pages/Onboard/OnboardProvider.jsx | 176 +++++++++++++++++++ client/src/pages/Onboard/RoleStep.js | 136 +++++++++++++- client/types/Onboard.d.ts | 59 +++++++ 6 files changed, 401 insertions(+), 126 deletions(-) create mode 100644 client/src/pages/Onboard/OnboardConstant.js create mode 100644 client/src/pages/Onboard/OnboardProvider.jsx create mode 100644 client/types/Onboard.d.ts diff --git a/client/src/App.js b/client/src/App.js index a7d0530..10890d8 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -12,6 +12,7 @@ import MainContent from "./components/MainContent"; import Login from "./pages/Login"; import Onboard from "./pages/Onboard/Onboard"; import "./App.css"; +import { OnboardProvider } from "./pages/Onboard/OnboardProvider"; const AppRoutes = () => { const navigate = useNavigate(); @@ -35,7 +36,7 @@ const AppRoutes = () => { return ( } /> - } /> + } /> { // const navigate = useNavigate(); - const [step, setStep] = useState(1); // Start from step 1 - /** @type {[FilterContainer[], Function]} */ - const [filters] = useState( - REQUIRED_PROSPECTING_CATEGORIES.map((category) => ({ - name: category, - filters: [], - filterLogic: "", - direction: category.toLowerCase().includes("inbound") - ? "Inbound" - : "Outbound", - })) - ); - /** - * @typedef {{ [key: string]: any }} GatheringResponses - */ + const { + step, + filters, + categoryFormTableData, + gatheringResponses, + isLargeDialog, + isTransitioning, + tasks, + setCategoryFormTableData, + setFilters, + setGatheringResponses, + setIsLargeDialog, + setIsTransitioning, + setStep, + setTasks, + handleStepClick + } = useOnboard(); - /** @type {[GatheringResponses, React.Dispatch>]} */ - const [gatheringResponses, setGatheringResponses] = useState({}); - const [isTransitioning, setIsTransitioning] = useState(false); - /** @type {[TableData, Function]} */ - const [categoryFormTableData, setCategoryFormTableData] = useState({ - availableColumns: [], - columns: [], - data: [], - selectedIds: new Set(), - }); - /** @type {[SObject[], Function]} */ - const [tasks, setTasks] = useState([]); const taskSObjectFields = useRef([]); const taskFilterFields = useRef([]); - useEffect(() => { - const setInitialCategoryFormTableData = async () => { - taskSObjectFields.current = - taskSObjectFields.current.length > 0 - ? taskSObjectFields.current - : (await fetchTaskFields()).data.map( - /** @param {SObjectField} field */ - (field) => ({ - id: field.name, - label: field.label, - dataType: field.type, - }) - ); - setCategoryFormTableData({ - availableColumns: taskSObjectFields.current, - columns: - categoryFormTableData.columns.length > 0 - ? categoryFormTableData.columns - : [ - { - id: "select", - label: "Select", - dataType: "select", - }, - { - id: "Subject", - label: "Subject", - dataType: "string", - }, - { - id: "Status", - label: "Status", - dataType: "string", - }, - { - id: "TaskSubtype", - label: "TaskSubtype", - dataType: "string", - }, - ], - data: tasks, - selectedIds: new Set(), - }); - }; - setInitialCategoryFormTableData(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tasks]); - - useEffect(() => { - const setTaskFilterFields = async () => { - taskFilterFields.current = - taskFilterFields.current.length > 0 - ? taskFilterFields.current - : (await fetchTaskFilterFields()).data; - }; - setTaskFilterFields(); - }, []); - useEffect(() => { const setSalesforceTasks = async () => { try { @@ -186,15 +113,6 @@ const Onboard = () => { } }, [isTransitioning]); - /** - * @param {number} clickedStep - */ - const handleStepClick = (clickedStep) => { - if (clickedStep < step) { - setStep(clickedStep); - } - }; - // const saveSettings = async () => { // try { // /** @type {Settings} */ @@ -261,10 +179,6 @@ const Onboard = () => { }; }; - const handleNext = () => { - setStep(step + 1); - }; - /** * Corresponds to the onboarding wizard step question, if the question is composed of an array of questions, * `responses` will be an array of responses, else it will be a single response @@ -288,14 +202,6 @@ const Onboard = () => { // handleNext(); // }; - const getProgressSteps = () => { - return [ - ...ONBOARD_WIZARD_STEPS, - { title: "Prospecting Categories" }, - { title: "Review" }, - ]; - }; - return ( @@ -322,13 +228,13 @@ const Onboard = () => { - {step === 1 && ()} + {step === 1 && ()} ); }; diff --git a/client/src/pages/Onboard/OnboardConstant.js b/client/src/pages/Onboard/OnboardConstant.js new file mode 100644 index 0000000..81865c7 --- /dev/null +++ b/client/src/pages/Onboard/OnboardConstant.js @@ -0,0 +1,15 @@ +import { ONBOARD_WIZARD_STEPS } from "src/utils/c"; + +/** @type {import("types/Onboard").RequiredProspectingCategory[]} */ +export const REQUIRED_PROSPECTING_CATEGORIES = [ + "Inbound Call", + "Outbound Call", + "Inbound Email", + "Outbound Email", +]; + +export const PROGRESS_STEP = [ + ...ONBOARD_WIZARD_STEPS, + { title: "Prospecting Categories" }, + { title: "Review" }, +]; diff --git a/client/src/pages/Onboard/OnboardProvider.jsx b/client/src/pages/Onboard/OnboardProvider.jsx new file mode 100644 index 0000000..26aba82 --- /dev/null +++ b/client/src/pages/Onboard/OnboardProvider.jsx @@ -0,0 +1,176 @@ +import { + createContext, + useContext, + useEffect, + useState, +} from "react"; +import { REQUIRED_PROSPECTING_CATEGORIES } from "./OnboardConstant"; + +/** + * @typedef {import("types/Filter").Filter} Filter + * @typedef {import("types/Onboard").GatheringResponses} GatheringResponses + * @typedef {import("types/Onboard").CategoryFormTableData} CategoryFormTableData + * @typedef {import("types/Onboard").OnboardContextValue} OnboardContextValue + */ + +/** @type {import("types/Onboard").OnboardContextInit} */ +const initOnboard = { + step: 1, + filters: REQUIRED_PROSPECTING_CATEGORIES.map((category) => { + /** @type {"Inbound" | "Outbound"} */ + const direction = category.toLowerCase().includes("inbound") + ? "Inbound" + : "Outbound"; + + /** @type {Filter[]} */ + const filters = [] + return { + name: category, + filters: filters, + filterLogic: "", + direction: direction, + } + }), + gatheringResponses: {}, + isLargeDialog: false, + isTransitioning: false, + categoryFormTableData: { + availableColumns: [], + columns: [], + data: [], + selectedIds: new Set(), + }, + tasks: [], + inputValues: { + userRole: "placeholder" + } +}; + +/** @type {import("react").Context} */ +const OnboardContext = createContext(/** @type {OnboardContextValue} */(initOnboard)); + +/** + * @param {{children: React.ReactNode}} props + */ +export const OnboardProvider = ({ + children, +}) => { + const [step, setStep] = useState(initOnboard.step); + const [filters, setFilters] = useState(initOnboard.filters); + + const [gatheringResponses, setGatheringResponses] = useState(initOnboard.gatheringResponses); + const [isLargeDialog, setIsLargeDialog] = useState(initOnboard.isLargeDialog); + const [isTransitioning, setIsTransitioning] = useState(initOnboard.isTransitioning); + + const [categoryFormTableData, setCategoryFormTableData] = useState(initOnboard.categoryFormTableData); + + const [tasks, setTasks] = useState(initOnboard.tasks); + + const [inputValues, setInputValues] = useState(initOnboard.inputValues); + + // const taskSObjectFields = useRef([]); + // const taskFilterFields = useRef([]); + + /** + * @param {number} clickedStep + */ + const handleStepClick = (clickedStep) => { + if (clickedStep < step) { + setStep(clickedStep); + } + }; + + useEffect(() => { + const setInitialCategoryFormTableData = async () => { + // taskSObjectFields.current = + // taskSObjectFields.current.length > 0 + // ? taskSObjectFields.current + // : (await fetchTaskFields()).data.map( + // /** @param {SObjectField} field */ + // (field) => ({ + // id: field.name, + // label: field.label, + // dataType: field.type, + // }) + // ); + // setCategoryFormTableData({ + // availableColumns: taskSObjectFields.current, + // columns: + // categoryFormTableData.columns.length > 0 + // ? categoryFormTableData.columns + // : [ + // { + // id: "select", + // label: "Select", + // dataType: "select", + // }, + // { + // id: "Subject", + // label: "Subject", + // dataType: "string", + // }, + // { + // id: "Status", + // label: "Status", + // dataType: "string", + // }, + // { + // id: "TaskSubtype", + // label: "TaskSubtype", + // dataType: "string", + // }, + // ], + // data: tasks, + // selectedIds: new Set(), + // }); + }; + setInitialCategoryFormTableData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tasks]); + + useEffect(() => { + const setTaskFilterFields = async () => { + // taskFilterFields.current = + // taskFilterFields.current.length > 0 + // ? taskFilterFields.current + // : (await fetchTaskFilterFields()).data; + }; + setTaskFilterFields(); + }, []); + + return ( + + {children} + + ); +}; + +export const useOnboard = () => { + const context = useContext(OnboardContext); + if (!context) { + throw new Error( + "useOnboard must be used within a OnboardProvider" + ); + } + return context; +}; diff --git a/client/src/pages/Onboard/RoleStep.js b/client/src/pages/Onboard/RoleStep.js index 55d125f..68b265e 100644 --- a/client/src/pages/Onboard/RoleStep.js +++ b/client/src/pages/Onboard/RoleStep.js @@ -1,20 +1,76 @@ -import React, { useState } from 'react'; -import { Box, Button, FormControl, MenuItem, Select } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { Box, Button, CircularProgress, FormControl, MenuItem, Select } from '@mui/material'; +import { useOnboard } from './OnboardProvider'; +import CustomTable from 'src/components/CustomTable/CustomTable'; +import { fetchSalesforceUsers } from 'src/components/Api/Api'; /** * @typedef {import('types').Settings} Settings + * @typedef {import('types').TableColumn} TableColumn + * @typedef {import('types').TableData} TableData */ +/** @type {TableColumn[]} */ +const COLUMNS = [ + { + id: "select", + label: "Select", + dataType: "select", + }, + { + id: "photoUrl", + label: "", + dataType: "image", + }, + { + id: "firstName", + label: "First Name", + dataType: "string", + }, + { + id: "lastName", + label: "Last Name", + dataType: "string", + }, + { + id: "email", + label: "Email", + dataType: "string", + }, + { + id: "role", + label: "Role", + dataType: "string", + }, + { + id: "username", + label: "Username", + dataType: "string", + }, +] + +/** @return {TableData|null} */ +const initTableData = () => { + return null +} + /** * @param {object} props * @param {() => void} props.handleNext */ -const RoleStep = ({ handleNext }) => { - /** @type {[Settings, React.Dispatch>]} */ - const [inputValues, setInputValues] = useState({ - userRole: "placeholder" - }); +const RoleStep = () => { + const { setStep, step, setInputValues, inputValues } = useOnboard(); + + /** @type {[TableData|null, React.Dispatch>]} */ + const [tableData, setTableData] = useState(initTableData()); + + const [isLoading, setIsLoading] = useState(false); + + /** + * @param {import('@mui/material').SelectChangeEvent} event + * @param {string} setting + */ const handleInputChange = (event, setting) => { setInputValues((prev) => ({ ...prev, @@ -22,6 +78,49 @@ const RoleStep = ({ handleNext }) => { })); }; + + /** @param {Set} newSelectedIds */ + const handleTableSelectionChange = (newSelectedIds) => { + setTableData((prev) => + prev ? { ...prev, selectedIds: newSelectedIds } : null + ); + }; + + /** @param {TableColumn[]} newColumns */ + const handleColumnsChange = (newColumns) => { + setTableData((prev) => (prev ? { ...prev, columns: newColumns } : null)); + }; + + const fetchTableData = async () => { + try { + setIsLoading(true) + const data = await fetchSalesforceUsers(); + + /** @type {TableData} */ + const _tableData = { + availableColumns: [], + columns: COLUMNS, + data: data.data, + selectedIds: new Set(), + }; + + setTableData(_tableData); + + } catch (error) { + console.error("Error fetching data:", error); + } finally { + setIsLoading(false); + } + } + + useEffect(() => { + if (inputValues.userRole === "I manage a team") { + fetchTableData() + } else { + setTableData(null) + } + }, [inputValues]) + return ( { Tell Us About Yourself - {/* User Role */} + {tableData && ( + { return; }} + onSelectionChange={handleTableSelectionChange} + onColumnsChange={handleColumnsChange} + paginate={true} + />)} + {isLoading && ( + + + + )} { }} >