mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-412] chore: estimates analytics (#4730)
* chore: estimate points in modules and cycle * chore: burn down chart analytics * chore: module serializer change * dev: handled y-axis estimates in analytics, implemented estimate points on modules * chore: burn down analytics * chore: state estimate point analytics * chore: updated the burn down values * Remove check mark from estimate point edit field in create estimate flow --------- Co-authored-by: guru_sainath <gurusainath007@gmail.com> Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
This commit is contained in:
parent
23f4d5418f
commit
dae291fe3b
@ -784,6 +784,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def post(self, request, slug, project_id, cycle_id):
|
def post(self, request, slug, project_id, cycle_id):
|
||||||
new_cycle_id = request.data.get("new_cycle_id", False)
|
new_cycle_id = request.data.get("new_cycle_id", False)
|
||||||
|
plot_type = request.GET.get("plot_type", "issues")
|
||||||
|
|
||||||
if not new_cycle_id:
|
if not new_cycle_id:
|
||||||
return Response(
|
return Response(
|
||||||
@ -865,6 +866,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||||||
queryset=old_cycle.first(),
|
queryset=old_cycle.first(),
|
||||||
slug=slug,
|
slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
plot_type=plot_type,
|
||||||
cycle_id=cycle_id,
|
cycle_id=cycle_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -177,6 +177,8 @@ class ModuleSerializer(DynamicBaseSerializer):
|
|||||||
started_issues = serializers.IntegerField(read_only=True)
|
started_issues = serializers.IntegerField(read_only=True)
|
||||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||||
backlog_issues = serializers.IntegerField(read_only=True)
|
backlog_issues = serializers.IntegerField(read_only=True)
|
||||||
|
total_estimate_points = serializers.IntegerField(read_only=True)
|
||||||
|
completed_estimate_points = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Module
|
model = Module
|
||||||
@ -201,6 +203,8 @@ class ModuleSerializer(DynamicBaseSerializer):
|
|||||||
"external_id",
|
"external_id",
|
||||||
"logo_props",
|
"logo_props",
|
||||||
# computed fields
|
# computed fields
|
||||||
|
"total_estimate_points",
|
||||||
|
"completed_estimate_points",
|
||||||
"is_favorite",
|
"is_favorite",
|
||||||
"total_issues",
|
"total_issues",
|
||||||
"cancelled_issues",
|
"cancelled_issues",
|
||||||
|
@ -177,6 +177,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug, project_id, pk=None):
|
def get(self, request, slug, project_id, pk=None):
|
||||||
|
plot_type = request.GET.get("plot_type", "issues")
|
||||||
if pk is None:
|
if pk is None:
|
||||||
queryset = (
|
queryset = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
@ -375,6 +376,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
queryset=queryset,
|
queryset=queryset,
|
||||||
slug=slug,
|
slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
plot_type=plot_type,
|
||||||
cycle_id=pk,
|
cycle_id=pk,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,8 +17,11 @@ from django.db.models import (
|
|||||||
UUIDField,
|
UUIDField,
|
||||||
Value,
|
Value,
|
||||||
When,
|
When,
|
||||||
|
Subquery,
|
||||||
|
Sum,
|
||||||
|
IntegerField,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce, Cast
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
@ -73,6 +76,89 @@ class CycleViewSet(BaseViewSet):
|
|||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
)
|
)
|
||||||
|
backlog_estimate_point = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
estimate_point__estimate__type="points",
|
||||||
|
state__group="backlog",
|
||||||
|
issue_cycle__cycle_id=OuterRef("pk"),
|
||||||
|
)
|
||||||
|
.values("issue_cycle__cycle_id")
|
||||||
|
.annotate(
|
||||||
|
backlog_estimate_point=Sum(
|
||||||
|
Cast("estimate_point__value", IntegerField())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values("backlog_estimate_point")[:1]
|
||||||
|
)
|
||||||
|
unstarted_estimate_point = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
estimate_point__estimate__type="points",
|
||||||
|
state__group="unstarted",
|
||||||
|
issue_cycle__cycle_id=OuterRef("pk"),
|
||||||
|
)
|
||||||
|
.values("issue_cycle__cycle_id")
|
||||||
|
.annotate(
|
||||||
|
unstarted_estimate_point=Sum(
|
||||||
|
Cast("estimate_point__value", IntegerField())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values("unstarted_estimate_point")[:1]
|
||||||
|
)
|
||||||
|
started_estimate_point = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
estimate_point__estimate__type="points",
|
||||||
|
state__group="started",
|
||||||
|
issue_cycle__cycle_id=OuterRef("pk"),
|
||||||
|
)
|
||||||
|
.values("issue_cycle__cycle_id")
|
||||||
|
.annotate(
|
||||||
|
started_estimate_point=Sum(
|
||||||
|
Cast("estimate_point__value", IntegerField())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values("started_estimate_point")[:1]
|
||||||
|
)
|
||||||
|
cancelled_estimate_point = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
estimate_point__estimate__type="points",
|
||||||
|
state__group="cancelled",
|
||||||
|
issue_cycle__cycle_id=OuterRef("pk"),
|
||||||
|
)
|
||||||
|
.values("issue_cycle__cycle_id")
|
||||||
|
.annotate(
|
||||||
|
cancelled_estimate_point=Sum(
|
||||||
|
Cast("estimate_point__value", IntegerField())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values("cancelled_estimate_point")[:1]
|
||||||
|
)
|
||||||
|
completed_estimate_point = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
estimate_point__estimate__type="points",
|
||||||
|
state__group="completed",
|
||||||
|
issue_cycle__cycle_id=OuterRef("pk"),
|
||||||
|
)
|
||||||
|
.values("issue_cycle__cycle_id")
|
||||||
|
.annotate(
|
||||||
|
completed_estimate_points=Sum(
|
||||||
|
Cast("estimate_point__value", IntegerField())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values("completed_estimate_points")[:1]
|
||||||
|
)
|
||||||
|
total_estimate_point = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
estimate_point__estimate__type="points",
|
||||||
|
issue_cycle__cycle_id=OuterRef("pk"),
|
||||||
|
)
|
||||||
|
.values("issue_cycle__cycle_id")
|
||||||
|
.annotate(
|
||||||
|
total_estimate_points=Sum(
|
||||||
|
Cast("estimate_point__value", IntegerField())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values("total_estimate_points")[:1]
|
||||||
|
)
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
@ -197,12 +283,49 @@ class CycleViewSet(BaseViewSet):
|
|||||||
Value([], output_field=ArrayField(UUIDField())),
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
backlog_estimate_points=Coalesce(
|
||||||
|
Subquery(backlog_estimate_point),
|
||||||
|
Value(0, output_field=IntegerField()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
unstarted_estimate_points=Coalesce(
|
||||||
|
Subquery(unstarted_estimate_point),
|
||||||
|
Value(0, output_field=IntegerField()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
started_estimate_points=Coalesce(
|
||||||
|
Subquery(started_estimate_point),
|
||||||
|
Value(0, output_field=IntegerField()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
cancelled_estimate_points=Coalesce(
|
||||||
|
Subquery(cancelled_estimate_point),
|
||||||
|
Value(0, output_field=IntegerField()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_estimate_points=Coalesce(
|
||||||
|
Subquery(completed_estimate_point),
|
||||||
|
Value(0, output_field=IntegerField()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
total_estimate_points=Coalesce(
|
||||||
|
Subquery(total_estimate_point),
|
||||||
|
Value(0, output_field=IntegerField()),
|
||||||
|
),
|
||||||
|
)
|
||||||
.order_by("-is_favorite", "name")
|
.order_by("-is_favorite", "name")
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||||
|
plot_type = request.GET.get("plot_type", "issues")
|
||||||
cycle_view = request.GET.get("cycle_view", "all")
|
cycle_view = request.GET.get("cycle_view", "all")
|
||||||
|
|
||||||
# Update the order by
|
# Update the order by
|
||||||
@ -233,6 +356,12 @@ class CycleViewSet(BaseViewSet):
|
|||||||
"progress_snapshot",
|
"progress_snapshot",
|
||||||
"logo_props",
|
"logo_props",
|
||||||
# meta fields
|
# meta fields
|
||||||
|
"backlog_estimate_points",
|
||||||
|
"unstarted_estimate_points",
|
||||||
|
"started_estimate_points",
|
||||||
|
"cancelled_estimate_points",
|
||||||
|
"completed_estimate_points",
|
||||||
|
"total_estimate_points",
|
||||||
"is_favorite",
|
"is_favorite",
|
||||||
"total_issues",
|
"total_issues",
|
||||||
"cancelled_issues",
|
"cancelled_issues",
|
||||||
@ -335,6 +464,7 @@ class CycleViewSet(BaseViewSet):
|
|||||||
queryset=queryset.first(),
|
queryset=queryset.first(),
|
||||||
slug=slug,
|
slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
plot_type=plot_type,
|
||||||
cycle_id=data[0]["id"],
|
cycle_id=data[0]["id"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -359,6 +489,8 @@ class CycleViewSet(BaseViewSet):
|
|||||||
"progress_snapshot",
|
"progress_snapshot",
|
||||||
"logo_props",
|
"logo_props",
|
||||||
# meta fields
|
# meta fields
|
||||||
|
"completed_estimate_points",
|
||||||
|
"total_estimate_points",
|
||||||
"is_favorite",
|
"is_favorite",
|
||||||
"total_issues",
|
"total_issues",
|
||||||
"cancelled_issues",
|
"cancelled_issues",
|
||||||
@ -527,6 +659,7 @@ class CycleViewSet(BaseViewSet):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk):
|
def retrieve(self, request, slug, project_id, pk):
|
||||||
|
plot_type = request.GET.get("plot_type", "issues")
|
||||||
queryset = (
|
queryset = (
|
||||||
self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
|
self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
|
||||||
)
|
)
|
||||||
@ -682,6 +815,7 @@ class CycleViewSet(BaseViewSet):
|
|||||||
queryset=queryset,
|
queryset=queryset,
|
||||||
slug=slug,
|
slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
plot_type=plot_type,
|
||||||
cycle_id=pk,
|
cycle_id=pk,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -798,6 +932,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def post(self, request, slug, project_id, cycle_id):
|
def post(self, request, slug, project_id, cycle_id):
|
||||||
new_cycle_id = request.data.get("new_cycle_id", False)
|
new_cycle_id = request.data.get("new_cycle_id", False)
|
||||||
|
plot_type = request.GET.get("plot_type", "issues")
|
||||||
|
|
||||||
if not new_cycle_id:
|
if not new_cycle_id:
|
||||||
return Response(
|
return Response(
|
||||||
@ -879,6 +1014,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||||||
queryset=old_cycle.first(),
|
queryset=old_cycle.first(),
|
||||||
slug=slug,
|
slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
plot_type=plot_type,
|
||||||
cycle_id=cycle_id,
|
cycle_id=cycle_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -165,6 +165,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug, project_id, pk=None):
|
def get(self, request, slug, project_id, pk=None):
|
||||||
|
plot_type = request.GET.get("plot_type", "issues")
|
||||||
if pk is None:
|
if pk is None:
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
modules = queryset.values( # Required fields
|
modules = queryset.values( # Required fields
|
||||||
@ -323,6 +324,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
queryset=modules,
|
queryset=modules,
|
||||||
slug=slug,
|
slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
plot_type=plot_type,
|
||||||
module_id=pk,
|
module_id=pk,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,8 +16,9 @@ from django.db.models import (
|
|||||||
Subquery,
|
Subquery,
|
||||||
UUIDField,
|
UUIDField,
|
||||||
Value,
|
Value,
|
||||||
|
Sum,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce, Cast
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@ -128,6 +129,34 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
.annotate(cnt=Count("pk"))
|
.annotate(cnt=Count("pk"))
|
||||||
.values("cnt")
|
.values("cnt")
|
||||||
)
|
)
|
||||||
|
completed_estimate_point = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
estimate_point__estimate__type="points",
|
||||||
|
state__group="completed",
|
||||||
|
issue_module__module_id=OuterRef("pk"),
|
||||||
|
)
|
||||||
|
.values("issue_module__module_id")
|
||||||
|
.annotate(
|
||||||
|
completed_estimate_points=Sum(
|
||||||
|
Cast("estimate_point__value", IntegerField())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values("completed_estimate_points")[:1]
|
||||||
|
)
|
||||||
|
|
||||||
|
total_estimate_point = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
estimate_point__estimate__type="points",
|
||||||
|
issue_module__module_id=OuterRef("pk"),
|
||||||
|
)
|
||||||
|
.values("issue_module__module_id")
|
||||||
|
.annotate(
|
||||||
|
total_estimate_points=Sum(
|
||||||
|
Cast("estimate_point__value", IntegerField())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values("total_estimate_points")[:1]
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
@ -182,6 +211,18 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
Value(0, output_field=IntegerField()),
|
Value(0, output_field=IntegerField()),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_estimate_points=Coalesce(
|
||||||
|
Subquery(completed_estimate_point),
|
||||||
|
Value(0, output_field=IntegerField()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
total_estimate_points=Coalesce(
|
||||||
|
Subquery(total_estimate_point),
|
||||||
|
Value(0, output_field=IntegerField()),
|
||||||
|
),
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
member_ids=Coalesce(
|
member_ids=Coalesce(
|
||||||
ArrayAgg(
|
ArrayAgg(
|
||||||
@ -233,6 +274,8 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
"total_issues",
|
"total_issues",
|
||||||
"started_issues",
|
"started_issues",
|
||||||
"unstarted_issues",
|
"unstarted_issues",
|
||||||
|
"completed_estimate_points",
|
||||||
|
"total_estimate_points",
|
||||||
"backlog_issues",
|
"backlog_issues",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
@ -284,6 +327,8 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
"external_id",
|
"external_id",
|
||||||
"logo_props",
|
"logo_props",
|
||||||
# computed fields
|
# computed fields
|
||||||
|
"completed_estimate_points",
|
||||||
|
"total_estimate_points",
|
||||||
"total_issues",
|
"total_issues",
|
||||||
"is_favorite",
|
"is_favorite",
|
||||||
"cancelled_issues",
|
"cancelled_issues",
|
||||||
@ -301,6 +346,7 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
return Response(modules, status=status.HTTP_200_OK)
|
return Response(modules, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk):
|
def retrieve(self, request, slug, project_id, pk):
|
||||||
|
plot_type = request.GET.get("plot_type", "burndown")
|
||||||
queryset = (
|
queryset = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
.filter(archived_at__isnull=True)
|
.filter(archived_at__isnull=True)
|
||||||
@ -423,6 +469,7 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
queryset=modules,
|
queryset=modules,
|
||||||
slug=slug,
|
slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
plot_type=plot_type,
|
||||||
module_id=pk,
|
module_id=pk,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -469,6 +516,8 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
"external_id",
|
"external_id",
|
||||||
"logo_props",
|
"logo_props",
|
||||||
# computed fields
|
# computed fields
|
||||||
|
"completed_estimate_points",
|
||||||
|
"total_estimate_points",
|
||||||
"is_favorite",
|
"is_favorite",
|
||||||
"cancelled_issues",
|
"cancelled_issues",
|
||||||
"completed_issues",
|
"completed_issues",
|
||||||
|
@ -4,18 +4,28 @@ from itertools import groupby
|
|||||||
|
|
||||||
# Django import
|
# Django import
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Case, CharField, Count, F, Sum, Value, When
|
from django.db.models import (
|
||||||
|
Case,
|
||||||
|
CharField,
|
||||||
|
Count,
|
||||||
|
F,
|
||||||
|
Sum,
|
||||||
|
Value,
|
||||||
|
When,
|
||||||
|
IntegerField,
|
||||||
|
)
|
||||||
from django.db.models.functions import (
|
from django.db.models.functions import (
|
||||||
Coalesce,
|
Coalesce,
|
||||||
Concat,
|
Concat,
|
||||||
ExtractMonth,
|
ExtractMonth,
|
||||||
ExtractYear,
|
ExtractYear,
|
||||||
TruncDate,
|
TruncDate,
|
||||||
|
Cast,
|
||||||
)
|
)
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import Issue
|
from plane.db.models import Issue, Project
|
||||||
|
|
||||||
|
|
||||||
def annotate_with_monthly_dimension(queryset, field_name, attribute):
|
def annotate_with_monthly_dimension(queryset, field_name, attribute):
|
||||||
@ -87,9 +97,9 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
|||||||
|
|
||||||
# Estimate
|
# Estimate
|
||||||
else:
|
else:
|
||||||
queryset = queryset.annotate(estimate=Sum("estimate_point")).order_by(
|
queryset = queryset.annotate(
|
||||||
x_axis
|
estimate=Sum(Cast("estimate_point__value", IntegerField()))
|
||||||
)
|
).order_by(x_axis)
|
||||||
queryset = (
|
queryset = (
|
||||||
queryset.annotate(segment=F(segment)) if segment else queryset
|
queryset.annotate(segment=F(segment)) if segment else queryset
|
||||||
)
|
)
|
||||||
@ -110,9 +120,33 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
|||||||
return sort_data(grouped_data, temp_axis)
|
return sort_data(grouped_data, temp_axis)
|
||||||
|
|
||||||
|
|
||||||
def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
def burndown_plot(
|
||||||
|
queryset,
|
||||||
|
slug,
|
||||||
|
project_id,
|
||||||
|
plot_type,
|
||||||
|
cycle_id=None,
|
||||||
|
module_id=None,
|
||||||
|
):
|
||||||
# Total Issues in Cycle or Module
|
# Total Issues in Cycle or Module
|
||||||
total_issues = queryset.total_issues
|
total_issues = queryset.total_issues
|
||||||
|
# check whether the estimate is a point or not
|
||||||
|
estimate_type = Project.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
pk=project_id,
|
||||||
|
estimate__isnull=False,
|
||||||
|
estimate__type="points",
|
||||||
|
).exists()
|
||||||
|
if estimate_type and plot_type == "points":
|
||||||
|
issue_estimates = Issue.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_cycle__cycle_id=cycle_id,
|
||||||
|
estimate_point__isnull=False,
|
||||||
|
).values_list("estimate_point__value", flat=True)
|
||||||
|
|
||||||
|
issue_estimates = [int(value) for value in issue_estimates]
|
||||||
|
total_estimate_points = sum(issue_estimates)
|
||||||
|
|
||||||
if cycle_id:
|
if cycle_id:
|
||||||
if queryset.end_date and queryset.start_date:
|
if queryset.end_date and queryset.start_date:
|
||||||
@ -128,6 +162,20 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
|||||||
|
|
||||||
chart_data = {str(date): 0 for date in date_range}
|
chart_data = {str(date): 0 for date in date_range}
|
||||||
|
|
||||||
|
if plot_type == "points":
|
||||||
|
completed_issues_estimate_point_distribution = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_cycle__cycle_id=cycle_id,
|
||||||
|
estimate_point__isnull=False,
|
||||||
|
)
|
||||||
|
.annotate(date=TruncDate("completed_at"))
|
||||||
|
.values("date")
|
||||||
|
.values("date", "estimate_point__value")
|
||||||
|
.order_by("date")
|
||||||
|
)
|
||||||
|
else:
|
||||||
completed_issues_distribution = (
|
completed_issues_distribution = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
@ -152,6 +200,20 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
|||||||
|
|
||||||
chart_data = {str(date): 0 for date in date_range}
|
chart_data = {str(date): 0 for date in date_range}
|
||||||
|
|
||||||
|
if plot_type == "points":
|
||||||
|
completed_issues_estimate_point_distribution = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_module__module_id=module_id,
|
||||||
|
estimate_point__isnull=False,
|
||||||
|
)
|
||||||
|
.annotate(date=TruncDate("completed_at"))
|
||||||
|
.values("date")
|
||||||
|
.values("date", "estimate_point__value")
|
||||||
|
.order_by("date")
|
||||||
|
)
|
||||||
|
else:
|
||||||
completed_issues_distribution = (
|
completed_issues_distribution = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
@ -166,6 +228,20 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for date in date_range:
|
for date in date_range:
|
||||||
|
if plot_type == "points":
|
||||||
|
cumulative_pending_issues = total_estimate_points
|
||||||
|
total_completed = 0
|
||||||
|
total_completed = sum(
|
||||||
|
int(item["estimate_point__value"])
|
||||||
|
for item in completed_issues_estimate_point_distribution
|
||||||
|
if item["date"] is not None and item["date"] <= date
|
||||||
|
)
|
||||||
|
cumulative_pending_issues -= total_completed
|
||||||
|
if date > timezone.now().date():
|
||||||
|
chart_data[str(date)] = None
|
||||||
|
else:
|
||||||
|
chart_data[str(date)] = cumulative_pending_issues
|
||||||
|
else:
|
||||||
cumulative_pending_issues = total_issues
|
cumulative_pending_issues = total_issues
|
||||||
total_completed = 0
|
total_completed = 0
|
||||||
total_completed = sum(
|
total_completed = sum(
|
||||||
|
2
packages/types/src/module/modules.d.ts
vendored
2
packages/types/src/module/modules.d.ts
vendored
@ -44,6 +44,8 @@ export interface IModule {
|
|||||||
target_date: string | null;
|
target_date: string | null;
|
||||||
total_issues: number;
|
total_issues: number;
|
||||||
unstarted_issues: number;
|
unstarted_issues: number;
|
||||||
|
total_estimate_points?: number;
|
||||||
|
completed_estimate_points?: number;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
updated_by?: string;
|
updated_by?: string;
|
||||||
archived_at: string | null;
|
archived_at: string | null;
|
||||||
|
@ -1,26 +1,54 @@
|
|||||||
// ui
|
import { observer } from "mobx-react";
|
||||||
import { TYAxisValues } from "@plane/types";
|
import { TYAxisValues } from "@plane/types";
|
||||||
import { CustomSelect } from "@plane/ui";
|
import { CustomSelect } from "@plane/ui";
|
||||||
// types
|
|
||||||
import { ANALYTICS_Y_AXIS_VALUES } from "@/constants/analytics";
|
|
||||||
// constants
|
// constants
|
||||||
|
import { ANALYTICS_Y_AXIS_VALUES } from "@/constants/analytics";
|
||||||
|
import { EEstimateSystem } from "@/constants/estimates";
|
||||||
|
// hooks
|
||||||
|
import { useAppRouter, useProjectEstimates } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: TYAxisValues;
|
value: TYAxisValues;
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectYAxis: React.FC<Props> = ({ value, onChange }) => (
|
export const SelectYAxis: React.FC<Props> = observer(({ value, onChange }) => {
|
||||||
|
// hooks
|
||||||
|
const { projectId } = useAppRouter();
|
||||||
|
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
|
||||||
|
|
||||||
|
const isEstimateEnabled = (analyticsOption: string) => {
|
||||||
|
if (analyticsOption === "estimate") {
|
||||||
|
if (
|
||||||
|
projectId &&
|
||||||
|
currentActiveEstimateId &&
|
||||||
|
areEstimateEnabledByProjectId(projectId) &&
|
||||||
|
estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={value}
|
value={value}
|
||||||
label={<span>{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "None"}</span>}
|
label={<span>{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "None"}</span>}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
maxHeight="lg"
|
maxHeight="lg"
|
||||||
>
|
>
|
||||||
{ANALYTICS_Y_AXIS_VALUES.map((item) => (
|
{ANALYTICS_Y_AXIS_VALUES.map(
|
||||||
|
(item) =>
|
||||||
|
isEstimateEnabled(item.value) && (
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
<CustomSelect.Option key={item.value} value={item.value}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</CustomSelect.Option>
|
</CustomSelect.Option>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
</CustomSelect>
|
</CustomSelect>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, FormEvent, useState } from "react";
|
import { FC, MouseEvent, FocusEvent, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Check, Info, X } from "lucide-react";
|
import { Check, Info, X } from "lucide-react";
|
||||||
import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types";
|
import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types";
|
||||||
@ -49,7 +49,7 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
|
|||||||
closeCallBack();
|
closeCallBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = async (event: FormEvent<HTMLFormElement>) => {
|
const handleCreate = async (event: MouseEvent<HTMLButtonElement> | FocusEvent<HTMLInputElement, Element>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
@ -122,10 +122,10 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleCreate} className="relative flex items-center gap-2 text-base">
|
<form className="relative flex items-center gap-2 text-base">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative w-full border rounded flex items-center",
|
"relative w-full border rounded flex items-center my-1",
|
||||||
error ? `border-red-500` : `border-custom-border-200`
|
error ? `border-red-500` : `border-custom-border-200`
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -136,6 +136,7 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
|
|||||||
className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent"
|
className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent"
|
||||||
placeholder="Enter estimate point"
|
placeholder="Enter estimate point"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
onBlur={(e) => !estimateId && handleCreate(e)}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
<>
|
<>
|
||||||
@ -148,10 +149,13 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{estimateId && (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer text-green-500"
|
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer text-green-500"
|
||||||
disabled={loader}
|
disabled={loader}
|
||||||
|
onClick={handleCreate}
|
||||||
>
|
>
|
||||||
{loader ? <Spinner className="w-4 h-4" /> : <Check size={14} />}
|
{loader ? <Spinner className="w-4 h-4" /> : <Check size={14} />}
|
||||||
</button>
|
</button>
|
||||||
@ -162,6 +166,8 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
|
|||||||
>
|
>
|
||||||
<X size={14} className="text-custom-text-200" />
|
<X size={14} className="text-custom-text-200" />
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, FormEvent, useEffect, useState } from "react";
|
import { FC, MouseEvent, useEffect, FocusEvent, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Check, Info, X } from "lucide-react";
|
import { Check, Info, X } from "lucide-react";
|
||||||
import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types";
|
import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types";
|
||||||
@ -57,7 +57,7 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
|
|||||||
closeCallBack();
|
closeCallBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdate = async (event: FormEvent<HTMLFormElement>) => {
|
const handleUpdate = async (event: MouseEvent<HTMLButtonElement> | FocusEvent<HTMLInputElement, Element>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
@ -134,10 +134,10 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleUpdate} className="relative flex items-center gap-2 text-base">
|
<form className="relative flex items-center gap-2 text-base">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative w-full border rounded flex items-center",
|
"relative w-full border rounded flex items-center my-1",
|
||||||
error ? `border-red-500` : `border-custom-border-200`
|
error ? `border-red-500` : `border-custom-border-200`
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -147,6 +147,7 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
|
|||||||
onChange={(e) => setEstimateInputValue(e.target.value)}
|
onChange={(e) => setEstimateInputValue(e.target.value)}
|
||||||
className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent"
|
className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent"
|
||||||
placeholder="Enter estimate point"
|
placeholder="Enter estimate point"
|
||||||
|
onBlur={(e) => !estimateId && handleUpdate(e)}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
@ -159,11 +160,13 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{estimateId && (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer text-green-500"
|
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer text-green-500"
|
||||||
disabled={loader}
|
disabled={loader}
|
||||||
|
onClick={handleUpdate}
|
||||||
>
|
>
|
||||||
{loader ? <Spinner className="w-4 h-4" /> : <Check size={14} />}
|
{loader ? <Spinner className="w-4 h-4" /> : <Check size={14} />}
|
||||||
</button>
|
</button>
|
||||||
@ -174,6 +177,8 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
|
|||||||
>
|
>
|
||||||
<X size={14} className="text-custom-text-200" />
|
<X size={14} className="text-custom-text-200" />
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -10,13 +10,14 @@ import { FavoriteStar } from "@/components/core";
|
|||||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||||
import { ModuleQuickActions } from "@/components/modules";
|
import { ModuleQuickActions } from "@/components/modules";
|
||||||
// constants
|
// constants
|
||||||
|
import { EEstimateSystem } from "@/constants/estimates";
|
||||||
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker";
|
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker";
|
||||||
import { MODULE_STATUS } from "@/constants/module";
|
import { MODULE_STATUS } from "@/constants/module";
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
// helpers
|
// helpers
|
||||||
import { getDate, renderFormattedDate } from "@/helpers/date-time.helper";
|
import { getDate, renderFormattedDate } from "@/helpers/date-time.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useMember, useModule, useUser } from "@/hooks/store";
|
import { useEventTracker, useMember, useModule, useProjectEstimates, useUser } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -37,6 +38,8 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
|
const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates();
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
@ -120,14 +123,30 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
if (!moduleDetails) return null;
|
if (!moduleDetails) return null;
|
||||||
|
|
||||||
const moduleTotalIssues =
|
/**
|
||||||
moduleDetails.backlog_issues +
|
* NOTE: This completion percentage calculation is based on the total issues count.
|
||||||
|
* when estimates are available and estimate type is points, we should consider the estimate point count
|
||||||
|
* when estimates are available and estimate type is not points, then by default we consider the issue count
|
||||||
|
*/
|
||||||
|
const isEstimateEnabled =
|
||||||
|
projectId &&
|
||||||
|
currentActiveEstimateId &&
|
||||||
|
areEstimateEnabledByProjectId(projectId.toString()) &&
|
||||||
|
estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS;
|
||||||
|
|
||||||
|
const moduleTotalIssues = isEstimateEnabled
|
||||||
|
? moduleDetails?.total_estimate_points || 0
|
||||||
|
: moduleDetails.backlog_issues +
|
||||||
moduleDetails.unstarted_issues +
|
moduleDetails.unstarted_issues +
|
||||||
moduleDetails.started_issues +
|
moduleDetails.started_issues +
|
||||||
moduleDetails.completed_issues +
|
moduleDetails.completed_issues +
|
||||||
moduleDetails.cancelled_issues;
|
moduleDetails.cancelled_issues;
|
||||||
|
|
||||||
const completionPercentage = (moduleDetails.completed_issues / moduleTotalIssues) * 100;
|
const moduleCompletedIssues = isEstimateEnabled
|
||||||
|
? moduleDetails?.completed_estimate_points || 0
|
||||||
|
: moduleDetails.completed_issues;
|
||||||
|
|
||||||
|
const completionPercentage = (moduleCompletedIssues / moduleTotalIssues) * 100;
|
||||||
|
|
||||||
const endDate = getDate(moduleDetails.target_date);
|
const endDate = getDate(moduleDetails.target_date);
|
||||||
const startDate = getDate(moduleDetails.start_date);
|
const startDate = getDate(moduleDetails.start_date);
|
||||||
@ -140,11 +159,11 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const issueCount = module
|
const issueCount = module
|
||||||
? !moduleTotalIssues || moduleTotalIssues === 0
|
? !moduleTotalIssues || moduleTotalIssues === 0
|
||||||
? "0 Issue"
|
? `0 ${isEstimateEnabled ? `Point` : `Issue`}`
|
||||||
: moduleTotalIssues === moduleDetails.completed_issues
|
: moduleTotalIssues === moduleCompletedIssues
|
||||||
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}`
|
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? `s` : ``}`
|
||||||
: `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues`
|
: `${moduleCompletedIssues}/${moduleTotalIssues} ${isEstimateEnabled ? `Points` : `Issues`}`
|
||||||
: "0 Issue";
|
: `0 ${isEstimateEnabled ? `Point` : `Issue`}`;
|
||||||
|
|
||||||
const moduleLeadDetails = moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined;
|
const moduleLeadDetails = moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined;
|
||||||
|
|
||||||
|
@ -8,8 +8,10 @@ import { CircularProgressIndicator } from "@plane/ui";
|
|||||||
// components
|
// components
|
||||||
import { ListItem } from "@/components/core/list";
|
import { ListItem } from "@/components/core/list";
|
||||||
import { ModuleListItemAction } from "@/components/modules";
|
import { ModuleListItemAction } from "@/components/modules";
|
||||||
|
// constants
|
||||||
|
import { EEstimateSystem } from "@/constants/estimates";
|
||||||
// hooks
|
// hooks
|
||||||
import { useModule } from "@/hooks/store";
|
import { useAppRouter, useModule, useProjectEstimates } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -26,14 +28,28 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
// store hooks
|
// store hooks
|
||||||
const { getModuleById } = useModule();
|
const { getModuleById } = useModule();
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
const { projectId } = useAppRouter();
|
||||||
|
const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates();
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const moduleDetails = getModuleById(moduleId);
|
const moduleDetails = getModuleById(moduleId);
|
||||||
|
|
||||||
if (!moduleDetails) return null;
|
if (!moduleDetails) return null;
|
||||||
|
|
||||||
const completionPercentage =
|
/**
|
||||||
((moduleDetails.completed_issues + moduleDetails.cancelled_issues) / moduleDetails.total_issues) * 100;
|
* NOTE: This completion percentage calculation is based on the total issues count.
|
||||||
|
* when estimates are available and estimate type is points, we should consider the estimate point count
|
||||||
|
* when estimates are available and estimate type is not points, then by default we consider the issue count
|
||||||
|
*/
|
||||||
|
const isEstimateEnabled =
|
||||||
|
projectId &&
|
||||||
|
currentActiveEstimateId &&
|
||||||
|
areEstimateEnabledByProjectId(projectId) &&
|
||||||
|
estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS;
|
||||||
|
|
||||||
|
const completionPercentage = isEstimateEnabled
|
||||||
|
? ((moduleDetails?.completed_estimate_points || 0) / (moduleDetails?.total_estimate_points || 0)) * 100
|
||||||
|
: ((moduleDetails.completed_issues + moduleDetails.cancelled_issues) / moduleDetails.total_issues) * 100;
|
||||||
|
|
||||||
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
|
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user