forked from github/plane
fix: workspace dashboard (#522)
* chore: completed issues graph * style: issue stats
This commit is contained in:
parent
3d34741356
commit
02f423bcb6
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
4
apps/app/types/users.d.ts
vendored
4
apps/app/types/users.d.ts
vendored
@ -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[];
|
||||||
|
Loading…
Reference in New Issue
Block a user