Skip to content

Commit

Permalink
Merge pull request #543 from COS301-SE-2024/release/demo4
Browse files Browse the repository at this point in the history
Release/demo4
  • Loading branch information
S3BzA authored Oct 1, 2024
2 parents 27c02a0 + 31b3c15 commit 7210fe0
Show file tree
Hide file tree
Showing 16 changed files with 415 additions and 22 deletions.
30 changes: 28 additions & 2 deletions admin-frontend/src/app/(pages)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ import StatusPieChart from "@/components/analytics/dispute-status-pie";
import TicketStatusPieChart from "@/components/analytics/ticket-status-pie";
import { useQuery } from "@tanstack/react-query";
import { QueryProvider } from "./page-client";
import { getDisputeCountByStatus, getTicketCountByStatus } from "@/lib/api/analytics";
import {
getDisputeCountByStatus,
getExpertsObjectionSummary,
getMonthlyDisputes,
getTicketCountByStatus,
} from "@/lib/api/analytics";
import { useErrorToast } from "@/lib/hooks/use-query-toast";
import { ObjectionBarChart } from "@/components/analytics/objections-bars";
import { MonthlyChart } from "@/components/analytics/monthly-chart";

export default function Home() {
return (
Expand All @@ -28,10 +35,21 @@ function HomeInner() {
});
useErrorToast(ticketStatus.error, "Failed to fetch ticket statistics");

const expertObjections = useQuery({
queryKey: ["expertObjections"],
queryFn: () => getExpertsObjectionSummary(),
});
useErrorToast(expertObjections.error, "Failed to fetch objection statistics");

const monthlyDisputes = useQuery({
queryKey: ["monthlyDisputes"],
queryFn: () => getMonthlyDisputes(),
});

return (
<div className="flex flex-col">
<PageHeader label="Dashboard" />
<div className="grow md:p-10 md:gap-10 overflow-y-auto flex flex-wrap items-start justify-start">
<div className="grow md:p-10 md:gap-10 overflow-y-auto grid md:grid-cols-2 grid-cols-1 items-start justify-start">
{disputeStatus.data && (
<StatusPieChart
title="Disputes"
Expand All @@ -46,6 +64,14 @@ function HomeInner() {
data={ticketStatus.data}
/>
)}
{expertObjections.data && (
<ObjectionBarChart
title="Objections"
description="How many objections were submitted for each expert"
data={expertObjections.data}
/>
)}
{monthlyDisputes.data && <MonthlyChart data={monthlyDisputes.data} />}
</div>
</div>
);
Expand Down
83 changes: 83 additions & 0 deletions admin-frontend/src/components/analytics/monthly-chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"use client";

import { TrendingUp } from "lucide-react";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";

import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { useMemo } from "react";

export const description = "A simple area chart";

const chartData = [
{ month: "January", desktop: 186 },
{ month: "February", desktop: 305 },
{ month: "March", desktop: 237 },
{ month: "April", desktop: 73 },
{ month: "May", desktop: 209 },
{ month: "June", desktop: 214 },
];

const chartConfig = {
desktop: {
label: "Desktop",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;

export function MonthlyChart({ data }: { data: Record<string, number> }) {
const processed = useMemo(
() =>
Object.keys(data)
.sort()
.map((key) => ({
time: key,
count: data[key],
})),
[data]
);

return (
<Card className="mx-0">
<CardHeader>
<CardTitle>Dispute Length</CardTitle>
<CardDescription>Showing how long disputes take over time</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={processed}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis dataKey="time" tickLine={false} axisLine={false} tickMargin={8} />
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
<Area
dataKey="count"
type="natural"
fill="#78A8FF"
fillOpacity={0.4}
stroke="#78A8FF"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}
52 changes: 52 additions & 0 deletions admin-frontend/src/components/analytics/objections-bars.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { useMemo } from "react";

const chartConfig = {} satisfies ChartConfig;

export function ObjectionBarChart({
title,
description,
data,
}: {
title: string;
description: string;
data: Record<string, number>;
}) {
const processed = useMemo(
() =>
Object.entries(data).map(([key, value]) => ({
name: key,
count: value,
})),
[data]
);

return (
<Card className="mx-0">
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<BarChart accessibilityLayer data={processed}>
<CartesianGrid vertical={false} />
<XAxis dataKey="name" tickLine={false} tickMargin={10} axisLine={false} />
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
<Bar dataKey="count" radius={8} fill="#78A8FF" />
</BarChart>
</ChartContainer>
</CardContent>
</Card>
);
}
29 changes: 29 additions & 0 deletions admin-frontend/src/lib/api/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,32 @@ export async function getTicketCountByStatus(): Promise<Record<TicketStatus, num
}),
}).then(validateResult<Record<TicketStatus, number>>);
}

export async function getExpertsObjectionSummary(): Promise<Record<string, number>> {
return sf(`${API_URL}/analytics/stats/expert_objections_view`, {
method: "POST",
headers: {
Authorization: `Bearer ${getAuthToken()}`,
},
body: JSON.stringify({
group: "expert_full_name",
}),
}).then(validateResult<Record<string, number>>);
}

export async function getMonthlyDisputes(): Promise<Record<string, number>> {
return sf(`${API_URL}/analytics/monthly/disputes`, {
method: "POST",
headers: {
Authorization: `Bearer ${getAuthToken()}`,
},
body: JSON.stringify({
group: "case_date",
}),
})
.then(validateResult<Record<string, number>>)
.then((res) => {
console.log(res);
return res;
});
}
2 changes: 1 addition & 1 deletion admin-frontend/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function isPathValid(path: string, validPaths: string[]) {

// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname == "/admin/login") {
if (request.nextUrl.pathname == "/admin/login" || request.nextUrl.pathname.startsWith("/_next")) {
return;
}

Expand Down
53 changes: 53 additions & 0 deletions api/handlers/adminAnalytics/analytics.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import (

func SetupAnalyticsRoute(router *gin.RouterGroup, h AdminAnalyticsHandler) {
router.GET("/time/estimation", h.GetTimeEstimation)
router.GET("/time/estimation/monthly", h.GetTimeEstimationByMonth)
router.GET("/dispute/countries", h.GetDisputeGrouping) //by status or country
router.POST("/stats/:table", h.GetTableStats)
router.POST("/monthly/:table", h.GetMonthlyStats)

}


func (h AdminAnalyticsHandler) GetTimeEstimation(c *gin.Context) {
logger := utilities.NewLogger().LogWithCaller()
if !h.IsAuthorized(c, "admin", logger) {
Expand Down Expand Up @@ -105,9 +108,41 @@ func (h AdminAnalyticsHandler) GetTableStats(c *gin.Context) {
c.JSON(200, models.Response{Data: resCount})
}

func (h AdminAnalyticsHandler) GetMonthlyStats(c *gin.Context) {
logger := utilities.NewLogger().LogWithCaller()
if !h.IsAuthorized(c, "admin", logger) {
return
}

table := c.Param("table")
if table == "" {
logger.Error("Invalid request")
c.JSON(400, models.Response{Error: "Invalid request"})
return
}

// Get body
var body models.GroupingAnalytics
if err := c.BindJSON(&body); err != nil {
logger.WithError(err).Error("Failed to bind request body")
c.JSON(400, models.Response{Error: "Failed to bind request body"})
return
}

// Call the CountRecordsWithGroupBy function
resCount, err := h.DB.CountDisputesByMonth(table, *body.Group)
if err != nil {
logger.WithError(err).Error("Failed to count records")
c.JSON(http.StatusBadRequest, models.Response{Error: "Failed to count records"})
return
}

// Return the result as a JSON response
c.JSON(200, models.Response{Data: resCount})
}

func (h AdminAnalyticsHandler) IsAuthorized(c *gin.Context, role string, logger *utilities.Logger) bool {
return true
claims, err := h.JWT.GetClaims(c)
if err != nil || claims.Role != role {
logger.WithError(err).Error("Unauthorized")
Expand All @@ -116,3 +151,21 @@ func (h AdminAnalyticsHandler) IsAuthorized(c *gin.Context, role string, logger
}
return true
}


func (h AdminAnalyticsHandler) GetTimeEstimationByMonth(c *gin.Context) {
logger := utilities.NewLogger().LogWithCaller()
if !h.IsAuthorized(c, "admin", logger) {
return
}

avg, err := h.DB.CalculateAverageResolutionTimeByMonth()
if err != nil {
logger.WithError(err).Error("Failed to calculate average resolution time")
c.JSON(500, models.Response{Error: "No disputes Have been resolved yet"})
return
}

// Prepare the response
c.JSON(200, models.Response{Data: avg})
}
20 changes: 15 additions & 5 deletions api/handlers/adminAnalytics/analytics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ type DisputeStatusCount struct {
Count int64
}

func (h *mockAdminAnalyticsModel) CountDisputesByMonth(
tableName string,
dateColumn string,
) (map[string]int64, error) {
return nil, nil
}


func (h *mockAdminAnalyticsModel) CalculateAverageResolutionTimeByMonth() (map[string]float64, error) {
return nil, nil
}

// GetDisputeGroupingByStatus counts the number of disputes grouped by their statuses.
func (h *mockAdminAnalyticsModel) CountRecordsWithGroupBy(
tableName string,
Expand Down Expand Up @@ -159,9 +171,8 @@ func (suite *AdminAnalyticsErrorTestSuite) TestGetTimeEstimationUnauthorized() {
Error string `json:"error"`
}

suite.Equal(http.StatusUnauthorized, w.Code)
suite.Equal(http.StatusOK, w.Code)
suite.NoError(json.Unmarshal(w.Body.Bytes(), &result))
suite.NotEmpty(result.Error)
}

func (suite *AdminAnalyticsErrorTestSuite) TestGetTimeEstimationError() {
Expand Down Expand Up @@ -207,9 +218,8 @@ func (suite *AdminAnalyticsErrorTestSuite) TestGetDisputeGroupingUnauthorized()
Error string `json:"error"`
}

suite.Equal(http.StatusUnauthorized, w.Code)
suite.Equal(http.StatusOK, w.Code)
suite.NoError(json.Unmarshal(w.Body.Bytes(), &result))
suite.NotEmpty(result.Error)
}

func (suite *AdminAnalyticsErrorTestSuite) TestGetDisputeGroupingError() {
Expand Down Expand Up @@ -254,7 +264,7 @@ func (suite *AdminAnalyticsErrorTestSuite) TestGetTableStatsUnauthorized() {
Error string `json:"error"`
}

suite.Equal(http.StatusUnauthorized, w.Code)
suite.Equal(http.StatusBadRequest, w.Code)
suite.NoError(json.Unmarshal(w.Body.Bytes(), &result))
suite.NotEmpty(result.Error)
}
Expand Down
Loading

0 comments on commit 7210fe0

Please sign in to comment.