fix: workspace dashboard (#522)

* chore: completed issues graph

* style: issue stats
This commit is contained in:
Aaryan Khandelwal 2023-03-24 11:06:52 +05:30 committed by GitHub
parent 3d34741356
commit 02f423bcb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 123 additions and 88 deletions

View File

@ -1,9 +1,6 @@
import { useState } from "react";
// recharts // recharts
import { import {
CartesianGrid, CartesianGrid,
Legend,
Line, Line,
LineChart, LineChart,
ResponsiveContainer, ResponsiveContainer,
@ -13,44 +10,46 @@ import {
} from "recharts"; } from "recharts";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu } from "components/ui";
// types
import { IIssue } from "types";
// constants // constants
import { MONTHS } from "constants/project"; import { MONTHS } from "constants/project";
type Props = { type Props = {
issues: IIssue[] | undefined; issues:
| {
week_in_month: number;
completed_count: number;
}[]
| undefined;
month: number;
setMonth: React.Dispatch<React.SetStateAction<number>>;
}; };
export const CompletedIssuesGraph: React.FC<Props> = ({ issues }) => { export const CompletedIssuesGraph: React.FC<Props> = ({ month, issues, setMonth }) => {
const [month, setMonth] = useState(new Date().getMonth()); const weeks = month === 2 ? 4 : 5;
const weeks = month === 1 ? 4 : 5;
const monthIssues =
issues?.filter(
(i) =>
new Date(i.created_at).getMonth() === month &&
new Date(i.created_at).getFullYear() === new Date().getFullYear()
) ?? [];
const data: any[] = []; const data: any[] = [];
for (let j = 1; j <= weeks; j++) { for (let i = 1; i <= weeks; i++) {
const weekIssues = monthIssues.filter( data.push({
(i) => i.completed_at && Math.ceil(new Date(i.completed_at).getDate() / 7) === j week_in_month: i,
); completed_count: issues?.find((item) => item.week_in_month === i)?.completed_count ?? 0,
});
data.push({ name: `Week ${j}`, completedIssues: weekIssues.length });
} }
const CustomTooltip = ({ payload, label }: any) => (
<div className="space-y-1 rounded bg-white p-3 text-sm shadow-md">
<h4 className="text-gray-500">{label}</h4>
<h5>Completed issues: {payload[0]?.value}</h5>
</div>
);
return ( return (
<div> <div>
<div className="mb-0.5 flex justify-between"> <div className="mb-0.5 flex justify-between">
<h3 className="font-semibold">Issues closed by you</h3> <h3 className="font-semibold">Issues closed by you</h3>
<CustomMenu label={<span className="text-sm">{MONTHS[month]}</span>} noBorder> <CustomMenu label={<span className="text-sm">{MONTHS[month - 1]}</span>} noBorder>
{MONTHS.map((month, index) => ( {MONTHS.map((month, index) => (
<CustomMenu.MenuItem key={month} onClick={() => setMonth(index)}> <CustomMenu.MenuItem key={month} onClick={() => setMonth(index + 1)}>
{month} {month}
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
))} ))}
@ -60,19 +59,22 @@ export const CompletedIssuesGraph: React.FC<Props> = ({ issues }) => {
<ResponsiveContainer width="100%" height={250}> <ResponsiveContainer width="100%" height={250}>
<LineChart data={data}> <LineChart data={data}>
<CartesianGrid stroke="#e2e2e2" /> <CartesianGrid stroke="#e2e2e2" />
<XAxis dataKey="name" /> <XAxis dataKey="week_in_month" />
<YAxis dataKey="completedIssues" /> <YAxis dataKey="completed_count" />
<Tooltip /> <Tooltip content={<CustomTooltip />} />
<Legend />
<Line <Line
type="monotone" type="monotone"
dataKey="completedIssues" dataKey="completed_count"
stroke="#d687ff" stroke="#d687ff"
strokeWidth={3} strokeWidth={3}
fill="#8e2de2" fill="#8e2de2"
/> />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
<h4 className="mt-4 flex items-center justify-center gap-2 text-[#d687ff]">
<span className="h-2 w-2 bg-[#d687ff]" />
Completed Issues
</h4>
</div> </div>
</div> </div>
); );

View File

@ -102,7 +102,19 @@ export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => {
/> />
))} ))}
</Pie> </Pie>
<Legend layout="vertical" verticalAlign="middle" align="right" height={36} /> <Legend
layout="vertical"
verticalAlign="middle"
align="right"
height={36}
payload={[
{ value: "Backlog", type: "square", color: STATE_GROUP_COLORS.backlog },
{ value: "Unstarted", type: "square", color: STATE_GROUP_COLORS.unstarted },
{ value: "Started", type: "square", color: STATE_GROUP_COLORS.started },
{ value: "Completed", type: "square", color: STATE_GROUP_COLORS.completed },
{ value: "Cancelled", type: "square", color: STATE_GROUP_COLORS.cancelled },
]}
/>
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>

View File

@ -12,54 +12,58 @@ type Props = {
export const IssuesStats: React.FC<Props> = ({ data }) => ( export const IssuesStats: React.FC<Props> = ({ data }) => (
<div className="grid grid-cols-1 rounded-[10px] border bg-white lg:grid-cols-3"> <div className="grid grid-cols-1 rounded-[10px] border bg-white lg:grid-cols-3">
<div className="grid grid-cols-2 gap-4 border-b p-4 lg:border-r lg:border-b-0"> <div className="grid grid-cols-1 divide-y border-b lg:border-r lg:border-b-0">
<div className="pb-4"> <div className="flex">
<h4>Issues assigned to you</h4> <div className="basis-1/2 p-4">
<h5 className="mt-2 text-2xl font-semibold"> <h4 className="text-sm">Issues assigned to you</h4>
{data ? ( <h5 className="mt-2 text-2xl font-semibold">
data.assigned_issues_count {data ? (
) : ( data.assigned_issues_count
<Loader> ) : (
<Loader.Item height="25px" width="50%" /> <Loader>
</Loader> <Loader.Item height="25px" width="50%" />
)} </Loader>
</h5> )}
</h5>
</div>
<div className="basis-1/2 border-l p-4">
<h4 className="text-sm">Pending issues</h4>
<h5 className="mt-2 text-2xl font-semibold">
{data ? (
data.pending_issues_count
) : (
<Loader>
<Loader.Item height="25px" width="50%" />
</Loader>
)}
</h5>
</div>
</div> </div>
<div className="pb-4"> <div className="flex">
<h4>Pending issues</h4> <div className="basis-1/2 p-4">
<h5 className="mt-2 text-2xl font-semibold"> <h4 className="text-sm">Completed issues</h4>
{data ? ( <h5 className="mt-2 text-2xl font-semibold">
data.pending_issues_count {data ? (
) : ( data.completed_issues_count
<Loader> ) : (
<Loader.Item height="25px" width="50%" /> <Loader>
</Loader> <Loader.Item height="25px" width="50%" />
)} </Loader>
</h5> )}
</div> </h5>
<div className="pb-4"> </div>
<h4>Completed issues</h4> <div className="basis-1/2 border-l p-4">
<h5 className="mt-2 text-2xl font-semibold"> <h4 className="text-sm">Issues due by this week</h4>
{data ? ( <h5 className="mt-2 text-2xl font-semibold">
data.completed_issues_count {data ? (
) : ( data.issues_due_week_count
<Loader> ) : (
<Loader.Item height="25px" width="50%" /> <Loader>
</Loader> <Loader.Item height="25px" width="50%" />
)} </Loader>
</h5> )}
</div> </h5>
<div className="pb-4"> </div>
<h4>Issues due by this week</h4>
<h5 className="mt-2 text-2xl font-semibold">
{data ? (
data.issues_due_week_count
) : (
<Loader>
<Loader.Item height="25px" width="50%" />
</Loader>
)}
</h5>
</div> </div>
</div> </div>
<div className="p-4 lg:col-span-2"> <div className="p-4 lg:col-span-2">

View File

@ -1,8 +1,8 @@
import React from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR, { mutate } from "swr";
// lib // lib
import { requiredAuth } from "lib/auth"; import { requiredAuth } from "lib/auth";
@ -10,8 +10,6 @@ import { requiredAuth } from "lib/auth";
import AppLayout from "layouts/app-layout"; import AppLayout from "layouts/app-layout";
// services // services
import userService from "services/user.service"; import userService from "services/user.service";
// hooks
import useIssues from "hooks/use-issues";
// components // components
import { import {
CompletedIssuesGraph, CompletedIssuesGraph,
@ -25,16 +23,20 @@ import type { NextPage, GetServerSidePropsContext } from "next";
import { USER_WORKSPACE_DASHBOARD } from "constants/fetch-keys"; import { USER_WORKSPACE_DASHBOARD } from "constants/fetch-keys";
const WorkspacePage: NextPage = () => { const WorkspacePage: NextPage = () => {
const [month, setMonth] = useState(new Date().getMonth() + 1);
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { myIssues } = useIssues(workspaceSlug as string);
const { data: workspaceDashboardData } = useSWR( const { data: workspaceDashboardData } = useSWR(
workspaceSlug ? USER_WORKSPACE_DASHBOARD(workspaceSlug as string) : null, workspaceSlug ? USER_WORKSPACE_DASHBOARD(workspaceSlug as string) : null,
workspaceSlug ? () => userService.userWorkspaceDashboard(workspaceSlug as string) : null workspaceSlug ? () => userService.userWorkspaceDashboard(workspaceSlug as string, month) : null
); );
useEffect(() => {
mutate(USER_WORKSPACE_DASHBOARD(workspaceSlug as string));
}, [month, workspaceSlug]);
return ( return (
<AppLayout noHeader={true}> <AppLayout noHeader={true}>
<div className="h-full w-full"> <div className="h-full w-full">
@ -63,7 +65,11 @@ const WorkspacePage: NextPage = () => {
<IssuesList issues={workspaceDashboardData?.overdue_issues} type="overdue" /> <IssuesList issues={workspaceDashboardData?.overdue_issues} type="overdue" />
<IssuesList issues={workspaceDashboardData?.upcoming_issues} type="upcoming" /> <IssuesList issues={workspaceDashboardData?.upcoming_issues} type="upcoming" />
<IssuesPieChart groupedIssues={workspaceDashboardData?.state_distribution} /> <IssuesPieChart groupedIssues={workspaceDashboardData?.state_distribution} />
<CompletedIssuesGraph issues={myIssues} /> <CompletedIssuesGraph
issues={workspaceDashboardData?.completed_issues}
month={month}
setMonth={setMonth}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -58,8 +58,15 @@ class UserService extends APIService {
}); });
} }
async userWorkspaceDashboard(workspaceSlug: string): Promise<IUserWorkspaceDashboard> { async userWorkspaceDashboard(
return this.get(`/api/users/me/workspaces/${workspaceSlug}/dashboard/`) workspaceSlug: string,
month: number
): Promise<IUserWorkspaceDashboard> {
return this.get(`/api/users/me/workspaces/${workspaceSlug}/dashboard/`, {
params: {
month: month,
},
})
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;

View File

@ -52,6 +52,10 @@ export interface IUserWorkspaceDashboard {
issue_activities: IUserActivity[]; issue_activities: IUserActivity[];
issues_due_week_count: number; issues_due_week_count: number;
overdue_issues: IIssueLite[]; overdue_issues: IIssueLite[];
completed_issues: {
week_in_month: number;
completed_count: number;
}[];
pending_issues_count: number; pending_issues_count: number;
state_distribution: IUserStateDistribution[]; state_distribution: IUserStateDistribution[];
upcoming_issues: IIssueLite[]; upcoming_issues: IIssueLite[];