mirror of
https://github.com/gosticks/plane.git
synced 2025-10-16 12:45:33 +00:00
[WEB-4953]fix: cycle progress percentage #7826
This commit is contained in:
parent
97059a2786
commit
f7d5ca4f83
@ -8,7 +8,7 @@ import { Check } from "lucide-react";
|
|||||||
import type { TCycleGroups } from "@plane/types";
|
import type { TCycleGroups } from "@plane/types";
|
||||||
import { CircularProgressIndicator } from "@plane/ui";
|
import { CircularProgressIndicator } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { generateQueryParams } from "@plane/utils";
|
import { generateQueryParams, calculateCycleProgress } from "@plane/utils";
|
||||||
import { ListItem } from "@/components/core/list";
|
import { ListItem } from "@/components/core/list";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCycle } from "@/hooks/store/use-cycle";
|
import { useCycle } from "@/hooks/store/use-cycle";
|
||||||
@ -50,7 +50,6 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
|||||||
// computed
|
// computed
|
||||||
// TODO: change this logic once backend fix the response
|
// TODO: change this logic once backend fix the response
|
||||||
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
|
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
|
||||||
const isCompleted = cycleStatus === "completed";
|
|
||||||
const isActive = cycleStatus === "current";
|
const isActive = cycleStatus === "current";
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
@ -73,20 +72,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
|||||||
|
|
||||||
const handleItemClick = cycleDetails.archived_at ? handleArchivedCycleClick : undefined;
|
const handleItemClick = cycleDetails.archived_at ? handleArchivedCycleClick : undefined;
|
||||||
|
|
||||||
const getCycleProgress = () => {
|
const progress = calculateCycleProgress(cycleDetails);
|
||||||
let completionPercentage =
|
|
||||||
((cycleDetails.completed_issues + cycleDetails.cancelled_issues) / cycleDetails.total_issues) * 100;
|
|
||||||
|
|
||||||
if (isCompleted && !isEmpty(cycleDetails.progress_snapshot)) {
|
|
||||||
completionPercentage =
|
|
||||||
((cycleDetails.progress_snapshot.completed_issues + cycleDetails.progress_snapshot.cancelled_issues) /
|
|
||||||
cycleDetails.progress_snapshot.total_issues) *
|
|
||||||
100;
|
|
||||||
}
|
|
||||||
return isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const progress = getCycleProgress();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
@ -96,16 +82,10 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
|||||||
className={className}
|
className={className}
|
||||||
prependTitleElement={
|
prependTitleElement={
|
||||||
<CircularProgressIndicator size={30} percentage={progress} strokeWidth={3}>
|
<CircularProgressIndicator size={30} percentage={progress} strokeWidth={3}>
|
||||||
{isCompleted ? (
|
{progress === 100 ? (
|
||||||
progress === 100 ? (
|
|
||||||
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
|
|
||||||
) : (
|
|
||||||
<span className="text-sm text-custom-primary-100">{`!`}</span>
|
|
||||||
)
|
|
||||||
) : progress === 100 ? (
|
|
||||||
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
|
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
|
||||||
) : (
|
) : (
|
||||||
<span className="text-[9px] text-custom-text-300">{`${progress}%`}</span>
|
<span className="text-[9px] text-custom-text-100">{`${progress}%`}</span>
|
||||||
)}
|
)}
|
||||||
</CircularProgressIndicator>
|
</CircularProgressIndicator>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import orderBy from "lodash/orderBy";
|
|||||||
import sortBy from "lodash/sortBy";
|
import sortBy from "lodash/sortBy";
|
||||||
import uniqBy from "lodash/uniqBy";
|
import uniqBy from "lodash/uniqBy";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { ICycle, TCycleFilters } from "@plane/types";
|
import { ICycle, TCycleFilters, TProgressSnapshot } from "@plane/types";
|
||||||
// local imports
|
// local imports
|
||||||
import { findTotalDaysInRange, generateDateArray, getDate } from "./datetime";
|
import { findTotalDaysInRange, generateDateArray, getDate } from "./datetime";
|
||||||
import { satisfiesDateFilter } from "./filter";
|
import { satisfiesDateFilter } from "./filter";
|
||||||
@ -191,3 +191,57 @@ export const formatActiveCycle = (args: {
|
|||||||
? formatV1Data(isTypeIssue!, cycle, isBurnDown!, endDate)
|
? formatV1Data(isTypeIssue!, cycle, isBurnDown!, endDate)
|
||||||
: formatV2Data(isTypeIssue!, cycle, isBurnDown!, endDate);
|
: formatV2Data(isTypeIssue!, cycle, isBurnDown!, endDate);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates cycle progress percentage excluding cancelled issues from total count
|
||||||
|
* Formula: completed / (total - cancelled) * 100
|
||||||
|
* This gives accurate progress based on: pendingIssues = totalIssues - completedIssues - cancelledIssues
|
||||||
|
* @param cycle - Cycle data object
|
||||||
|
* @param estimateType - Whether to calculate based on "issues" or "points"
|
||||||
|
* @param includeInProgress - Whether to include started/in-progress items in completion calculation
|
||||||
|
* @returns Progress percentage (0-100)
|
||||||
|
*/
|
||||||
|
export const calculateCycleProgress = (
|
||||||
|
cycle: ICycle | undefined,
|
||||||
|
estimateType: "issues" | "points" = "issues",
|
||||||
|
includeInProgress: boolean = false
|
||||||
|
): number => {
|
||||||
|
if (!cycle) return 0;
|
||||||
|
|
||||||
|
const progressSnapshot: TProgressSnapshot | undefined = cycle.progress_snapshot;
|
||||||
|
const cycleDetails = progressSnapshot && !isEmpty(progressSnapshot) ? progressSnapshot : cycle;
|
||||||
|
|
||||||
|
let completed: number;
|
||||||
|
let cancelled: number;
|
||||||
|
let total: number;
|
||||||
|
|
||||||
|
if (estimateType === "points") {
|
||||||
|
completed = cycleDetails.completed_estimate_points || 0;
|
||||||
|
cancelled = cycleDetails.cancelled_estimate_points || 0;
|
||||||
|
total = cycleDetails.total_estimate_points || 0;
|
||||||
|
|
||||||
|
if (includeInProgress) {
|
||||||
|
completed += cycleDetails.started_estimate_points || 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completed = cycleDetails.completed_issues || 0;
|
||||||
|
cancelled = cycleDetails.cancelled_issues || 0;
|
||||||
|
total = cycleDetails.total_issues || 0;
|
||||||
|
|
||||||
|
if (includeInProgress) {
|
||||||
|
completed += cycleDetails.started_issues || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude cancelled issues from total (pendingIssues = total - completed - cancelled)
|
||||||
|
const adjustedTotal = total - cancelled;
|
||||||
|
|
||||||
|
// Handle edge cases
|
||||||
|
if (adjustedTotal === 0) return 0;
|
||||||
|
if (completed < 0 || adjustedTotal < 0) return 0;
|
||||||
|
if (completed > adjustedTotal) return 100;
|
||||||
|
|
||||||
|
// Calculate percentage and round
|
||||||
|
const percentage = (completed / adjustedTotal) * 100;
|
||||||
|
return Math.round(percentage);
|
||||||
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user