diff --git a/packages/types/src/cycle/cycle.d.ts b/packages/types/src/cycle/cycle.d.ts index d8a6915a92b..fdcffb52b39 100644 --- a/packages/types/src/cycle/cycle.d.ts +++ b/packages/types/src/cycle/cycle.d.ts @@ -46,11 +46,11 @@ export type TCycleEstimateDistribution = { export type TCycleProgress = { date: string; started: number; + actual: number; pending: number; ideal: number | null; scope: number; completed: number; - actual: number; unstarted: number; backlog: number; cancelled: number; @@ -103,6 +103,7 @@ export interface ICycle extends TProgressSnapshot { workspace_id: string; project_detail: IProjectDetails; progress: any[]; + version: number; } export interface CycleIssueResponse { diff --git a/web/core/components/cycles/active-cycle/root.tsx b/web/core/components/cycles/active-cycle/root.tsx deleted file mode 100644 index 4bd8ff2973f..00000000000 --- a/web/core/components/cycles/active-cycle/root.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; - -import { observer } from "mobx-react"; -import { Disclosure } from "@headlessui/react"; -// ui -import { Row } from "@plane/ui"; -// components -import { - ActiveCycleProductivity, - ActiveCycleProgress, - ActiveCycleStats, - CycleListGroupHeader, - CyclesListItem, -} from "@/components/cycles"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; -import { useCycle } from "@/hooks/store"; -import { ActiveCycleIssueDetails } from "@/store/issue/cycle"; -import useCyclesDetails from "./use-cycles-details"; - -interface IActiveCycleDetails { - workspaceSlug: string; - projectId: string; -} - -export const ActiveCycleRoot: React.FC = observer((props) => { - const { workspaceSlug, projectId } = props; - const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle(); - const { - handleFiltersUpdate, - cycle: activeCycle, - cycleIssueDetails, - } = useCyclesDetails({ workspaceSlug, projectId, cycleId: currentProjectActiveCycleId }); - - return ( - <> - - {({ open }) => ( - <> - - - - - {!currentProjectActiveCycle ? ( - - ) : ( -
- {currentProjectActiveCycleId && ( - - )} - -
- - - -
-
-
- )} -
- - )} -
- - ); -}); diff --git a/web/core/components/cycles/list/root.tsx b/web/core/components/cycles/list/root.tsx index 1531b88aa80..25a02fc6a90 100644 --- a/web/core/components/cycles/list/root.tsx +++ b/web/core/components/cycles/list/root.tsx @@ -28,7 +28,7 @@ export const CyclesList: FC = observer((props) => { ) : ( <> - + {upcomingCycleIds && ( diff --git a/web/core/constants/cycle.ts b/web/core/constants/cycle.ts index 8ea15c5bc17..2db37136672 100644 --- a/web/core/constants/cycle.ts +++ b/web/core/constants/cycle.ts @@ -60,7 +60,7 @@ export const CYCLE_STATUS: { { label: "day left", value: "current", - title: "Active", + title: "In progress", color: "#F59E0B", textColor: "text-amber-500", bgColor: "bg-amber-50", diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index 51d245b0829..e25e3b63be6 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -71,11 +71,7 @@ export interface ICycleStore { fetchArchivedCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; fetchActiveCycleProgress: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; - fetchActiveCycleProgressPro: ( - workspaceSlug: string, - projectId: string, - cycleId: string - ) => Promise | Promise; + fetchActiveCycleProgressPro: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; fetchActiveCycleAnalytics: ( workspaceSlug: string, projectId: string, @@ -146,7 +142,6 @@ export class CycleStore implements ICycleStore { fetchArchivedCycles: action, fetchArchivedCycleDetails: action, fetchActiveCycleProgress: action, - fetchActiveCycleProgressPro: action, fetchActiveCycleAnalytics: action, fetchCycleDetails: action, createCycle: action, @@ -282,7 +277,7 @@ export class CycleStore implements ICycleStore { */ getActiveCycleProgress = computedFn((cycleId?: string) => { const cycle = cycleId ? this.cycleMap[cycleId] : this.currentProjectActiveCycle; - if (!cycle?.progress) return null; + if (!cycle) return null; const isTypeIssue = this.getEstimateTypeByCycleId(cycle.id) === "issues"; const isBurnDown = this.getPlotTypeByCycleId(cycle.id) === "burndown"; @@ -403,13 +398,13 @@ export class CycleStore implements ICycleStore { await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload); /** - * @description gets the plot type for the module store + * @description gets the plot type for the cycle store * @param {TCyclePlotType} plotType */ getPlotTypeByCycleId = computedFn((cycleId: string) => this.plotType[cycleId] || "burndown"); /** - * @description gets the estimate type for the module store + * @description gets the estimate type for the cycle store * @param {TCycleEstimateType} estimateType */ getEstimateTypeByCycleId = computedFn((cycleId: string) => { @@ -421,7 +416,7 @@ export class CycleStore implements ICycleStore { }); /** - * @description updates the plot type for the module store + * @description updates the plot type for the cycle store * @param {TCyclePlotType} plotType */ setPlotType = (cycleId: string, plotType: TCyclePlotType) => { @@ -429,7 +424,7 @@ export class CycleStore implements ICycleStore { }; /** - * @description updates the estimate type for the module store + * @description updates the estimate type for the cycle store * @param {TCycleEstimateType} estimateType */ setEstimateType = (cycleId: string, estimateType: TCycleEstimateType) => { @@ -545,7 +540,7 @@ export class CycleStore implements ICycleStore { * @param cycleId * @returns */ - fetchActiveCycleProgressPro = async (workspaceSlug: string, projectId: string, cycleId: string) => null; + fetchActiveCycleProgressPro = action(async (workspaceSlug: string, projectId: string, cycleId: string) => {}); /** * @description fetches active cycle analytics diff --git a/web/helpers/cycle.helper.ts b/web/helpers/cycle.helper.ts index 49725511f09..6e62c3a04a1 100644 --- a/web/helpers/cycle.helper.ts +++ b/web/helpers/cycle.helper.ts @@ -1,8 +1,9 @@ +import { startOfToday, format } from "date-fns"; import { isEmpty, orderBy, uniqBy } from "lodash"; import sortBy from "lodash/sortBy"; import { ICycle, TCycleFilters } from "@plane/types"; // helpers -import { generateDateArray, getDate, getToday } from "@/helpers/date-time.helper"; +import { findTotalDaysInRange, generateDateArray, getDate } from "@/helpers/date-time.helper"; import { satisfiesDateFilter } from "@/helpers/filter.helper"; export type TProgressChartData = { @@ -75,47 +76,97 @@ export const shouldFilterCycle = (cycle: ICycle, filter: TCycleFilters): boolean return fallsInFilters; }; -export const formatActiveCycle = (args: { - cycle: ICycle; - isBurnDown?: boolean | undefined; - isTypeIssue?: boolean | undefined; -}) => { - const { cycle, isBurnDown, isTypeIssue } = args; - let today = getToday(); - const endDate: Date | string = new Date(cycle.end_date!); +const scope = (p: any, isTypeIssue: boolean) => (isTypeIssue ? p.total_issues : p.total_estimate_points); +const ideal = (date: string, scope: number, cycle: ICycle) => + Math.floor( + ((findTotalDaysInRange(date, cycle.end_date) || 0) / + (findTotalDaysInRange(cycle.start_date, cycle.end_date) || 0)) * + scope + ); - const extendedArray = endDate > today ? generateDateArray(today as Date, endDate) : []; - if (isEmpty(cycle.progress)) return extendedArray; - today = getToday(true); +const formatV1Data = (isTypeIssue: boolean, cycle: ICycle, isBurnDown: boolean, endDate: Date | string) => { + const today = format(startOfToday(), "yyyy-MM-dd"); + const data = isTypeIssue ? cycle.distribution : cycle.estimate_distribution; + const extendedArray = generateDateArray(endDate, endDate).map((d) => d.date); + + if (isEmpty(data)) return []; + const progress = [...Object.keys(data.completion_chart), ...extendedArray].map((p) => { + const pending = data.completion_chart[p] || 0; + const total = isTypeIssue ? cycle.total_issues : cycle.total_estimate_points; + const completed = scope(cycle, isTypeIssue) - pending; + + return { + date: p, + scope: p! < today ? scope(cycle, isTypeIssue) : null, + completed, + backlog: isTypeIssue ? cycle.backlog_issues : cycle.backlog_estimate_points, + started: p === today ? cycle[isTypeIssue ? "started_issues" : "started_estimate_points"] : undefined, + unstarted: p === today ? cycle[isTypeIssue ? "unstarted_issues" : "unstarted_estimate_points"] : undefined, + cancelled: p === today ? cycle[isTypeIssue ? "cancelled_issues" : "cancelled_estimate_points"] : undefined, + pending: Math.abs(pending || 0), + ideal: + p < today + ? ideal(p, total || 0, cycle) + : p <= cycle.end_date! + ? ideal(today as string, total || 0, cycle) + : null, + actual: p <= today ? (isBurnDown ? Math.abs(pending) : completed) : undefined, + }; + }); - const scope = (p: any) => (isTypeIssue ? p.total_issues : p.total_estimate_points); - const ideal = (p: any) => - isTypeIssue - ? Math.abs(p.total_issues - p.completed_issues + (Math.random() < 0.5 ? -1 : 1)) - : Math.abs(p.total_estimate_points - p.completed_estimate_points + (Math.random() < 0.5 ? -1 : 1)); + return progress; +}; + +const formatV2Data = (isTypeIssue: boolean, cycle: ICycle, isBurnDown: boolean, endDate: Date | string) => { + if (!cycle.progress) return []; + let today: Date | string = startOfToday(); - const scopeToday = scope(cycle?.progress[cycle?.progress.length - 1]); - const idealToday = ideal(cycle?.progress[cycle?.progress.length - 1]); + const extendedArray = endDate > today ? generateDateArray(today as Date, endDate) : []; + if (isEmpty(cycle.progress)) return extendedArray; + today = format(startOfToday(), "yyyy-MM-dd"); + const todaysData = cycle?.progress[cycle?.progress.length - 1]; + const scopeToday = scope(todaysData, isTypeIssue); + const idealToday = ideal(todaysData.date, scopeToday, cycle); - const progress = [...orderBy(cycle?.progress, "date"), ...extendedArray].map((p) => { + let progress = [...orderBy(cycle?.progress, "date"), ...extendedArray].map((p) => { const pending = isTypeIssue ? p.total_issues - p.completed_issues - p.cancelled_issues : p.total_estimate_points - p.completed_estimate_points - p.cancelled_estimate_points; const completed = isTypeIssue ? p.completed_issues : p.completed_estimate_points; + const dataDate = p.progress_date ? format(new Date(p.progress_date), "yyyy-MM-dd") : p.date; return { - date: p.date, - scope: p.date! < today ? scope(p) : p.date! < cycle.end_date! ? scopeToday : null, + date: dataDate, + scope: dataDate! < today ? scope(p, isTypeIssue) : dataDate! <= cycle.end_date! ? scopeToday : null, completed, backlog: isTypeIssue ? p.backlog_issues : p.backlog_estimate_points, started: isTypeIssue ? p.started_issues : p.started_estimate_points, unstarted: isTypeIssue ? p.unstarted_issues : p.unstarted_estimate_points, cancelled: isTypeIssue ? p.cancelled_issues : p.cancelled_estimate_points, pending: Math.abs(pending), - // TODO: This is a temporary logic to show the ideal line in the cycle chart - ideal: p.date! < today ? ideal(p) : p.date! < cycle.end_date! ? idealToday : null, - actual: p.date! <= today ? (isBurnDown ? Math.abs(pending) : completed) : undefined, + ideal: + dataDate! < today + ? ideal(dataDate, scope(p, isTypeIssue), cycle) + : dataDate! < cycle.end_date! + ? idealToday + : null, + actual: dataDate! <= today ? (isBurnDown ? Math.abs(pending) : completed) : undefined, }; }); - return uniqBy(progress, "date"); + progress = uniqBy(progress, "date"); + + return progress; +}; + +export const formatActiveCycle = (args: { + cycle: ICycle; + isBurnDown?: boolean | undefined; + isTypeIssue?: boolean | undefined; +}) => { + const { cycle, isBurnDown, isTypeIssue } = args; + const endDate: Date | string = new Date(cycle.end_date!); + + return cycle.version === 1 + ? formatV1Data(isTypeIssue!, cycle, isBurnDown!, endDate) + : formatV2Data(isTypeIssue!, cycle, isBurnDown!, endDate); }; diff --git a/web/helpers/date-time.helper.ts b/web/helpers/date-time.helper.ts index e20dc0637be..3121b58e817 100644 --- a/web/helpers/date-time.helper.ts +++ b/web/helpers/date-time.helper.ts @@ -358,57 +358,13 @@ export const getReadTimeFromWordsCount = (wordsCount: number): number => { return minutes * 60; }; -/** - * @description calculates today's date - * @param {boolean} format - * @returns {Date | string} today's date - * @example getToday() // Output: 2024-09-29T00:00:00.000Z - * @example getToday(true) // Output: 2024-09-29 - */ -export const getToday = (format: boolean = false) => { - const today = new Date(); - today.setHours(0, 0, 0, 0); - if (!format) return today; - - const year = today.getFullYear(); - const month = String(today.getMonth() + 1).padStart(2, "0"); // Months are 0-based, so add 1 - const day = String(today.getDate()).padStart(2, "0"); // Add leading zero for single digits - return `${year}-${month}-${day}`; -}; - -/** - * @description calculates the date of the day before today - * @param {boolean} format - * @returns {Date | string} date of the day before today - * @example dateFormatter() // Output: "Sept 20, 2024" - */ -export const dateFormatter = (dateString: string) => { - // Convert to Date object - const date = new Date(dateString); - - // Options for the desired format (Month Day, Year) - const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric" }; - - // Format the date - const formattedDate = date.toLocaleDateString("en-US", options); - - return formattedDate; -}; - -/** - * @description calculates days left from today to the end date - * @returns {Date | string} number of days left - */ -export const daysLeft = (end_date: string) => - end_date ? Math.ceil((new Date(end_date).getTime() - new Date().getTime()) / (1000 * 3600 * 24)) : 0; - /** * @description generates an array of dates between the start and end dates * @param startDate * @param endDate * @returns */ -export const generateDateArray = (startDate: Date, endDate: Date) => { +export const generateDateArray = (startDate: string | Date, endDate: string | Date) => { // Convert the start and end dates to Date objects if they aren't already const start = new Date(startDate); // start.setDate(start.getDate() + 1); diff --git a/web/package.json b/web/package.json index 7ae957b5561..b01a86b3ee9 100644 --- a/web/package.json +++ b/web/package.json @@ -65,7 +65,8 @@ "use-debounce": "^9.0.4", "use-font-face-observer": "^1.2.2", "uuid": "^9.0.0", - "zxcvbn": "^4.4.2" + "zxcvbn": "^4.4.2", + "recharts": "^2.12.7" }, "devDependencies": { "@plane/eslint-config": "*",