Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/demo4 #543

Merged
merged 20 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@

func SetupAnalyticsRoute(router *gin.RouterGroup, h AdminAnalyticsHandler) {
router.GET("/time/estimation", h.GetTimeEstimation)
router.GET("/time/estimation/monthly", h.GetTimeEstimationByMonth)

Check warning on line 14 in api/handlers/adminAnalytics/analytics.go

View check run for this annotation

Codecov / codecov/patch

api/handlers/adminAnalytics/analytics.go#L14

Added line #L14 was not covered by tests
router.GET("/dispute/countries", h.GetDisputeGrouping) //by status or country
router.POST("/stats/:table", h.GetTableStats)
router.POST("/monthly/:table", h.GetMonthlyStats)

Check warning on line 17 in api/handlers/adminAnalytics/analytics.go

View check run for this annotation

Codecov / codecov/patch

api/handlers/adminAnalytics/analytics.go#L17

Added line #L17 was not covered by tests

}


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 @@
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

Check warning on line 114 in api/handlers/adminAnalytics/analytics.go

View check run for this annotation

Codecov / codecov/patch

api/handlers/adminAnalytics/analytics.go#L111-L114

Added lines #L111 - L114 were not covered by tests
}

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

Check warning on line 121 in api/handlers/adminAnalytics/analytics.go

View check run for this annotation

Codecov / codecov/patch

api/handlers/adminAnalytics/analytics.go#L117-L121

Added lines #L117 - L121 were not covered by tests
}

// 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

Check warning on line 129 in api/handlers/adminAnalytics/analytics.go

View check run for this annotation

Codecov / codecov/patch

api/handlers/adminAnalytics/analytics.go#L125-L129

Added lines #L125 - L129 were not covered by tests
}

// 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

Check warning on line 137 in api/handlers/adminAnalytics/analytics.go

View check run for this annotation

Codecov / codecov/patch

api/handlers/adminAnalytics/analytics.go#L133-L137

Added lines #L133 - L137 were not covered by tests
}

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

Check warning on line 141 in api/handlers/adminAnalytics/analytics.go

View check run for this annotation

Codecov / codecov/patch

api/handlers/adminAnalytics/analytics.go#L141

Added line #L141 was not covered by tests
}

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 @@
}
return true
}


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

Check warning on line 159 in api/handlers/adminAnalytics/analytics.go

View check run for this annotation

Codecov / codecov/patch

api/handlers/adminAnalytics/analytics.go#L156-L159

Added lines #L156 - L159 were not covered by tests
}

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

Check warning on line 166 in api/handlers/adminAnalytics/analytics.go

View check run for this annotation

Codecov / codecov/patch

api/handlers/adminAnalytics/analytics.go#L162-L166

Added lines #L162 - L166 were not covered by tests
}

// Prepare the response
c.JSON(200, models.Response{Data: avg})

Check warning on line 170 in api/handlers/adminAnalytics/analytics.go

View check run for this annotation

Codecov / codecov/patch

api/handlers/adminAnalytics/analytics.go#L170

Added line #L170 was not covered by tests
}
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
Loading