feat: burndown chart (#268)
* chore: recharts dependencie added * chore: tpye added for issue completed at * feat: date range helper fn added * feat: progress chart added * feat: ideal task line added in progress chart * feat: chart legends added
This commit is contained in:
parent
af22dc9c58
commit
bb4ffec7e8
8 changed files with 528 additions and 165 deletions
97
apps/app/components/core/sidebar/progress-chart.tsx
Normal file
97
apps/app/components/core/sidebar/progress-chart.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import React from "react";
|
||||
|
||||
import {
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
AreaChart,
|
||||
Area,
|
||||
ReferenceLine,
|
||||
} from "recharts";
|
||||
|
||||
//types
|
||||
import { IIssue } from "types";
|
||||
// helper
|
||||
import { getDatesInRange, renderShortNumericDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
issues: IIssue[];
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
|
||||
const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
const getChartData = () => {
|
||||
const dateRangeArray = getDatesInRange(startDate, endDate);
|
||||
let count = 0;
|
||||
const dateWiseData = dateRangeArray.map((d) => {
|
||||
const current = d.toISOString().split("T")[0];
|
||||
const total = issues.length;
|
||||
const currentData = issues.filter(
|
||||
(i) => i.completed_at && i.completed_at.toString().split("T")[0] === current
|
||||
);
|
||||
count = currentData ? currentData.length + count : count;
|
||||
|
||||
return {
|
||||
currentDate: renderShortNumericDateFormat(current),
|
||||
currentDateData: currentData,
|
||||
pending: new Date(current) < new Date() ? total - count : null,
|
||||
};
|
||||
});
|
||||
return dateWiseData;
|
||||
};
|
||||
const ChartData = getChartData();
|
||||
return (
|
||||
<div className="relative h-[200px] w-full ">
|
||||
<div className="flex items-center justify-center h-full w-full absolute -left-8 py-3 text-xs">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
width={300}
|
||||
height={200}
|
||||
data={ChartData}
|
||||
margin={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<XAxis dataKey="currentDate" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="pending"
|
||||
stroke="#8884d8"
|
||||
fill="#98d1fb"
|
||||
activeDot={{ r: 8 }}
|
||||
/>
|
||||
<ReferenceLine
|
||||
stroke="#16a34a"
|
||||
strokeDasharray="3 3"
|
||||
segment={[
|
||||
{ x: `${renderShortNumericDateFormat(endDate)}`, y: 0 },
|
||||
{ x: `${renderShortNumericDateFormat(startDate)}`, y: issues.length },
|
||||
]}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="z-10 flex flex-col absolute top-2 right-2 justify-center items-start gap-1 text-xs">
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
<span className="h-2 w-2 bg-green-600" />
|
||||
<span>Ideal</span>
|
||||
</div>
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
<span className="h-2 w-2 bg-[#8884d8]" />
|
||||
<span>Current</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressChart;
|
||||
|
|
@ -53,130 +53,128 @@ export const SidebarProgressStats: React.FC<Props> = ({ groupedIssues, issues })
|
|||
: null
|
||||
);
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full gap-2 ">
|
||||
<Tab.Group>
|
||||
<Tab.List
|
||||
as="div"
|
||||
className="flex items-center justify-between w-full rounded bg-gray-100 text-xs"
|
||||
<Tab.Group>
|
||||
<Tab.List
|
||||
as="div"
|
||||
className="flex items-center justify-between w-full rounded bg-gray-100 text-xs"
|
||||
>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-1/2 rounded py-1 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
|
||||
}
|
||||
>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-1/2 rounded py-1 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
|
||||
Assignees
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-1/2 rounded py-1 ${selected ? "bg-gray-300 font-semibold" : "hover:bg-gray-200 "}`
|
||||
}
|
||||
>
|
||||
Labels
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-1/2 rounded py-1 ${selected ? "bg-gray-300 font-semibold" : "hover:bg-gray-200 "}`
|
||||
}
|
||||
>
|
||||
States
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="flex items-center justify-between w-full">
|
||||
<Tab.Panel as="div" className="w-full flex flex-col ">
|
||||
{members?.map((member, index) => {
|
||||
const totalArray = issues?.filter((i) => i.assignees?.includes(member.member.id));
|
||||
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
|
||||
if (totalArray.length > 0) {
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<>
|
||||
<Avatar user={member.member} />
|
||||
<span>{member.member.first_name}</span>
|
||||
</>
|
||||
}
|
||||
completed={completeArray.length}
|
||||
total={totalArray.length}
|
||||
/>
|
||||
);
|
||||
}
|
||||
>
|
||||
Assignees
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-1/2 rounded py-1 ${selected ? "bg-gray-300 font-semibold" : "hover:bg-gray-200 "}`
|
||||
}
|
||||
>
|
||||
Labels
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-1/2 rounded py-1 ${selected ? "bg-gray-300 font-semibold" : "hover:bg-gray-200 "}`
|
||||
}
|
||||
>
|
||||
States
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="flex items-center justify-between w-full">
|
||||
<Tab.Panel as="div" className="w-full flex flex-col ">
|
||||
{members?.map((member, index) => {
|
||||
const totalArray = issues?.filter((i) => i.assignees?.includes(member.member.id));
|
||||
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
|
||||
if (totalArray.length > 0) {
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<>
|
||||
<Avatar user={member.member} />
|
||||
<span>{member.member.first_name}</span>
|
||||
</>
|
||||
}
|
||||
completed={completeArray.length}
|
||||
total={totalArray.length}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
{issues?.filter((i) => i.assignees?.length === 0).length > 0 ? (
|
||||
<SingleProgressStats
|
||||
title={
|
||||
<>
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
|
||||
<Image
|
||||
src={User}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt="User"
|
||||
/>
|
||||
</div>
|
||||
<span>No assignee</span>
|
||||
</>
|
||||
}
|
||||
completed={
|
||||
issues?.filter(
|
||||
(i) => i.state_detail.group === "completed" && i.assignees?.length === 0
|
||||
).length
|
||||
}
|
||||
total={issues?.filter((i) => i.assignees?.length === 0).length}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="w-full flex flex-col ">
|
||||
{issueLabels?.map((issue, index) => {
|
||||
const totalArray = issues?.filter((i) => i.labels?.includes(issue.id));
|
||||
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
|
||||
if (totalArray.length > 0) {
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<>
|
||||
<span
|
||||
className="block h-2 w-2 rounded-full "
|
||||
style={{
|
||||
backgroundColor: issue.color,
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs capitalize">{issue.name}</span>
|
||||
</>
|
||||
}
|
||||
completed={completeArray.length}
|
||||
total={totalArray.length}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="w-full flex flex-col ">
|
||||
{Object.keys(groupedIssues).map((group, index) => (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<>
|
||||
<span
|
||||
className="block h-2 w-2 rounded-full "
|
||||
style={{
|
||||
backgroundColor: stateGroupColours[group],
|
||||
}}
|
||||
})}
|
||||
{issues?.filter((i) => i.assignees?.length === 0).length > 0 ? (
|
||||
<SingleProgressStats
|
||||
title={
|
||||
<>
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
|
||||
<Image
|
||||
src={User}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt="User"
|
||||
/>
|
||||
<span className="text-xs capitalize">{group}</span>
|
||||
</>
|
||||
}
|
||||
completed={groupedIssues[group].length}
|
||||
total={issues.length}
|
||||
/>
|
||||
))}
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</div>
|
||||
<span>No assignee</span>
|
||||
</>
|
||||
}
|
||||
completed={
|
||||
issues?.filter(
|
||||
(i) => i.state_detail.group === "completed" && i.assignees?.length === 0
|
||||
).length
|
||||
}
|
||||
total={issues?.filter((i) => i.assignees?.length === 0).length}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="w-full flex flex-col ">
|
||||
{issueLabels?.map((issue, index) => {
|
||||
const totalArray = issues?.filter((i) => i.labels?.includes(issue.id));
|
||||
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
|
||||
if (totalArray.length > 0) {
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<>
|
||||
<span
|
||||
className="block h-2 w-2 rounded-full "
|
||||
style={{
|
||||
backgroundColor: issue.color,
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs capitalize">{issue.name}</span>
|
||||
</>
|
||||
}
|
||||
completed={completeArray.length}
|
||||
total={totalArray.length}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as="div" className="w-full flex flex-col ">
|
||||
{Object.keys(groupedIssues).map((group, index) => (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<>
|
||||
<span
|
||||
className="block h-2 w-2 rounded-full "
|
||||
style={{
|
||||
backgroundColor: stateGroupColours[group],
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs capitalize">{group}</span>
|
||||
</>
|
||||
}
|
||||
completed={groupedIssues[group].length}
|
||||
total={issues.length}
|
||||
/>
|
||||
))}
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue