Skip to content

Commit

Permalink
Merge pull request #218 from CS3219-AY2425S1/141-Integrate-tracker-fo…
Browse files Browse the repository at this point in the history
…r-main-dashboard

141 integrate tracker for main dashboard
  • Loading branch information
glemenneo authored Nov 13, 2024
2 parents 0e40b12 + db844a5 commit 714b694
Show file tree
Hide file tree
Showing 18 changed files with 235 additions and 54 deletions.
27 changes: 17 additions & 10 deletions backend/matching-service/src/controllers/matching.controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { ICollabDto, LanguageMode } from '@repo/collaboration-types'
import { convertComplexityToSortedComplexity } from '@repo/question-types'
import { IPaginationRequest, ITypedBodyRequest } from '@repo/request-types'
import { IMatch } from '@repo/user-types'
import { WebSocketMessageType } from '@repo/ws-types'
import axios from 'axios'
import { randomUUID } from 'crypto'
import { Response } from 'express'
import { wsConnection } from '../server'
import mqConnection from '../services/rabbitmq.service'
import { IMatch } from '@repo/user-types'
import { ICollabDto, LanguageMode } from '@repo/collaboration-types'
import { MatchDto } from '../types/MatchDto'
import { Request, Response } from 'express'
import config from '../common/config.util'
import logger from '../common/logger.util'
import {
createMatch,
findCompletedQuestionCount,
findMatchCount,
findOngoingMatch,
findPaginatedMatches,
Expand All @@ -18,12 +20,11 @@ import {
isValidSort,
updateMatchCompletion,
} from '../models/matching.repository'
import { wsConnection } from '../server'
import { getRandomQuestion } from '../services/matching.service'
import { convertComplexityToSortedComplexity } from '@repo/question-types'
import mqConnection from '../services/rabbitmq.service'
import { MatchDto } from '../types/MatchDto'
import { UserQueueRequest, UserQueueRequestDto } from '../types/UserQueueRequestDto'
import axios from 'axios'
import config from '../common/config.util'
import logger from '../common/logger.util'

export async function generateWS(request: ITypedBodyRequest<void>, response: Response): Promise<void> {
const userHasMatch = await isUserInMatch(request.user.id)
Expand Down Expand Up @@ -197,3 +198,9 @@ export async function handleIsUserInMatch(request: ITypedBodyRequest<void>, resp
data: userMatch,
})
}

export async function handleGetCompletedQuestionCounts(request: Request, response: Response): Promise<void> {
const userId = request.params.id
const counts = await findCompletedQuestionCount(userId)
response.status(200).json(counts).send()
}
44 changes: 42 additions & 2 deletions backend/matching-service/src/models/matching.repository.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { convertSortedComplexityToComplexity, IQuestionCountsDto } from '@repo/question-types'
import { IMatch, SortedComplexity } from '@repo/user-types'
import { Model, model, SortOrder } from 'mongoose'
import matchSchema from './matching.model'
import { IMatch } from '@repo/user-types'
import { MatchDto } from '../types/MatchDto'
import matchSchema from './matching.model'

const matchModel: Model<IMatch> = model('Match', matchSchema)

Expand Down Expand Up @@ -74,3 +75,42 @@ export async function findOngoingMatch(userId: string): Promise<IMatch> {
})
return match
}

export async function findCompletedQuestionCount(userId: string): Promise<IQuestionCountsDto> {
const query = [
{
$match: {
$or: [{ user1Id: userId }, { user2Id: userId }],
isCompleted: true,
},
},
{
$group: {
_id: { complexity: '$complexity', question: '$question' },
count: { $sum: 1 },
},
},
{
$group: {
_id: '$_id.complexity',
count: { $sum: 1 },
},
},
{
$project: {
_id: 0,
complexity: '$_id',
count: 1,
},
},
]
const result: {
complexity: SortedComplexity
count: number
}[] = await matchModel.aggregate(query)
return {
data: result.map((row) => {
return { ...row, complexity: convertSortedComplexityToComplexity(row.complexity) }
}),
}
}
2 changes: 2 additions & 0 deletions backend/matching-service/src/routes/matching.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import passport from 'passport'
import {
generateWS,
getMatchDetails,
handleGetCompletedQuestionCounts,
handleGetPaginatedSessions,
handleIsUserInMatch,
updateCompletion,
Expand All @@ -14,6 +15,7 @@ router.put('/', updateCompletion)
router.use(passport.authenticate('jwt', { session: false }))
router.post('/', generateWS)
router.get('/current', handleIsUserInMatch)
router.get('/user/:id/complexity/count', handleGetCompletedQuestionCounts)
router.get('/:id', getMatchDetails)
router.get('/', handleGetPaginatedSessions)

Expand Down
1 change: 1 addition & 0 deletions backend/question-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@repo/eslint-config": "*",
"@repo/request-types": "*",
"@repo/typescript-config": "*",
"@repo/question-types": "*",
"@repo/user-types": "*",
"@testcontainers/mongodb": "^10.13.1",
"@types/cors": "^2.8.17",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
findPaginatedQuestionsWithSort,
findPaginatedQuestionsWithSortAndFilter,
findQuestionCount,
findQuestionCountsByComplexity,
findQuestionCountWithFilter,
findRandomQuestionByTopicAndComplexity,
getFilterKeys,
Expand Down Expand Up @@ -198,3 +199,8 @@ export async function handleGetRandomQuestion(request: Request, response: Respon

response.status(200).json(question).send()
}

export async function handleGetQuestionCounts(_: Request, response: Response): Promise<void> {
const counts = await findQuestionCountsByComplexity()
response.status(200).json(counts).send()
}
24 changes: 20 additions & 4 deletions backend/question-service/src/models/question.repository.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { convertSortedComplexityToComplexity, IQuestionCountsDto } from '@repo/question-types'
import { Category, SortedComplexity } from '@repo/user-types'
import { FilterQuery, Model, model, SortOrder } from 'mongoose'

import { CreateQuestionDto } from '../types/CreateQuestionDto'
import { IQuestion } from '../types/IQuestion'
import { SortedComplexity } from '@repo/user-types'
import questionSchema from './question.model'
import { Category } from '@repo/user-types'
import { QuestionDto } from '../types/QuestionDto'
import questionSchema from './question.model'

const questionModel: Model<IQuestion> = model('Question', questionSchema)

Expand Down Expand Up @@ -110,6 +109,23 @@ export async function deleteQuestion(id: string): Promise<void> {
await questionModel.findByIdAndDelete(id)
}

export async function findQuestionCountsByComplexity(): Promise<IQuestionCountsDto> {
const query = [
{
$group: {
_id: '$complexity',
count: { $sum: 1 },
},
},
]
const counts: IQuestionCountsDto = { data: [] }
const result = await questionModel.aggregate(query)
for (const { _id, count } of result) {
counts.data.push({ complexity: convertSortedComplexityToComplexity(_id), count })
}
return counts
}

function getFilterQueryOptions(filterBy: string[][]): FilterQuery<IQuestion>[] {
return filterBy.map(([key, value]) => {
const query: { [key: string]: string | object } = {}
Expand Down
10 changes: 5 additions & 5 deletions backend/question-service/src/routes/question.routes.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { Role } from '@repo/user-types'
import { Router } from 'express'
import passport from 'passport'
import {
handleCreateQuestion,
handleDeleteQuestion,
handleGetCategories,
handleGetFilters,
handleGetPaginatedQuestions,
handleGetQuestionById,
handleGetQuestionCounts,
handleGetRandomQuestion,
handleGetSorts,
handleUpdateQuestion,
} from '../controllers/question.controller'

import { Role } from '@repo/user-types'
import { Router } from 'express'
import { handleRoleBasedAccessControl } from '../middlewares/accessControl.middleware'
import passport from 'passport'

const router = Router()

router.get('/random-question', handleGetRandomQuestion)
router.use(passport.authenticate('jwt', { session: false }))

router.get('/complexity/count', handleGetQuestionCounts)
router.get('/', handleGetPaginatedQuestions)
router.get('/:id', handleGetQuestionById)
router.post('/', handleRoleBasedAccessControl([Role.ADMIN]), handleCreateQuestion)
Expand Down
22 changes: 17 additions & 5 deletions frontend/components/dashboard/progress-card.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
'use client'

import { Progress } from '@/components/ui/progress'
import { Complexity } from '@repo/user-types'

interface ProgressCardProps {
complexity: string
score: string
complexity: Complexity
completed: number
total: number
progress: number
indicatorColor: string
backgroundColor: string
}

export const ProgressCard = ({ complexity, score, progress, indicatorColor, backgroundColor }: ProgressCardProps) => {
export const ProgressCard = ({
complexity,
completed,
total,
progress,
indicatorColor,
backgroundColor,
}: ProgressCardProps) => {
return (
<div className="border-solid border-2 border-gray-200 rounded flex flex-col w-2/6 mx-2 p-6">
<p className="text-medium font-medium">{complexity}</p>
<h4 className="text-4xl font-extrabold mb-4">{score}</h4>
<p className="text-medium font-medium">
{complexity.charAt(0).toUpperCase()}
{complexity.slice(1).toLowerCase()}
</p>
<h4 className="text-4xl font-extrabold mb-4">{`${completed}/${total}`}</h4>
<Progress value={progress} indicatorColor={indicatorColor} backgroundColor={backgroundColor} />
</div>
)
Expand Down
85 changes: 60 additions & 25 deletions frontend/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,42 @@ import { ProgressCard } from '@/components/dashboard/progress-card'
import { RecentSessions } from '@/components/dashboard/recent-sessions'
import ResumeSession from '@/components/dashboard/resume-session'
import useProtectedRoute from '@/hooks/UseProtectedRoute'
import { getOngoingMatch } from '@/services/matching-service-api'
import { getCompletedQuestionCountsRequest, getOngoingMatch } from '@/services/matching-service-api'
import { getSessionsRequest } from '@/services/session-service-api'
import { IGetSessions, IPartialSessions } from '@/types'
import { IMatch } from '@repo/user-types'
import { IQuestionCountsDto } from '@repo/question-types'
import { Complexity, IMatch } from '@repo/user-types'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { getQuestionCountsRequest } from '../services/question-service-api'

export default function Home() {
const progressData = [
const [progressData, setProgressData] = useState([
{
complexity: 'Easy',
score: '10/20',
progress: 50,
indicatorColor: 'bg-green-700',
backgroundColor: 'bg-green-500',
complexity: Complexity.EASY,
completed: 0,
total: 0,
progress: 0,
indicatorColor: 'bg-green-500',
backgroundColor: 'bg-green-300',
},
{
complexity: 'Medium',
score: '15/27',
progress: 60,
complexity: Complexity.MEDIUM,
completed: 0,
total: 0,
progress: 0,
indicatorColor: 'bg-amber-500',
backgroundColor: 'bg-amber-300',
},
{
complexity: 'Hard',
score: '5/19',
progress: 20,
complexity: Complexity.HARD,
completed: 0,
total: 0,
progress: 0,
indicatorColor: 'bg-red-500',
backgroundColor: 'bg-red-400',
},
]
])

const { session, loading } = useProtectedRoute()
const [ongoingMatchData, setOngoingMatchData] = useState<IMatch | null>()
Expand All @@ -54,6 +60,31 @@ export default function Home() {
},
showCancelButton: false,
})

const loadProgressData = async () => {
if (!session?.user?.id) return
try {
const [completedQuestionCountsResponse, questionCountsResponse]: (IQuestionCountsDto | undefined)[] =
await Promise.all([getCompletedQuestionCountsRequest(session.user.id), getQuestionCountsRequest()])
if (completedQuestionCountsResponse && questionCountsResponse) {
const { data: questionCountsData } = questionCountsResponse
const { data: completedQuestionCountsData } = completedQuestionCountsResponse
setProgressData((oldProgressData) => {
return oldProgressData.map((oldData) => {
const { complexity } = oldData
const completed =
completedQuestionCountsData?.find((item) => item.complexity === complexity)?.count || 0
const total = questionCountsData?.find((item) => item.complexity === complexity)?.count || 0
const progress = (completed / total) * 100
return { ...oldData, completed, total, progress }
})
})
}
} catch (error: unknown) {
toast.error((error as Error).message)
}
}

const [recentSessions, setRecentSessions] = useState<IPartialSessions[]>([])

const checkOngoingSession = async () => {
Expand Down Expand Up @@ -104,6 +135,7 @@ export default function Home() {
useEffect(() => {
checkOngoingSession()
getRecentSessions()
loadProgressData()
}, [])

if (loading)
Expand All @@ -117,16 +149,19 @@ export default function Home() {
<div className="my-4">
<h2 className="text-xl font-bold my-6">Welcome Back, {session?.user.username}</h2>
<div className="flex flex-row justify-evenly -mx-2">
{progressData.map(({ complexity, score, progress, indicatorColor, backgroundColor }, index) => (
<ProgressCard
key={index}
complexity={complexity}
score={score}
progress={progress}
indicatorColor={indicatorColor}
backgroundColor={backgroundColor}
/>
))}
{progressData.map(
({ complexity, completed, total, progress, indicatorColor, backgroundColor }, index) => (
<ProgressCard
key={index}
complexity={complexity}
completed={completed}
total={total}
progress={progress}
indicatorColor={indicatorColor}
backgroundColor={backgroundColor}
/>
)
)}
</div>
<div className="flex flex-row justify-between my-4">
{ongoingMatchData ? (
Expand Down
Loading

0 comments on commit 714b694

Please sign in to comment.