From 591adf7dbc139dcaf63119fce9e95294a53b6506 Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Sat, 31 Jan 2026 01:12:12 +0530 Subject: [PATCH 1/6] fixed day left incorrect data --- .../modules/[moduleKey]/issues/[issueId]/page.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index 067dfc0da2..05d5f8b3ad 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -27,18 +27,20 @@ const ModuleIssueDetailsPage = () => { const formatDeadline = (deadline: string | null) => { if (!deadline) return { text: 'No deadline set', color: 'text-gray-600 dark:text-gray-300' } + const now = new Date() + + const todayUTC = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) + const deadlineDate = new Date(deadline) - const today = new Date() - const deadlineUTC = new Date( + const deadlineUTC = Date.UTC( deadlineDate.getUTCFullYear(), deadlineDate.getUTCMonth(), deadlineDate.getUTCDate() ) - const todayUTC = new Date(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()) - const isOverdue = deadlineUTC < todayUTC - const daysLeft = Math.ceil((deadlineUTC.getTime() - todayUTC.getTime()) / (1000 * 60 * 60 * 24)) + const daysLeft = Math.ceil((deadlineUTC - todayUTC) / (1000 * 60 * 60 * 24)) + const isOverdue = daysLeft < 0 let statusText: string if (isOverdue) { From b39aaabce174725569f5c98bc8370f313849fb99 Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Sat, 31 Jan 2026 13:23:12 +0530 Subject: [PATCH 2/6] added deadline cols --- .../apps/github/api/internal/nodes/issue.py | 31 +++++++++ .../mentorship/api/internal/nodes/module.py | 11 +++- .../modules/[moduleKey]/issues/page.tsx | 2 + frontend/src/components/IssuesTable.tsx | 64 ++++++++++++++++++- frontend/src/server/queries/moduleQueries.ts | 1 + frontend/src/types/__generated__/graphql.ts | 1 + .../__generated__/moduleQueries.generated.ts | 4 +- 7 files changed, 109 insertions(+), 5 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index 0312c25cdf..a5b5488f3c 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -1,7 +1,10 @@ """GitHub issue GraphQL node.""" +from datetime import datetime + import strawberry import strawberry_django +from strawberry.types import Info from apps.github.api.internal.nodes.pull_request import PullRequestNode from apps.github.api.internal.nodes.user import UserNode @@ -59,3 +62,31 @@ def interested_users(self, root: Issue) -> list[UserNode]: "user__login" ) ] + + @strawberry.field + def task_deadline(self, root: Issue, info: Info) -> datetime | None: + """Return the deadline for the latest assigned task linked to this issue. + + Reads the current module from GraphQL context and queries the Task model + to find the most recent deadline. Returns None if no module is in context + or if no deadline exists for this issue. + """ + # Import here to avoid circular dependency + from apps.mentorship.models.task import Task + + # Get module from context (injected by ModuleNode resolvers) + module = getattr(info.context, "current_module", None) + if not module: + return None + + # Query Task table for deadline + return ( + Task.objects.filter( + module=module, + issue__number=root.number, + deadline_at__isnull=False, + ) + .order_by("-assigned_at") + .values_list("deadline_at", flat=True) + .first() + ) diff --git a/backend/apps/mentorship/api/internal/nodes/module.py b/backend/apps/mentorship/api/internal/nodes/module.py index 0a224089d3..eb7fd6a43b 100644 --- a/backend/apps/mentorship/api/internal/nodes/module.py +++ b/backend/apps/mentorship/api/internal/nodes/module.py @@ -3,6 +3,7 @@ from datetime import datetime import strawberry +from strawberry.types import Info from apps.github.api.internal.nodes.issue import IssueNode from apps.github.api.internal.nodes.pull_request import PullRequestNode @@ -76,9 +77,12 @@ def project_name(self) -> str | None: @strawberry.field def issues( - self, limit: int = 20, offset: int = 0, label: str | None = None + self, info: Info, limit: int = 20, offset: int = 0, label: str | None = None ) -> list[IssueNode]: """Return paginated issues linked to this module, optionally filtered by label.""" + # Inject current module into context for IssueNode.task_deadline + info.context.current_module = self + queryset = self.issues.select_related("repository", "author").prefetch_related( "assignees", "labels" ) @@ -110,8 +114,11 @@ def available_labels(self) -> list[str]: return sorted(label_names) @strawberry.field - def issue_by_number(self, number: int) -> IssueNode | None: + def issue_by_number(self, info: Info, number: int) -> IssueNode | None: """Return a single issue by its GitHub number within this module's linked issues.""" + # Inject current module into context for IssueNode.task_deadline + info.context.current_module = self + return ( self.issues.select_related("repository", "author") .prefetch_related("assignees", "labels") diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index 21934208c5..2982b06d73 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -47,6 +47,7 @@ const IssuesPage = () => { isMerged: i.isMerged, labels: i.labels || [], assignees: i.assignees || [], + deadline: i.taskDeadline ?? null, })) }, [moduleData]) @@ -134,6 +135,7 @@ const IssuesPage = () => { diff --git a/frontend/src/components/IssuesTable.tsx b/frontend/src/components/IssuesTable.tsx index d0c2f53d97..4a3b12cd03 100644 --- a/frontend/src/components/IssuesTable.tsx +++ b/frontend/src/components/IssuesTable.tsx @@ -20,6 +20,7 @@ export type IssueRow = { interface IssuesTableProps { issues: IssueRow[] showAssignee?: boolean + showDeadline?: boolean onIssueClick?: (issueNumber: number) => void issueUrl?: (issueNumber: number) => string maxVisibleLabels?: number @@ -31,6 +32,7 @@ const MAX_VISIBLE_LABELS = 5 const IssuesTable: React.FC = ({ issues, showAssignee = true, + showDeadline = false, onIssueClick, issueUrl, maxVisibleLabels = MAX_VISIBLE_LABELS, @@ -66,11 +68,47 @@ const IssuesTable: React.FC = ({ } const getColumnCount = () => { - let count = 3 + let count = 3 // Title, Status, Labels if (showAssignee) count++ + if (showDeadline) count++ return count } + const getDeadlineStatus = (deadline?: string | null) => { + if (!deadline) + return { + text: 'No Deadline', + class: 'border border-dashed border-gray-500 text-gray-500 bg-transparent', + } + + const now = new Date() + const deadlineDate = new Date(deadline) + const nowStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + const deadlineStart = new Date( + deadlineDate.getFullYear(), + deadlineDate.getMonth(), + deadlineDate.getDate() + ) + const diffDays = Math.round((deadlineStart.getTime() - nowStart.getTime()) / 86400000) + + if (diffDays < 0) + return { + text: 'Overdue', + class: 'bg-red-500/15 text-red-400 border border-red-500/30', + } + + if (diffDays <= 7) + return { + text: 'Due Soon', + class: 'bg-amber-500/15 text-amber-400 border border-amber-500/30', + } + + return { + text: 'Upcoming', + class: 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20', + } + } + return (
@@ -102,6 +140,14 @@ const IssuesTable: React.FC = ({ Assignee )} + {showDeadline && ( + + )} @@ -180,6 +226,22 @@ const IssuesTable: React.FC = ({ ) : null} )} + + {/* Deadline */} + {showDeadline && ( + + )} ))} {issues.length === 0 && ( diff --git a/frontend/src/server/queries/moduleQueries.ts b/frontend/src/server/queries/moduleQueries.ts index a21f879a6b..a1c884afa3 100644 --- a/frontend/src/server/queries/moduleQueries.ts +++ b/frontend/src/server/queries/moduleQueries.ts @@ -132,6 +132,7 @@ export const GET_MODULE_ISSUES = gql` state isMerged labels + taskDeadline assignees { avatarUrl login diff --git a/frontend/src/types/__generated__/graphql.ts b/frontend/src/types/__generated__/graphql.ts index c1eb07d419..67b4af003d 100644 --- a/frontend/src/types/__generated__/graphql.ts +++ b/frontend/src/types/__generated__/graphql.ts @@ -227,6 +227,7 @@ export type IssueNode = Node & { pullRequests: Array; repositoryName?: Maybe; state: Scalars['String']['output']; + taskDeadline?: Maybe; title: Scalars['String']['output']; url: Scalars['String']['output']; }; diff --git a/frontend/src/types/__generated__/moduleQueries.generated.ts b/frontend/src/types/__generated__/moduleQueries.generated.ts index 9fd6cf5802..1671d7a025 100644 --- a/frontend/src/types/__generated__/moduleQueries.generated.ts +++ b/frontend/src/types/__generated__/moduleQueries.generated.ts @@ -33,10 +33,10 @@ export type GetModuleIssuesQueryVariables = Types.Exact<{ }>; -export type GetModuleIssuesQuery = { getModule: { __typename: 'ModuleNode', name: string, issuesCount: number, availableLabels: Array, issues: Array<{ __typename: 'IssueNode', id: string, number: number, title: string, state: string, isMerged: boolean, labels: Array, assignees: Array<{ __typename: 'UserNode', avatarUrl: string, login: string, name: string }> }> } | null }; +export type GetModuleIssuesQuery = { getModule: { __typename: 'ModuleNode', name: string, issuesCount: number, availableLabels: Array, issues: Array<{ __typename: 'IssueNode', id: string, number: number, title: string, state: string, isMerged: boolean, labels: Array, taskDeadline: any | null, assignees: Array<{ __typename: 'UserNode', avatarUrl: string, login: string, name: string }> }> } | null }; export const GetModulesByProgramDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModulesByProgram"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getProgramModules"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"projectName"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mentees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetModuleByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleByID"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mentees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetProgramAdminsAndModulesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProgramAdminsAndModules"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getProgram"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"admins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"projectName"}},{"kind":"Field","name":{"kind":"Name","value":"domains"}},{"kind":"Field","name":{"kind":"Name","value":"experienceLevel"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"mentors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mentees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPullRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"mergedAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const GetModuleIssuesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleIssues"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"20"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"label"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"label"},"value":{"kind":"Variable","name":{"kind":"Name","value":"label"}}}]},{"kind":"Field","name":{"kind":"Name","value":"availableLabels"}},{"kind":"Field","name":{"kind":"Name","value":"issues"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}},{"kind":"Argument","name":{"kind":"Name","value":"label"},"value":{"kind":"Variable","name":{"kind":"Name","value":"label"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"isMerged"}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"assignees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const GetModuleIssuesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModuleIssues"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"20"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"label"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getModule"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"moduleKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"moduleKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"programKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"programKey"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"label"},"value":{"kind":"Variable","name":{"kind":"Name","value":"label"}}}]},{"kind":"Field","name":{"kind":"Name","value":"availableLabels"}},{"kind":"Field","name":{"kind":"Name","value":"issues"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}},{"kind":"Argument","name":{"kind":"Name","value":"label"},"value":{"kind":"Variable","name":{"kind":"Name","value":"label"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"isMerged"}},{"kind":"Field","name":{"kind":"Name","value":"labels"}},{"kind":"Field","name":{"kind":"Name","value":"taskDeadline"}},{"kind":"Field","name":{"kind":"Name","value":"assignees"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file From e62dbb5e030f2dbb96be699620017ec11e1c44d9 Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Sat, 31 Jan 2026 17:02:27 +0530 Subject: [PATCH 3/6] added filtering option --- .../apps/github/api/internal/nodes/issue.py | 6 +- .../mentorship/api/internal/nodes/module.py | 6 +- .../github/api/internal/nodes/issue_test.py | 1 + .../modules/[moduleKey]/issues/page.tsx | 143 ++++++++++++++---- frontend/src/components/IssuesTable.tsx | 2 +- 5 files changed, 118 insertions(+), 40 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index a5b5488f3c..74a2854e0f 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -66,19 +66,19 @@ def interested_users(self, root: Issue) -> list[UserNode]: @strawberry.field def task_deadline(self, root: Issue, info: Info) -> datetime | None: """Return the deadline for the latest assigned task linked to this issue. - + Reads the current module from GraphQL context and queries the Task model to find the most recent deadline. Returns None if no module is in context or if no deadline exists for this issue. """ # Import here to avoid circular dependency from apps.mentorship.models.task import Task - + # Get module from context (injected by ModuleNode resolvers) module = getattr(info.context, "current_module", None) if not module: return None - + # Query Task table for deadline return ( Task.objects.filter( diff --git a/backend/apps/mentorship/api/internal/nodes/module.py b/backend/apps/mentorship/api/internal/nodes/module.py index eb7fd6a43b..c0a63b03ca 100644 --- a/backend/apps/mentorship/api/internal/nodes/module.py +++ b/backend/apps/mentorship/api/internal/nodes/module.py @@ -80,9 +80,8 @@ def issues( self, info: Info, limit: int = 20, offset: int = 0, label: str | None = None ) -> list[IssueNode]: """Return paginated issues linked to this module, optionally filtered by label.""" - # Inject current module into context for IssueNode.task_deadline info.context.current_module = self - + queryset = self.issues.select_related("repository", "author").prefetch_related( "assignees", "labels" ) @@ -116,9 +115,8 @@ def available_labels(self) -> list[str]: @strawberry.field def issue_by_number(self, info: Info, number: int) -> IssueNode | None: """Return a single issue by its GitHub number within this module's linked issues.""" - # Inject current module into context for IssueNode.task_deadline info.context.current_module = self - + return ( self.issues.select_related("repository", "author") .prefetch_related("assignees", "labels") diff --git a/backend/tests/apps/github/api/internal/nodes/issue_test.py b/backend/tests/apps/github/api/internal/nodes/issue_test.py index 54a6f61f62..bb243e5d81 100644 --- a/backend/tests/apps/github/api/internal/nodes/issue_test.py +++ b/backend/tests/apps/github/api/internal/nodes/issue_test.py @@ -28,6 +28,7 @@ def test_issue_node_fields(self): "pull_requests", "repository_name", "state", + "task_deadline", "title", "url", } diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index 2982b06d73..eb99bf71ea 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -12,12 +12,41 @@ import Pagination from 'components/Pagination' const ITEMS_PER_PAGE = 20 const LABEL_ALL = 'all' +const DEADLINE_ALL = 'all' +const DEADLINE_OPTIONS = [ + { key: 'all', label: 'All' }, + { key: 'overdue', label: 'Overdue' }, + { key: 'due-soon', label: 'Due Soon' }, + { key: 'upcoming', label: 'Upcoming' }, + { key: 'no-deadline', label: 'No Deadline' }, +] + +const getDeadlineCategory = (deadline?: string | null): string => { + if (!deadline) return 'no-deadline' + + const now = new Date() + const deadlineDate = new Date(deadline) + const nowStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + const deadlineStart = new Date( + deadlineDate.getFullYear(), + deadlineDate.getMonth(), + deadlineDate.getDate() + ) + const diffDays = Math.round((deadlineStart.getTime() - nowStart.getTime()) / 86400000) + + if (diffDays < 0) return 'overdue' + if (diffDays <= 7) return 'due-soon' + return 'upcoming' +} const IssuesPage = () => { const { programKey, moduleKey } = useParams<{ programKey: string; moduleKey: string }>() const router = useRouter() const searchParams = useSearchParams() const [selectedLabel, setSelectedLabel] = useState(searchParams.get('label') || LABEL_ALL) + const [selectedDeadline, setSelectedDeadline] = useState( + searchParams.get('deadline') || DEADLINE_ALL + ) const [currentPage, setCurrentPage] = useState(1) const { data, loading, error } = useQuery(GetModuleIssuesDocument, { @@ -39,7 +68,7 @@ const IssuesPage = () => { const moduleData = data?.getModule const moduleIssues: IssueRow[] = useMemo(() => { - return (moduleData?.issues || []).map((i) => ({ + const issues = (moduleData?.issues || []).map((i) => ({ objectID: i.id, number: i.number, title: i.title, @@ -49,7 +78,13 @@ const IssuesPage = () => { assignees: i.assignees || [], deadline: i.taskDeadline ?? null, })) - }, [moduleData]) + + if (selectedDeadline !== DEADLINE_ALL) { + return issues.filter((issue) => getDeadlineCategory(issue.deadline) === selectedDeadline) + } + + return issues + }, [moduleData, selectedDeadline]) const totalPages = Math.ceil((moduleData?.issuesCount || 0) / ITEMS_PER_PAGE) @@ -78,6 +113,18 @@ const IssuesPage = () => { router.replace(`?${params.toString()}`) } + const handleDeadlineChange = (deadline: string) => { + setSelectedDeadline(deadline) + setCurrentPage(1) + const params = new URLSearchParams(searchParams.toString()) + if (deadline === DEADLINE_ALL) { + params.delete('deadline') + } else { + params.set('deadline', deadline) + } + router.replace(`?${params.toString()}`) + } + const handlePageChange = (page: number) => { setCurrentPage(page) } @@ -98,37 +145,69 @@ const IssuesPage = () => { return (
-
+

{moduleData.name} Issues

-
- +
+
+ +
+
+ +
diff --git a/frontend/src/components/IssuesTable.tsx b/frontend/src/components/IssuesTable.tsx index 4a3b12cd03..2d08aad586 100644 --- a/frontend/src/components/IssuesTable.tsx +++ b/frontend/src/components/IssuesTable.tsx @@ -229,7 +229,7 @@ const IssuesTable: React.FC = ({ {/* Deadline */} {showDeadline && ( -
- )} + + ) + })()} ))} {issues.length === 0 && (
+ Deadline +
+ {(() => { + const status = getDeadlineStatus(issue.deadline) + return ( + + {status.text} + + ) + })()} +
+ {(() => { const status = getDeadlineStatus(issue.deadline) return ( From 9786093caad2e28581ee8d4e2be2369ab4da40e1 Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Sat, 31 Jan 2026 23:44:01 +0530 Subject: [PATCH 4/6] fixed coderabbit review --- .../apps/github/api/internal/nodes/issue.py | 28 ++----------- .../mentorship/api/internal/nodes/module.py | 34 +++++++++++++-- .../modules/[moduleKey]/issues/page.tsx | 42 ++++++++++++------- frontend/src/components/IssuesTable.tsx | 9 +--- 4 files changed, 64 insertions(+), 49 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index 74a2854e0f..5217bae885 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -65,28 +65,8 @@ def interested_users(self, root: Issue) -> list[UserNode]: @strawberry.field def task_deadline(self, root: Issue, info: Info) -> datetime | None: - """Return the deadline for the latest assigned task linked to this issue. - - Reads the current module from GraphQL context and queries the Task model - to find the most recent deadline. Returns None if no module is in context - or if no deadline exists for this issue. - """ - # Import here to avoid circular dependency - from apps.mentorship.models.task import Task - - # Get module from context (injected by ModuleNode resolvers) - module = getattr(info.context, "current_module", None) - if not module: + """Return the deadline for the latest assigned task linked to this issue.""" + mapping = getattr(info.context, "task_deadlines_by_issue", None) + if mapping is None: return None - - # Query Task table for deadline - return ( - Task.objects.filter( - module=module, - issue__number=root.number, - deadline_at__isnull=False, - ) - .order_by("-assigned_at") - .values_list("deadline_at", flat=True) - .first() - ) + return mapping.get(root.number) diff --git a/backend/apps/mentorship/api/internal/nodes/module.py b/backend/apps/mentorship/api/internal/nodes/module.py index c0a63b03ca..c220dba52f 100644 --- a/backend/apps/mentorship/api/internal/nodes/module.py +++ b/backend/apps/mentorship/api/internal/nodes/module.py @@ -82,6 +82,25 @@ def issues( """Return paginated issues linked to this module, optionally filtered by label.""" info.context.current_module = self + # BULK load data + task_rows = ( + Task.objects.filter(module=self, deadline_at__isnull=False) + .order_by("issue__number", "-assigned_at") + .values("issue__number", "deadline_at", "assigned_at") + ) + + deadline_map = {} + assigned_map = {} + + for row in task_rows: + num = row["issue__number"] + if num not in deadline_map: + deadline_map[num] = row["deadline_at"] + assigned_map[num] = row["assigned_at"] + + info.context.task_deadlines_by_issue = deadline_map + info.context.task_assigned_at_by_issue = assigned_map + queryset = self.issues.select_related("repository", "author").prefetch_related( "assignees", "labels" ) @@ -138,8 +157,13 @@ def interested_users(self, issue_number: int) -> list[UserNode]: return [i.user for i in interests] @strawberry.field - def task_deadline(self, issue_number: int) -> datetime | None: + def task_deadline(self, info: Info, issue_number: int) -> datetime | None: """Return the deadline for the latest assigned task linked to this module and issue.""" + mapping = getattr(info.context, "task_deadlines_by_issue", None) + if mapping: + return mapping.get(issue_number) + + # fallback (single issue query) return ( Task.objects.filter( module=self, @@ -152,8 +176,12 @@ def task_deadline(self, issue_number: int) -> datetime | None: ) @strawberry.field - def task_assigned_at(self, issue_number: int) -> datetime | None: - """Return the latest assignment time for tasks linked to this module and issue number.""" + def task_assigned_at(self, info: Info, issue_number: int) -> datetime | None: + """Return the latest assignment time for tasks linked to this module and issue.""" + mapping = getattr(info.context, "task_assigned_at_by_issue", None) + if mapping: + return mapping.get(issue_number) + return ( Task.objects.filter( module=self, diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx index eb99bf71ea..92356cff42 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx @@ -6,7 +6,7 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation' import { useCallback, useEffect, useMemo, useState } from 'react' import { ErrorDisplay, handleAppError } from 'app/global-error' import { GetModuleIssuesDocument } from 'types/__generated__/moduleQueries.generated' -import IssuesTable, { type IssueRow } from 'components/IssuesTable' +import IssuesTable from 'components/IssuesTable' import LoadingSpinner from 'components/LoadingSpinner' import Pagination from 'components/Pagination' @@ -26,13 +26,13 @@ const getDeadlineCategory = (deadline?: string | null): string => { const now = new Date() const deadlineDate = new Date(deadline) - const nowStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()) - const deadlineStart = new Date( - deadlineDate.getFullYear(), - deadlineDate.getMonth(), - deadlineDate.getDate() + const nowStartUtc = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) + const deadlineStartUtc = Date.UTC( + deadlineDate.getUTCFullYear(), + deadlineDate.getUTCMonth(), + deadlineDate.getUTCDate() ) - const diffDays = Math.round((deadlineStart.getTime() - nowStart.getTime()) / 86400000) + const diffDays = Math.floor((deadlineStartUtc - nowStartUtc) / 86400000) if (diffDays < 0) return 'overdue' if (diffDays <= 7) return 'due-soon' @@ -49,12 +49,15 @@ const IssuesPage = () => { ) const [currentPage, setCurrentPage] = useState(1) + const isDeadlineFilterActive = selectedDeadline !== DEADLINE_ALL + const MAX_ISSUES_FOR_DEADLINE_FILTER = 1000 + const { data, loading, error } = useQuery(GetModuleIssuesDocument, { variables: { programKey, moduleKey, - limit: ITEMS_PER_PAGE, - offset: (currentPage - 1) * ITEMS_PER_PAGE, + limit: isDeadlineFilterActive ? MAX_ISSUES_FOR_DEADLINE_FILTER : ITEMS_PER_PAGE, + offset: isDeadlineFilterActive ? 0 : (currentPage - 1) * ITEMS_PER_PAGE, label: selectedLabel === LABEL_ALL ? null : selectedLabel, }, skip: !programKey || !moduleKey, @@ -67,8 +70,8 @@ const IssuesPage = () => { const moduleData = data?.getModule - const moduleIssues: IssueRow[] = useMemo(() => { - const issues = (moduleData?.issues || []).map((i) => ({ + const { moduleIssues, filteredCount } = useMemo(() => { + const allIssues = (moduleData?.issues || []).map((i) => ({ objectID: i.id, number: i.number, title: i.title, @@ -80,13 +83,22 @@ const IssuesPage = () => { })) if (selectedDeadline !== DEADLINE_ALL) { - return issues.filter((issue) => getDeadlineCategory(issue.deadline) === selectedDeadline) + // Filter by deadline category + const filtered = allIssues.filter( + (issue) => getDeadlineCategory(issue.deadline) === selectedDeadline + ) + // Apply client-side pagination on filtered results + const start = (currentPage - 1) * ITEMS_PER_PAGE + const paginatedIssues = filtered.slice(start, start + ITEMS_PER_PAGE) + return { moduleIssues: paginatedIssues, filteredCount: filtered.length } } - return issues - }, [moduleData, selectedDeadline]) + return { moduleIssues: allIssues, filteredCount: moduleData?.issuesCount || 0 } + }, [moduleData, selectedDeadline, currentPage]) - const totalPages = Math.ceil((moduleData?.issuesCount || 0) / ITEMS_PER_PAGE) + const totalPages = Math.ceil( + (isDeadlineFilterActive ? filteredCount : moduleData?.issuesCount || 0) / ITEMS_PER_PAGE + ) const allLabels: string[] = useMemo(() => { const serverLabels = moduleData?.availableLabels diff --git a/frontend/src/components/IssuesTable.tsx b/frontend/src/components/IssuesTable.tsx index 2d08aad586..90fdbc9a3d 100644 --- a/frontend/src/components/IssuesTable.tsx +++ b/frontend/src/components/IssuesTable.tsx @@ -83,13 +83,8 @@ const IssuesTable: React.FC = ({ const now = new Date() const deadlineDate = new Date(deadline) - const nowStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()) - const deadlineStart = new Date( - deadlineDate.getFullYear(), - deadlineDate.getMonth(), - deadlineDate.getDate() - ) - const diffDays = Math.round((deadlineStart.getTime() - nowStart.getTime()) / 86400000) + const utcStart = (d: Date) => Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()) + const diffDays = Math.floor((utcStart(deadlineDate) - utcStart(now)) / 86400000) if (diffDays < 0) return { From 38ee33af3be063b7967621ae3dd36a8b0ecbc3e2 Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Sun, 1 Feb 2026 00:21:36 +0530 Subject: [PATCH 5/6] fixed review --- .../mentorship/api/internal/nodes/module.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/backend/apps/mentorship/api/internal/nodes/module.py b/backend/apps/mentorship/api/internal/nodes/module.py index c220dba52f..ec92226644 100644 --- a/backend/apps/mentorship/api/internal/nodes/module.py +++ b/backend/apps/mentorship/api/internal/nodes/module.py @@ -83,19 +83,27 @@ def issues( info.context.current_module = self # BULK load data - task_rows = ( + deadline_rows = ( Task.objects.filter(module=self, deadline_at__isnull=False) .order_by("issue__number", "-assigned_at") - .values("issue__number", "deadline_at", "assigned_at") + .values("issue__number", "deadline_at") + ) + assigned_rows = ( + Task.objects.filter(module=self, assigned_at__isnull=False) + .order_by("issue__number", "-assigned_at") + .values("issue__number", "assigned_at") ) deadline_map = {} assigned_map = {} - for row in task_rows: + for row in deadline_rows: num = row["issue__number"] if num not in deadline_map: deadline_map[num] = row["deadline_at"] + for row in assigned_rows: + num = row["issue__number"] + if num not in assigned_map: assigned_map[num] = row["assigned_at"] info.context.task_deadlines_by_issue = deadline_map @@ -160,7 +168,7 @@ def interested_users(self, issue_number: int) -> list[UserNode]: def task_deadline(self, info: Info, issue_number: int) -> datetime | None: """Return the deadline for the latest assigned task linked to this module and issue.""" mapping = getattr(info.context, "task_deadlines_by_issue", None) - if mapping: + if mapping is not None: return mapping.get(issue_number) # fallback (single issue query) @@ -179,7 +187,7 @@ def task_deadline(self, info: Info, issue_number: int) -> datetime | None: def task_assigned_at(self, info: Info, issue_number: int) -> datetime | None: """Return the latest assignment time for tasks linked to this module and issue.""" mapping = getattr(info.context, "task_assigned_at_by_issue", None) - if mapping: + if mapping is not None: return mapping.get(issue_number) return ( From 5bc601bce35c4c271e34c8d659f319f18b3f4070 Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Sat, 7 Feb 2026 23:07:44 +0530 Subject: [PATCH 6/6] fixed coderabbit review --- backend/apps/github/api/internal/nodes/issue.py | 2 +- frontend/src/components/IssuesTable.tsx | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index 738ac33169..40c1bddf66 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -4,8 +4,8 @@ import strawberry import strawberry_django -from strawberry.types import Info from django.db.models import Exists, OuterRef +from strawberry.types import Info from apps.github.api.internal.nodes.pull_request import PullRequestNode from apps.github.api.internal.nodes.user import UserNode diff --git a/frontend/src/components/IssuesTable.tsx b/frontend/src/components/IssuesTable.tsx index 397ab86000..2015ce12dd 100644 --- a/frontend/src/components/IssuesTable.tsx +++ b/frontend/src/components/IssuesTable.tsx @@ -214,20 +214,19 @@ const IssuesTable: React.FC = ({ )} {/* Deadline */} - {showDeadline && ( - - {(() => { - const status = getDeadlineStatus(issue.deadline) - return ( + {showDeadline && + (() => { + const status = getDeadlineStatus(issue.deadline) + return ( + {status.text} - ) - })()} -