diff --git a/apps/app/components/analytics/custom-analytics/create-update-analytics-modal.tsx b/apps/app/components/analytics/custom-analytics/create-update-analytics-modal.tsx index 625a2a4c7..afaaf1b98 100644 --- a/apps/app/components/analytics/custom-analytics/create-update-analytics-modal.tsx +++ b/apps/app/components/analytics/custom-analytics/create-update-analytics-modal.tsx @@ -62,7 +62,7 @@ export const CreateUpdateAnalyticsModal: React.FC = ({ isOpen, handleClos x_axis: "priority", y_axis: "issue_count", ...params, - project: params?.project ? [params.project] : [], + project: params?.project ?? [], }, }; diff --git a/apps/app/components/analytics/custom-analytics/custom-analytics.tsx b/apps/app/components/analytics/custom-analytics/custom-analytics.tsx index d1ec7392c..f9be7d1dd 100644 --- a/apps/app/components/analytics/custom-analytics/custom-analytics.tsx +++ b/apps/app/components/analytics/custom-analytics/custom-analytics.tsx @@ -1,144 +1,130 @@ -import { useState } from "react"; - import { useRouter } from "next/router"; -import useSWR from "swr"; +import { mutate } from "swr"; // react-hook-form -import { useForm } from "react-hook-form"; -// services -import analyticsService from "services/analytics.service"; +import { Control, UseFormSetValue } from "react-hook-form"; +// hooks +import useProjects from "hooks/use-projects"; // components import { AnalyticsGraph, + AnalyticsSelectBar, AnalyticsSidebar, AnalyticsTable, - CreateUpdateAnalyticsModal, } from "components/analytics"; // ui import { Loader, PrimaryButton } from "components/ui"; // helpers import { convertResponseToBarGraphData } from "helpers/analytics.helper"; // types -import { IAnalyticsParams } from "types"; +import { IAnalyticsParams, IAnalyticsResponse } from "types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; -const defaultValues: IAnalyticsParams = { - x_axis: "priority", - y_axis: "issue_count", - segment: null, - project: null, -}; - type Props = { - isProjectLevel?: boolean; - fullScreen?: boolean; + analytics: IAnalyticsResponse | undefined; + analyticsError: any; + params: IAnalyticsParams; + control: Control; + setValue: UseFormSetValue; + fullScreen: boolean; }; -export const CustomAnalytics: React.FC = ({ isProjectLevel = false, fullScreen = true }) => { - const [saveAnalyticsModal, setSaveAnalyticsModal] = useState(false); - +export const CustomAnalytics: React.FC = ({ + analytics, + analyticsError, + params, + control, + setValue, + fullScreen, +}) => { const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { workspaceSlug, projectId } = router.query; - const { control, watch, setValue } = useForm({ defaultValues }); + const isProjectLevel = projectId ? true : false; - const params: IAnalyticsParams = { - x_axis: watch("x_axis"), - y_axis: watch("y_axis"), - segment: watch("segment"), - project: isProjectLevel ? projectId?.toString() : watch("project"), - cycle: isProjectLevel && cycleId ? cycleId.toString() : null, - module: isProjectLevel && moduleId ? moduleId.toString() : null, - }; - - const { - data: analytics, - error: analyticsError, - mutate: mutateAnalytics, - } = useSWR( - workspaceSlug ? ANALYTICS(workspaceSlug.toString(), params) : null, - workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null - ); - - const yAxisKey = params.y_axis === "issue_count" ? "count" : "effort"; + const yAxisKey = params.y_axis === "issue_count" ? "count" : "estimate"; const barGraphData = convertResponseToBarGraphData(analytics?.distribution, params); + const { projects } = useProjects(); + return ( - <> - setSaveAnalyticsModal(false)} - params={params} - /> -
-
- {!analyticsError ? ( - analytics ? ( - analytics.total > 0 ? ( - <> - - - - ) : ( -
-
-

- No matching issues found. Try changing the parameters. -

-
-
- ) +
+
+ + {!analyticsError ? ( + analytics ? ( + analytics.total > 0 ? ( +
+ + +
) : ( - - - - - - - - - - ) - ) : ( -
-
-

There was some error in fetching the data.

-
- mutateAnalytics()}>Refresh +
+
+

No matching issues found. Try changing the parameters.

+ ) + ) : ( + + + + + + + + + + ) + ) : ( +
+
+

There was some error in fetching the data.

+
+ { + if (!workspaceSlug) return; + + mutate(ANALYTICS(workspaceSlug.toString(), params)); + }} + > + Refresh + +
- )} -
-
- -
+
+ )}
- + +
); }; diff --git a/apps/app/components/analytics/custom-analytics/graph/custom-tooltip.tsx b/apps/app/components/analytics/custom-analytics/graph/custom-tooltip.tsx index 9d392c214..d288ae3a9 100644 --- a/apps/app/components/analytics/custom-analytics/graph/custom-tooltip.tsx +++ b/apps/app/components/analytics/custom-analytics/graph/custom-tooltip.tsx @@ -18,11 +18,11 @@ export const CustomTooltip: React.FC = ({ datum, params }) => { else tooltipValue = datum.id; } else { if (DATE_KEYS.includes(params.x_axis)) tooltipValue = datum.indexValue; - else tooltipValue = datum.id === "count" ? "Issue count" : "Effort"; + else tooltipValue = datum.id === "count" ? "Issue count" : "Estimate"; } return ( -
+
= ({ height={fullScreen ? "400px" : "300px"} margin={{ right: 20, bottom: longestXAxisLabel.length * 5 + 20 }} theme={{ + background: "rgb(var(--color-bg-base))", axis: {}, }} /> diff --git a/apps/app/components/analytics/custom-analytics/index.ts b/apps/app/components/analytics/custom-analytics/index.ts index 11670a494..ecdb29b74 100644 --- a/apps/app/components/analytics/custom-analytics/index.ts +++ b/apps/app/components/analytics/custom-analytics/index.ts @@ -1,5 +1,6 @@ export * from "./graph"; export * from "./create-update-analytics-modal"; export * from "./custom-analytics"; +export * from "./select-bar"; export * from "./sidebar"; export * from "./table"; diff --git a/apps/app/components/analytics/custom-analytics/select-bar.tsx b/apps/app/components/analytics/custom-analytics/select-bar.tsx new file mode 100644 index 000000000..5f667b035 --- /dev/null +++ b/apps/app/components/analytics/custom-analytics/select-bar.tsx @@ -0,0 +1,80 @@ +// react-hook-form +import { Control, Controller, UseFormSetValue } from "react-hook-form"; +// components +import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics"; +// types +import { IAnalyticsParams, IProject } from "types"; + +type Props = { + control: Control; + setValue: UseFormSetValue; + projects: IProject[]; + params: IAnalyticsParams; + fullScreen: boolean; + isProjectLevel: boolean; +}; + +export const AnalyticsSelectBar: React.FC = ({ + control, + setValue, + projects, + params, + fullScreen, + isProjectLevel, +}) => ( +
+ {!isProjectLevel && ( +
+
Project
+ ( + + )} + /> +
+ )} +
+
Measure (y-axis)
+ ( + + )} + /> +
+
+
Dimension (x-axis)
+ ( + { + if (params.segment === val) setValue("segment", null); + + onChange(val); + }} + /> + )} + /> +
+
+
Group
+ ( + + )} + /> +
+
+); diff --git a/apps/app/components/analytics/custom-analytics/sidebar.tsx b/apps/app/components/analytics/custom-analytics/sidebar.tsx index 9c0964021..e0b88b23b 100644 --- a/apps/app/components/analytics/custom-analytics/sidebar.tsx +++ b/apps/app/components/analytics/custom-analytics/sidebar.tsx @@ -1,51 +1,82 @@ import { useRouter } from "next/router"; -import { mutate } from "swr"; +import useSWR, { mutate } from "swr"; -// react-hook-form -import { Control, Controller, UseFormSetValue } from "react-hook-form"; // services import analyticsService from "services/analytics.service"; +import projectService from "services/project.service"; +import cyclesService from "services/cycles.service"; +import modulesService from "services/modules.service"; // hooks import useProjects from "hooks/use-projects"; import useToast from "hooks/use-toast"; // ui -import { CustomMenu, CustomSelect, PrimaryButton } from "components/ui"; +import { PrimaryButton, SecondaryButton } from "components/ui"; // icons -import { ArrowPathIcon, ArrowUpTrayIcon } from "@heroicons/react/24/outline"; +import { ArrowDownTrayIcon, ArrowPathIcon, UserGroupIcon } from "@heroicons/react/24/outline"; +import { ContrastIcon, LayerDiagonalIcon } from "components/icons"; +// helpers +import { renderShortDate } from "helpers/date-time.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData } from "types"; +import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IProject } from "types"; // fetch-keys -import { ANALYTICS } from "constants/fetch-keys"; +import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; // constants -import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; +import { NETWORK_CHOICES } from "constants/project"; type Props = { analytics: IAnalyticsResponse | undefined; params: IAnalyticsParams; - control: Control; - setValue: UseFormSetValue; - setSaveAnalyticsModal: React.Dispatch>; fullScreen: boolean; - isProjectLevel?: boolean; + isProjectLevel: boolean; }; export const AnalyticsSidebar: React.FC = ({ analytics, params, - control, - setValue, - setSaveAnalyticsModal, fullScreen, isProjectLevel = false, }) => { const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { projects } = useProjects(); const { setToastAlert } = useToast(); + const { data: projectDetails } = useSWR( + workspaceSlug && projectId && !(cycleId || moduleId) + ? PROJECT_DETAILS(projectId.toString()) + : null, + workspaceSlug && projectId && !(cycleId || moduleId) + ? () => projectService.getProject(workspaceSlug.toString(), projectId.toString()) + : null + ); + + const { data: cycleDetails } = useSWR( + workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null, + workspaceSlug && projectId && cycleId + ? () => + cyclesService.getCycleDetails( + workspaceSlug.toString(), + projectId.toString(), + cycleId.toString() + ) + : null + ); + + const { data: moduleDetails } = useSWR( + workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null, + workspaceSlug && projectId && moduleId + ? () => + modulesService.getModuleDetails( + workspaceSlug.toString(), + projectId.toString(), + moduleId.toString() + ) + : null + ); + const exportAnalytics = () => { if (!workspaceSlug) return; @@ -55,7 +86,7 @@ export const AnalyticsSidebar: React.FC = ({ }; if (params.segment) data.segment = params.segment; - if (params.project) data.project = [params.project]; + if (params.project) data.project = params.project; analyticsService .exportAnalytics(workspaceSlug.toString(), data) @@ -77,155 +108,180 @@ export const AnalyticsSidebar: React.FC = ({ return (
-
-
-
- {analytics?.total ?? 0}{" "} - issues -
- - { - if (!workspaceSlug) return; - - mutate(ANALYTICS(workspaceSlug.toString(), params)); - }} - > -
- - Refresh -
-
- -
- - Export analytics as CSV -
-
-
+
+
+ + {analytics ? analytics.total : "..."} Issues
-
- {isProjectLevel === false && ( -
-
Project
- ( - p.id === value)?.name ?? "All projects"} - onChange={onChange} - width="w-full" - maxHeight="lg" - > - All projects - {projects.map((project) => ( - - {project.name} - - ))} - - )} - /> -
- )} -
-
Measure (y-axis)
- ( - - {ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "None"} - - } - onChange={onChange} - width="w-full" - > - {ANALYTICS_Y_AXIS_VALUES.map((item) => ( - - {item.label} - - ))} - - )} - /> -
-
-
Dimension (x-axis)
- ( - {ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label} - } - onChange={(val: string) => { - if (params.segment === val) setValue("segment", null); - - onChange(val); - }} - width="w-full" - maxHeight="lg" - > - {ANALYTICS_X_AXIS_VALUES.map((item) => ( - - {item.label} - - ))} - - )} - /> -
-
-
Segment
- ( - - {ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label ?? ( - No value - )} - - } - onChange={onChange} - width="w-full" - maxHeight="lg" - > - No value - {ANALYTICS_X_AXIS_VALUES.map((item) => { - if (params.x_axis === item.value) return null; +
+
+ {fullScreen ? ( + <> + {!isProjectLevel && params.project && params.project.length > 0 && ( +
+

Selected Projects

+
+ {params.project.map((projectId) => { + const project: IProject = projects.find((p) => p.id === projectId); return ( - - {item.label} - +
+
+ {project.icon ? ( + + {String.fromCodePoint(parseInt(project.icon))} + + ) : ( + + {project?.name.charAt(0)} + + )} + {project.name} +
+
+
+
+ +
Total members
+
+ {project.total_members} +
+
+
+ +
Total cycles
+
+ {project.total_cycles} +
+
+
+ +
Total modules
+
+ {project.total_modules} +
+
+
); })} - - )} - /> +
+
+ )} + {projectId ? ( + cycleId && cycleDetails ? ( +
+

{cycleDetails.name}

+
+
+
Lead
+ + {cycleDetails.owned_by?.first_name} {cycleDetails.owned_by?.last_name} + +
+
+
Start Date
+ + {cycleDetails.start_date && cycleDetails.start_date !== "" + ? renderShortDate(cycleDetails.start_date) + : "No start date"} + +
+
+
Target Date
+ + {cycleDetails.end_date && cycleDetails.end_date !== "" + ? renderShortDate(cycleDetails.end_date) + : "No end date"} + +
+
+
+ ) : moduleId && moduleDetails ? ( +
+

{moduleDetails.name}

+
+
+
Lead
+ + {moduleDetails.lead_detail?.first_name}{" "} + {moduleDetails.lead_detail?.last_name} + +
+
+
Start Date
+ + {moduleDetails.start_date && moduleDetails.start_date !== "" + ? renderShortDate(moduleDetails.start_date) + : "No start date"} + +
+
+
Target Date
+ + {moduleDetails.target_date && moduleDetails.target_date !== "" + ? renderShortDate(moduleDetails.target_date) + : "No end date"} + +
+
+
+ ) : ( +
+
+ {projectDetails?.icon ? ( + + {String.fromCodePoint(parseInt(projectDetails.icon))} + + ) : ( + + {projectDetails?.name.charAt(0)} + + )} +

{projectDetails?.name}

+
+
+
+
Network
+ + { + NETWORK_CHOICES[ + `${projectDetails?.network}` as keyof typeof NETWORK_CHOICES + ] + } + +
+
+
+ ) + ) : null} + + ) : null} +
+
+ { + if (!workspaceSlug) return; + + mutate(ANALYTICS(workspaceSlug.toString(), params)); + }} + > +
+ + Refresh
-
- {/*
- setSaveAnalyticsModal(true)}> - Save analytics - -
*/} + + +
+ + Export as CSV +
+
); diff --git a/apps/app/components/analytics/custom-analytics/table.tsx b/apps/app/components/analytics/custom-analytics/table.tsx index 73be59538..900f3c692 100644 --- a/apps/app/components/analytics/custom-analytics/table.tsx +++ b/apps/app/components/analytics/custom-analytics/table.tsx @@ -10,7 +10,6 @@ import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper"; import { IAnalyticsParams, IAnalyticsResponse } from "types"; // constants import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, DATE_KEYS } from "constants/analytics"; -import { MONTHS_LIST } from "constants/calendar"; type Props = { analytics: IAnalyticsResponse; @@ -19,7 +18,7 @@ type Props = { xAxisKeys: string[]; }; params: IAnalyticsParams; - yAxisKey: "effort" | "count"; + yAxisKey: "count" | "estimate"; }; export const AnalyticsTable: React.FC = ({ analytics, barGraphData, params, yAxisKey }) => ( @@ -27,7 +26,7 @@ export const AnalyticsTable: React.FC = ({ analytics, barGraphData, param
- +
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label} diff --git a/apps/app/components/analytics/index.ts b/apps/app/components/analytics/index.ts index 5ebcfae0d..ec0fd5dc1 100644 --- a/apps/app/components/analytics/index.ts +++ b/apps/app/components/analytics/index.ts @@ -1,4 +1,4 @@ export * from "./custom-analytics"; export * from "./scope-and-demand"; +export * from "./select"; export * from "./project-modal"; -export * from "./workspace-modal"; diff --git a/apps/app/components/analytics/project-modal.tsx b/apps/app/components/analytics/project-modal.tsx index 7e3e5efeb..293d4e891 100644 --- a/apps/app/components/analytics/project-modal.tsx +++ b/apps/app/components/analytics/project-modal.tsx @@ -1,7 +1,15 @@ import React, { Fragment, useState } from "react"; +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// react-hook-form +import { useForm } from "react-hook-form"; // headless ui import { Tab } from "@headlessui/react"; +// services +import analyticsService from "services/analytics.service"; // components import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; // icons @@ -10,17 +18,47 @@ import { ArrowsPointingOutIcon, XMarkIcon, } from "@heroicons/react/24/outline"; +// types +import { IAnalyticsParams } from "types"; +// fetch-keys +import { ANALYTICS } from "constants/fetch-keys"; type Props = { isOpen: boolean; onClose: () => void; }; +const defaultValues: IAnalyticsParams = { + x_axis: "priority", + y_axis: "issue_count", + segment: null, + project: null, +}; + const tabsList = ["Scope and Demand", "Custom Analytics"]; export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { const [fullScreen, setFullScreen] = useState(false); + const router = useRouter(); + const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + + const { control, watch, setValue } = useForm({ defaultValues }); + + const params: IAnalyticsParams = { + x_axis: watch("x_axis"), + y_axis: watch("y_axis"), + segment: watch("segment"), + project: projectId ? [projectId.toString()] : watch("project"), + cycle: cycleId ? cycleId.toString() : null, + module: moduleId ? moduleId.toString() : null, + }; + + const { data: analytics, error: analyticsError } = useSWR( + workspaceSlug ? ANALYTICS(workspaceSlug.toString(), params) : null, + workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null + ); + const handleClose = () => { onClose(); }; @@ -32,12 +70,12 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { } ${isOpen ? "right-0" : "-right-full"} duration-300 transition-all`} >
@@ -64,13 +102,13 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => {
- + {tabsList.map((tab) => ( - `rounded-3xl border border-brand-base px-4 py-2 text-xs hover:bg-brand-base ${ - selected ? "bg-brand-base" : "" + `rounded-3xl border border-brand-base px-4 py-2 text-xs hover:bg-brand-surface-2 ${ + selected ? "bg-brand-surface-2" : "" }` } > @@ -80,10 +118,17 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { - + - + diff --git a/apps/app/components/analytics/scope-and-demand/demand.tsx b/apps/app/components/analytics/scope-and-demand/demand.tsx index 784b3fcb4..1ab750c93 100644 --- a/apps/app/components/analytics/scope-and-demand/demand.tsx +++ b/apps/app/components/analytics/scope-and-demand/demand.tsx @@ -50,7 +50,7 @@ export const AnalyticsDemand: React.FC = ({ defaultAnalytics }) => ( ); })} -
+

{title}
- {users.map((user) => ( -
+ {users.map((user, index) => ( +
{user && user.avatar && user.avatar !== "" ? (
@@ -23,15 +24,17 @@ export const AnalyticsLeaderboard: React.FC = ({ users, title }) => ( height="100%" width="100%" className="rounded-full" - alt={user.email ?? "No assignee"} + alt={user.firstName + " " + user.lastName} />
) : (
- {(user.email ?? "No assignee").charAt(0)} + {user.firstName !== "" ? user.firstName[0] : "?"}
)} - {user.email ?? "No assignee"} + + {user.firstName !== "" ? `${user.firstName} ${user.lastName}` : "No assignee"} +
{user.count}
diff --git a/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx b/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx index 424836a89..ee0ee4019 100644 --- a/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx +++ b/apps/app/components/analytics/scope-and-demand/scope-and-demand.tsx @@ -18,16 +18,17 @@ import { DEFAULT_ANALYTICS } from "constants/fetch-keys"; type Props = { fullScreen?: boolean; - isProjectLevel?: boolean; }; -export const ScopeAndDemand: React.FC = ({ fullScreen = true, isProjectLevel = true }) => { +export const ScopeAndDemand: React.FC = ({ fullScreen = true }) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const isProjectLevel = projectId ? true : false; + const params = isProjectLevel ? { - project: projectId ? projectId.toString() : null, + project: projectId ? [projectId.toString()] : null, cycle: cycleId ? cycleId.toString() : null, module: moduleId ? moduleId.toString() : null, } @@ -55,7 +56,8 @@ export const ScopeAndDemand: React.FC = ({ fullScreen = true, isProjectLe ({ avatar: user.created_by__avatar, - email: user.created_by__email, + firstName: user.created_by__first_name, + lastName: user.created_by__last_name, count: user.count, }))} title="Most issues created" @@ -63,7 +65,8 @@ export const ScopeAndDemand: React.FC = ({ fullScreen = true, isProjectLe ({ avatar: user.assignees__avatar, - email: user.assignees__email, + firstName: user.assignees__first_name, + lastName: user.assignees__last_name, count: user.count, }))} title="Most issues closed" diff --git a/apps/app/components/analytics/scope-and-demand/scope.tsx b/apps/app/components/analytics/scope-and-demand/scope.tsx index 60a50fa12..93ea71091 100644 --- a/apps/app/components/analytics/scope-and-demand/scope.tsx +++ b/apps/app/components/analytics/scope-and-demand/scope.tsx @@ -21,7 +21,7 @@ export const AnalyticsScope: React.FC = ({ defaultAnalytics }) => ( colors={() => `#f97316`} customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)} tooltip={(datum) => ( -
+
Issue count- {datum.indexValue ?? "No assignee"}:{" "} @@ -51,13 +51,17 @@ export const AnalyticsScope: React.FC = ({ defaultAnalytics }) => ( - {(`${datum.value}` ?? "No assignee").toUpperCase().charAt(0)} + {datum.value ? `${datum.value}`.toUpperCase()[0] : "?"} ); }, }} margin={{ top: 20 }} + theme={{ + background: "rgb(var(--color-bg-base))", + axis: {}, + }} />
diff --git a/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx b/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx index e9721bd2f..e8f03e736 100644 --- a/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx +++ b/apps/app/components/analytics/scope-and-demand/year-wise-issues.tsx @@ -40,6 +40,9 @@ export const AnalyticsYearWiseIssues: React.FC = ({ defaultAnalytics }) = colors={(datum) => datum.color} curve="monotoneX" margin={{ top: 20 }} + theme={{ + background: "rgb(var(--color-bg-base))", + }} enableArea />
diff --git a/apps/app/components/analytics/select/index.ts b/apps/app/components/analytics/select/index.ts new file mode 100644 index 000000000..0c89bd2ad --- /dev/null +++ b/apps/app/components/analytics/select/index.ts @@ -0,0 +1,4 @@ +export * from "./project"; +export * from "./segment"; +export * from "./x-axis"; +export * from "./y-axis"; diff --git a/apps/app/components/analytics/select/project.tsx b/apps/app/components/analytics/select/project.tsx new file mode 100644 index 000000000..bfb975d11 --- /dev/null +++ b/apps/app/components/analytics/select/project.tsx @@ -0,0 +1,38 @@ +// ui +import { CustomSearchSelect } from "components/ui"; +// types +import { IProject } from "types"; + +type Props = { + value: string[] | null | undefined; + onChange: (val: string[] | null) => void; + projects: IProject[]; +}; + +export const SelectProject: React.FC = ({ value, onChange, projects }) => { + const options = projects?.map((project) => ({ + value: project.id, + query: project.name + project.identifier, + content: <>{project.name}, + })); + + return ( + onChange(val)} + options={options} + label={ + value && value.length > 0 + ? projects + .filter((p) => value.includes(p.id)) + .map((p) => p.identifier) + .join(", ") + : "All projects" + } + optionsClassName="min-w-full" + position="right" + noChevron + multiple + /> + ); +}; diff --git a/apps/app/components/analytics/select/segment.tsx b/apps/app/components/analytics/select/segment.tsx new file mode 100644 index 000000000..4358cdfa1 --- /dev/null +++ b/apps/app/components/analytics/select/segment.tsx @@ -0,0 +1,39 @@ +// ui +import { CustomSelect } from "components/ui"; +// types +import { IAnalyticsParams, TXAxisValues } from "types"; +// constants +import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; + +type Props = { + value: TXAxisValues | null | undefined; + onChange: () => void; + params: IAnalyticsParams; +}; + +export const SelectSegment: React.FC = ({ value, onChange, params }) => ( + + {ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label ?? ( + No value + )} + + } + onChange={onChange} + width="w-full" + maxHeight="lg" + > + No value + {ANALYTICS_X_AXIS_VALUES.map((item) => { + if (params.x_axis === item.value) return null; + + return ( + + {item.label} + + ); + })} + +); diff --git a/apps/app/components/analytics/select/x-axis.tsx b/apps/app/components/analytics/select/x-axis.tsx new file mode 100644 index 000000000..a284ae807 --- /dev/null +++ b/apps/app/components/analytics/select/x-axis.tsx @@ -0,0 +1,27 @@ +// ui +import { CustomSelect } from "components/ui"; +// types +import { IAnalyticsParams, TXAxisValues, TYAxisValues } from "types"; +// constants +import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; + +type Props = { + value: TXAxisValues; + onChange: (val: string) => void; +}; + +export const SelectXAxis: React.FC = ({ value, onChange }) => ( + {ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label}} + onChange={onChange} + width="w-full" + maxHeight="lg" + > + {ANALYTICS_X_AXIS_VALUES.map((item) => ( + + {item.label} + + ))} + +); diff --git a/apps/app/components/analytics/select/y-axis.tsx b/apps/app/components/analytics/select/y-axis.tsx new file mode 100644 index 000000000..248852620 --- /dev/null +++ b/apps/app/components/analytics/select/y-axis.tsx @@ -0,0 +1,26 @@ +// ui +import { CustomSelect } from "components/ui"; +// types +import { IAnalyticsParams, TYAxisValues } from "types"; +// constants +import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; + +type Props = { + value: TYAxisValues; + onChange: () => void; +}; + +export const SelectYAxis: React.FC = ({ value, onChange }) => ( + {ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "None"}} + onChange={onChange} + width="w-full" + > + {ANALYTICS_Y_AXIS_VALUES.map((item) => ( + + {item.label} + + ))} + +); diff --git a/apps/app/components/analytics/workspace-modal.tsx b/apps/app/components/analytics/workspace-modal.tsx deleted file mode 100644 index fc0632421..000000000 --- a/apps/app/components/analytics/workspace-modal.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { Fragment } from "react"; - -// headless ui -import { Tab } from "@headlessui/react"; -// components -import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; -// icons -import { XMarkIcon } from "@heroicons/react/24/outline"; - -type Props = { - isOpen: boolean; - onClose: () => void; -}; - -const tabsList = ["Scope and Demand", "Custom Analytics"]; - -export const AnalyticsWorkspaceModal: React.FC = ({ isOpen, onClose }) => { - const handleClose = () => { - onClose(); - }; - - return ( - <> -
-
-
-

Workspace Analytics

-
- -
-
- - - {tabsList.map((tab) => ( - - `rounded-3xl border border-brand-base px-4 py-2 text-xs hover:bg-brand-base ${ - selected ? "bg-brand-base" : "" - }` - } - > - {tab} - - ))} - - - - - - - - - - -
-
- - ); -}; diff --git a/apps/app/components/icons/priority-icon.tsx b/apps/app/components/icons/priority-icon.tsx index 0a888f802..58212ca5a 100644 --- a/apps/app/components/icons/priority-icon.tsx +++ b/apps/app/components/icons/priority-icon.tsx @@ -1,6 +1,8 @@ export const getPriorityIcon = (priority: string | null, className?: string) => { if (!className || className === "") className = "text-xs flex items-center"; + priority = priority?.toLowerCase() ?? null; + switch (priority) { case "urgent": return error; diff --git a/apps/app/components/workspace/sidebar-menu.tsx b/apps/app/components/workspace/sidebar-menu.tsx index a7ecacc32..d163173fa 100644 --- a/apps/app/components/workspace/sidebar-menu.tsx +++ b/apps/app/components/workspace/sidebar-menu.tsx @@ -6,18 +6,10 @@ import Link from "next/link"; // hooks import useTheme from "hooks/use-theme"; // icons -import { GridViewIcon, AssignmentClipboardIcon, TickMarkIcon, SettingIcon } from "components/icons"; import { ChartBarIcon } from "@heroicons/react/24/outline"; +import { GridViewIcon, AssignmentClipboardIcon, TickMarkIcon, SettingIcon } from "components/icons"; -type Props = { - isAnalyticsModalOpen: boolean; - setAnalyticsModal: React.Dispatch>; -}; - -export const WorkspaceSidebarMenu: React.FC = ({ - isAnalyticsModalOpen, - setAnalyticsModal, -}) => { +export const WorkspaceSidebarMenu = () => { const router = useRouter(); const { workspaceSlug } = router.query; @@ -33,8 +25,7 @@ export const WorkspaceSidebarMenu: React.FC = ({ { icon: ChartBarIcon, name: "Analytics", - highlight: isAnalyticsModalOpen, - onClick: () => setAnalyticsModal((prevData) => !prevData), + href: `/${workspaceSlug}/analytics`, }, { icon: AssignmentClipboardIcon, @@ -55,59 +46,33 @@ export const WorkspaceSidebarMenu: React.FC = ({ return (
- {workspaceLinks(workspaceSlug as string).map((link, index) => { - if (link.href) - return ( - - - - - {!sidebarCollapse && link.name} - - - ); - else - return ( - - ); - })} + {workspaceLinks(workspaceSlug as string).map((link, index) => ( + + + + + {!sidebarCollapse && link.name} + + + ))}
); }; diff --git a/apps/app/constants/analytics.ts b/apps/app/constants/analytics.ts index 97310c741..486a4e760 100644 --- a/apps/app/constants/analytics.ts +++ b/apps/app/constants/analytics.ts @@ -58,8 +58,8 @@ export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] = label: "Issue Count", }, { - value: "effort", - label: "Effort", + value: "estimate", + label: "Estimate", }, ]; diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 202b87dc1..044dad38b 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -169,10 +169,10 @@ export const ESTIMATE_DETAILS = (estimateId: string) => // analytics export const ANALYTICS = (workspaceSlug: string, params: IAnalyticsParams) => - `ANALYTICS${workspaceSlug.toUpperCase()}_${params.x_axis}_${params.y_axis}_${params.segment}_${ - params.project - }`; + `ANALYTICS${workspaceSlug.toUpperCase()}_${params.x_axis}_${params.y_axis}_${ + params.segment + }_${params.project?.toString()}`; export const DEFAULT_ANALYTICS = (workspaceSlug: string, params?: Partial) => - `DEFAULT_ANALYTICS_${workspaceSlug.toUpperCase()}_${params?.project}_${params?.cycle}_${ - params?.module - }`; + `DEFAULT_ANALYTICS_${workspaceSlug.toUpperCase()}_${params?.project?.toString()}_${ + params?.cycle + }_${params?.module}`; diff --git a/apps/app/helpers/analytics.helper.ts b/apps/app/helpers/analytics.helper.ts index b81d71d51..1caecde0d 100644 --- a/apps/app/helpers/analytics.helper.ts +++ b/apps/app/helpers/analytics.helper.ts @@ -1,5 +1,7 @@ // nivo import { BarDatum } from "@nivo/bar"; +// helpers +import { capitalizeFirstLetter } from "helpers/string.helper"; // types import { IAnalyticsData, IAnalyticsParams, IAnalyticsResponse } from "types"; // constants @@ -17,7 +19,7 @@ export const convertResponseToBarGraphData = ( const data: BarDatum[] = []; let xAxisKeys: string[] = []; - const yAxisKey = params.y_axis === "issue_count" ? "count" : "effort"; + const yAxisKey = params.y_axis === "issue_count" ? "count" : "estimate"; Object.keys(response).forEach((key) => { const segments: { [key: string]: number } = {}; @@ -31,7 +33,11 @@ export const convertResponseToBarGraphData = ( }); data.push({ - name: DATE_KEYS.includes(params.x_axis) ? renderMonthAndYear(key) : key, + name: DATE_KEYS.includes(params.x_axis) + ? renderMonthAndYear(key) + : params.x_axis === "priority" || params.x_axis === "state__group" + ? capitalizeFirstLetter(key) + : key, ...segments, }); } else { @@ -42,6 +48,8 @@ export const convertResponseToBarGraphData = ( data.push({ name: DATE_KEYS.includes(params.x_axis) ? renderMonthAndYear(item.dimension) + : params.x_axis === "priority" || params.x_axis === "state__group" + ? capitalizeFirstLetter(item.dimension ?? "None") : item.dimension ?? "None", [yAxisKey]: item[yAxisKey] ?? 0, }); @@ -64,17 +72,17 @@ export const generateBarColor = ( if (params[type] === "state__name" || params[type] === "labels__name") color = analytics?.extras?.colors.find((c) => c.name === value)?.color; - if (params[type] === "state__group") color = STATE_GROUP_COLORS[value]; + if (params[type] === "state__group") color = STATE_GROUP_COLORS[value.toLowerCase()]; if (params[type] === "priority") color = - value === "urgent" + value === "Urgent" ? "#ef4444" - : value === "high" + : value === "High" ? "#f97316" - : value === "medium" + : value === "Medium" ? "#eab308" - : value === "low" + : value === "Low" ? "#22c55e" : "#ced4da"; diff --git a/apps/app/layouts/app-layout/app-sidebar.tsx b/apps/app/layouts/app-layout/app-sidebar.tsx index b704392a5..a24c4e2d0 100644 --- a/apps/app/layouts/app-layout/app-sidebar.tsx +++ b/apps/app/layouts/app-layout/app-sidebar.tsx @@ -11,16 +11,9 @@ import { ProjectSidebarList } from "components/project"; export interface SidebarProps { toggleSidebar: boolean; setToggleSidebar: React.Dispatch>; - isAnalyticsModalOpen: boolean; - setAnalyticsModal: React.Dispatch>; } -const Sidebar: React.FC = ({ - toggleSidebar, - setToggleSidebar, - isAnalyticsModalOpen, - setAnalyticsModal, -}) => { +const Sidebar: React.FC = ({ toggleSidebar, setToggleSidebar }) => { // theme const { collapsed: sidebarCollapse } = useTheme(); @@ -34,10 +27,7 @@ const Sidebar: React.FC = ({ >
- +
diff --git a/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx index 969cace9a..eec8a8f2e 100644 --- a/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx @@ -13,7 +13,6 @@ import AppHeader from "layouts/app-layout/app-header"; import AppSidebar from "layouts/app-layout/app-sidebar"; // components import { NotAuthorizedView, JoinProject } from "components/auth-screens"; -import { AnalyticsWorkspaceModal } from "components/analytics"; import { CommandPalette } from "components/command-palette"; // ui import { PrimaryButton, Spinner } from "components/ui"; @@ -53,13 +52,10 @@ const ProjectAuthorizationWrapped: React.FC = ({ right, }) => { const [toggleSidebar, setToggleSidebar] = useState(false); - const [analyticsModal, setAnalyticsModal] = useState(false); const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { issueView } = useIssuesView(); - const { loading, error, memberRole: memberType } = useProjectMyMembership(); const settingsLayout = router.pathname.includes("/settings"); @@ -68,12 +64,7 @@ const ProjectAuthorizationWrapped: React.FC = ({
- + {loading ? (
@@ -121,12 +112,6 @@ const ProjectAuthorizationWrapped: React.FC = ({ : "bg-brand-base" }`} > - {analyticsModal && ( - setAnalyticsModal(false)} - /> - )} {!noHeader && ( = ({ right, }) => { const [toggleSidebar, setToggleSidebar] = useState(false); - const [analyticsModal, setAnalyticsModal] = useState(false); const router = useRouter(); const { workspaceSlug } = router.query; @@ -93,12 +91,7 @@ export const WorkspaceAuthorizationLayout: React.FC = ({
- + {settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? ( = ({ : "bg-brand-base" }`} > - {analyticsModal && ( - setAnalyticsModal(false)} - /> - )} {!noHeader && ( { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { control, watch, setValue } = useForm({ defaultValues }); + + const params: IAnalyticsParams = { + x_axis: watch("x_axis"), + y_axis: watch("y_axis"), + segment: watch("segment"), + project: watch("project"), + }; + + const { data: analytics, error: analyticsError } = useSWR( + workspaceSlug ? ANALYTICS(workspaceSlug.toString(), params) : null, + workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null + ); + + return ( + + + + } + // right={ + // { + // const e = new KeyboardEvent("keydown", { key: "p" }); + // document.dispatchEvent(e); + // }} + // > + // + // Save Analytics + // + // } + > +
+ + + {tabsList.map((tab) => ( + + `rounded-3xl border border-brand-base px-4 py-2 text-xs hover:bg-brand-surface-2 ${ + selected ? "bg-brand-surface-2" : "" + }` + } + > + {tab} + + ))} + + + + + + + + + + +
+
+ ); +}; + +export default Analytics; diff --git a/apps/app/services/analytics.service.ts b/apps/app/services/analytics.service.ts index 7cdb5860c..0b38f8c57 100644 --- a/apps/app/services/analytics.service.ts +++ b/apps/app/services/analytics.service.ts @@ -18,7 +18,10 @@ class AnalyticsServices extends APIService { async getAnalytics(workspaceSlug: string, params: IAnalyticsParams): Promise { return this.get(`/api/workspaces/${workspaceSlug}/analytics/`, { - params, + params: { + ...params, + project: params?.project ? params.project.toString() : null, + }, }) .then((response) => response?.data) .catch((error) => { @@ -31,7 +34,10 @@ class AnalyticsServices extends APIService { params?: Partial ): Promise { return this.get(`/api/workspaces/${workspaceSlug}/default-analytics/`, { - params, + params: { + ...params, + project: params?.project ? params.project.toString() : null, + }, }) .then((response) => response?.data) .catch((error) => { diff --git a/apps/app/types/analytics.d.ts b/apps/app/types/analytics.d.ts index 8501c34a5..eb5d75e58 100644 --- a/apps/app/types/analytics.d.ts +++ b/apps/app/types/analytics.d.ts @@ -11,7 +11,7 @@ export interface IAnalyticsData { dimension: string | null; segment?: string; count?: number; - effort?: number | null; + estimate?: number | null; }[]; } @@ -34,13 +34,13 @@ export type TXAxisValues = | "created_at" | "completed_at"; -export type TYAxisValues = "issue_count" | "effort"; +export type TYAxisValues = "issue_count" | "estimate"; export interface IAnalyticsParams { x_axis: TXAxisValues; y_axis: TYAxisValues; segment?: TXAxisValues | null; - project?: string | null; + project?: string[] | null; cycle?: string | null; module?: string | null; } @@ -59,7 +59,8 @@ export interface IExportAnalyticsFormData { export interface IDefaultAnalyticsUser { assignees__avatar: string | null; - assignees__email: string; + assignees__first_name: string; + assignees__last_name: string; count: number; } @@ -68,7 +69,8 @@ export interface IDefaultAnalyticsResponse { most_issue_closed_user: IDefaultAnalyticsUser[]; most_issue_created_user: { created_by__avatar: string | null; - created_by__email: string; + created_by__first_name: string; + created_by__last_name: string; count: number; }[]; open_estimate_sum: number; diff --git a/apps/app/types/projects.d.ts b/apps/app/types/projects.d.ts index e3d04e642..11c76ab61 100644 --- a/apps/app/types/projects.d.ts +++ b/apps/app/types/projects.d.ts @@ -27,6 +27,9 @@ export interface IProject { network: number; project_lead: IUser | string | null; slug: string; + total_cycles: number; + total_members: number; + total_modules: number; updated_at: Date; updated_by: string; workspace: IWorkspace | string;