Skip to content
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
6 changes: 3 additions & 3 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
name: END TO END TESTS

on:
push:
branches:
- customer-portal-milestone-1
#push:
#branches:
#- customer-portal-milestone-1
workflow_dispatch:
Comment thread
dileepapeiris marked this conversation as resolved.

jobs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ vi.mock("@/hooks/useLogger", () => ({

const mockSupportStatsResponse = {
ongoingCases: 47,
activeChats: 3,
sessionChats: 10,
resolvedRecently: 10,
resolvedChats: 34,
activeChats: 3,
};

vi.mock("@asgardeo/react", () => ({
Expand Down
50 changes: 43 additions & 7 deletions apps/customer-portal/webapp/src/api/useGetProjectCasesStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ import { useAuthApiClient } from "@api/useAuthApiClient";
import { useLogger } from "@hooks/useLogger";
import { ApiQueryKeys } from "@constants/apiConstants";
import type { ProjectCasesStats } from "@models/responses";
import { CaseType } from "@constants/supportConstants";

export { DASHBOARD_CASE_TYPE_LABELS } from "@constants/dashboardConstants";

export interface UseGetProjectCasesStatsOptions {
incidentId?: string;
queryId?: string;
/** When false, the query will not run. Use to wait for filters before fetching. */
caseTypes?: Array<`${(typeof CaseType)[keyof typeof CaseType]}` | string>;
enabled?: boolean;
}

Expand All @@ -45,10 +46,10 @@ export function useGetProjectCasesStats(
const logger = useLogger();
const { isSignedIn, isLoading: isAuthLoading } = useAsgardeo();
const authFetch = useAuthApiClient();
const { incidentId, queryId, enabled = true } = options ?? {};
const { incidentId, queryId, caseTypes, enabled = true } = options ?? {};

return useQuery<ProjectCasesStats, Error>({
queryKey: [ApiQueryKeys.CASES_STATS, id, incidentId, queryId],
queryKey: [ApiQueryKeys.CASES_STATS, id, incidentId, queryId, caseTypes],
queryFn: async (): Promise<ProjectCasesStats> => {
logger.debug(`Fetching case stats for project ID: ${id}`);

Expand All @@ -60,11 +61,23 @@ export function useGetProjectCasesStats(
}

let requestUrl = `${baseUrl}/projects/${id}/stats/cases`;
if (incidentId && queryId) {
const params = new URLSearchParams();

const params = new URLSearchParams();

if (Array.isArray(caseTypes) && caseTypes.length > 0) {
caseTypes.forEach((type) => {
if (type) {
params.append("caseTypes", type);
}
});
} else if (incidentId && queryId) {
params.append("caseTypes", queryId);
params.append("caseTypes", incidentId);
requestUrl += `?${params.toString()}`;
}

const queryString = params.toString();
if (queryString) {
requestUrl += `?${queryString}`;
}

const response = await authFetch(requestUrl, {
Expand All @@ -79,7 +92,30 @@ export function useGetProjectCasesStats(
throw new Error(`Error fetching case stats: ${response.statusText}`);
}

const data: ProjectCasesStats = await response.json();
const raw = (await response.json()) as any;

const data: ProjectCasesStats = {
totalCases: raw?.totalCases ?? raw?.totalCount ?? 0,
totalCount: raw?.totalCount,
activeCount: raw?.activeCount,
outstandingCount: raw?.outstandingCount,
averageResponseTime: raw?.averageResponseTime ?? 0,
resolvedCases: {
total: raw?.resolvedCases?.total ?? 0,
currentMonth: raw?.resolvedCases?.currentMonth ?? 0,
pastThirtyDays: raw?.resolvedCases?.pastThirtyDays,
},
changeRate: raw?.changeRate,
stateCount: raw?.stateCount ?? [],
severityCount: raw?.severityCount ?? [],
outstandingSeverityCount: raw?.outstandingSeverityCount ?? [],
caseTypeCount: raw?.caseTypeCount ?? [],
casesTrend: raw?.casesTrend ?? [],
engagementTypeCount: raw?.engagementTypeCount ?? [],
outstandingEngagementTypeCount:
raw?.outstandingEngagementTypeCount ?? [],
};

logger.debug("[useGetProjectCasesStats] Data received:", data);
return data;
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import { useQuery, type UseQueryResult } from "@tanstack/react-query";
import { useAsgardeo } from "@asgardeo/react";
import { useAuthApiClient } from "@api/useAuthApiClient";
import { useLogger } from "@hooks/useLogger";
import { ApiQueryKeys } from "@constants/apiConstants";
import type { ChangeRequestStatsResponse } from "@models/responses";

export interface UseGetProjectChangeRequestsStatsOptions {
enabled?: boolean;
}

/**
* Custom hook to fetch change request statistics for a project.
*
* @param {string} id - The ID of the project.
* @param {UseGetProjectChangeRequestsStatsOptions} [options] - Optional configuration (enabled).
* @returns {UseQueryResult<ChangeRequestStatsResponse, Error>} The query result object.
*/
export function useGetProjectChangeRequestsStats(
id: string,
options?: UseGetProjectChangeRequestsStatsOptions,
): UseQueryResult<ChangeRequestStatsResponse, Error> {
const logger = useLogger();
const { isSignedIn, isLoading: isAuthLoading } = useAsgardeo();
const authFetch = useAuthApiClient();
const { enabled = true } = options ?? {};

return useQuery<ChangeRequestStatsResponse, Error>({
queryKey: [ApiQueryKeys.CHANGE_REQUEST_STATS, id],
Comment thread
dileepapeiris marked this conversation as resolved.
queryFn: async (): Promise<ChangeRequestStatsResponse> => {
logger.debug(
`Fetching change request stats for project ID: ${id}`,
);

try {
const baseUrl = window.config?.CUSTOMER_PORTAL_BACKEND_BASE_URL;

if (!baseUrl) {
throw new Error("CUSTOMER_PORTAL_BACKEND_BASE_URL is not configured");
}

const requestUrl = `${baseUrl}/projects/${id}/stats/change-requests`;

const response = await authFetch(requestUrl, {
method: "GET",
});

logger.debug(
`[useGetProjectChangeRequestsStats] Response status for ${id}: ${response.status}`,
);

if (!response.ok) {
throw new Error(
`Error fetching change request stats: ${response.statusText}`,
);
}

const raw = (await response.json()) as any;

const data: ChangeRequestStatsResponse = {
totalCount: raw?.totalCount ?? 0,
activeCount: raw?.activeCount,
outstandingCount: raw?.outstandingCount,
stateCount: raw?.stateCount ?? [],
};

logger.debug(
"[useGetProjectChangeRequestsStats] Data received:",
data,
);
return data;
} catch (error) {
logger.error("[useGetProjectChangeRequestsStats] Error:", error);
throw error;
}
},
enabled: !!id && isSignedIn && !isAuthLoading && enabled,
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
}

Original file line number Diff line number Diff line change
Expand Up @@ -96,65 +96,68 @@ export const ActiveCasesChart = ({
"& *:focus": { outline: "none" },
}}
>
<ResponsiveContainer width="100%" height="100%">
<PieChart legend={{ show: false }} tooltip={{ show: !isError }}>
<Pie
data={chartData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={0}
minAngle={15}
dataKey="value"
nameKey="name"
startAngle={90}
endAngle={-270}
label={false}
labelLine={false}
<ResponsiveContainer width="100%" height="100%">
<PieChart
legend={{ show: false }}
tooltip={{ show: !isError, wrapperStyle: { zIndex: 1000 } }}
>
{chartData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color}
stroke={colors.common.white}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
{/* Center content (Total count or Error indicator) */}
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
textAlign: "center",
pointerEvents: "none",
}}
>
{isError ? (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<ErrorIndicator entityName="active cases" />
<Typography variant="caption">Total</Typography>
</Box>
) : (
<>
<Typography variant="h4">
{data ? safeData.total : "N/A"}
</Typography>
<Typography variant="caption">Total</Typography>
</>
)}
<Pie
data={chartData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={0}
minAngle={15}
dataKey="value"
nameKey="name"
startAngle={90}
endAngle={-270}
label={false}
labelLine={false}
>
{chartData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color}
stroke={colors.common.white}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
{/* Center content (Total count or Error indicator) */}
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
textAlign: "center",
pointerEvents: "none",
}}
>
{isError ? (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<ErrorIndicator entityName="active cases" />
<Typography variant="caption">Total</Typography>
</Box>
) : (
<>
<Typography variant="h4">
{data ? safeData.total : "N/A"}
</Typography>
<Typography variant="caption">Total</Typography>
</>
)}
</Box>
</Box>
</Box>
)}
{/* Loading state */}
{isLoading ? (
Expand Down
Loading