diff --git a/apps/customer-portal/webapp/src/api/useGetProjectSupportStats.ts b/apps/customer-portal/webapp/src/api/useGetProjectSupportStats.ts index 47a1e63c46..fbe2f9c557 100644 --- a/apps/customer-portal/webapp/src/api/useGetProjectSupportStats.ts +++ b/apps/customer-portal/webapp/src/api/useGetProjectSupportStats.ts @@ -82,7 +82,14 @@ export function useGetProjectSupportStats( ); } - const data: ProjectSupportStats = await response.json(); + const raw = (await response.json()) as any; + const data: ProjectSupportStats = { + ongoingCases: raw?.ongoingCases ?? 0, + resolvedRecently: + raw?.resolvedRecently ?? raw?.sessionChats ?? 0, + resolvedChats: raw?.resolvedChats ?? 0, + activeChats: raw?.activeChats ?? 0, + }; logger.debug("[useGetProjectSupportStats] Data received:", data); return data; } catch (error) { diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx index 4227d1e48c..b52df26cb1 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx @@ -37,10 +37,7 @@ import { getStatusColor, mapSeverityToDisplay, } from "@utils/support"; -import { - getCaseTypeChipConfig, - getSeverityLegendColor, -} from "@constants/dashboardConstants"; +import { getSeverityLegendColor } from "@constants/dashboardConstants"; import ErrorIndicator from "@components/common/error-indicator/ErrorIndicator"; import CasesTableSkeleton from "@components/dashboard/cases-table/CasesTableSkeleton"; @@ -76,11 +73,10 @@ const CasesList = ({ Created - Engagement - Type + Details + Severity Assigned to Status - Severity @@ -88,7 +84,7 @@ const CasesList = ({ ) : isError ? ( - + ) : data?.cases.length === 0 ? ( - + No cases found. @@ -170,39 +166,24 @@ const CasesList = ({ {(() => { - const typeLabel = - row.type?.label ?? row.caseTypes?.label; - const config = getCaseTypeChipConfig(typeLabel ?? undefined); - if (!config) { - return ( - - -- - - ); - } - const { Icon } = config; - const mainColor = config.textColor; + const severityColor = getSeverityLegendColor( + row.severity?.label, + ); return ( } - label={config.displayLabel} + label={mapSeverityToDisplay(row.severity?.label)} size="small" variant="outlined" sx={{ + bgcolor: alpha(severityColor, 0.1), + color: severityColor, + borderColor: alpha(severityColor, 0.3), fontWeight: 500, - bgcolor: alpha(mainColor, 0.1), - color: mainColor, - borderColor: alpha(mainColor, 0.3), px: 0, height: 20, fontSize: "0.75rem", - "& .MuiChip-icon": { - color: "inherit", - ml: "6px", - mr: "6px", - }, "& .MuiChip-label": { - pl: 0, + pl: "6px", pr: "6px", }, }} @@ -237,33 +218,6 @@ const CasesList = ({ - - {(() => { - const severityColor = getSeverityLegendColor( - row.severity?.label, - ); - return ( - - ); - })()} - )) )} diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx index 616a5ceed4..6ee8b3dcb9 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx @@ -26,11 +26,7 @@ import type { FilterField } from "@components/common/filter-panel/FilterPopover" import CasesTableHeader from "@components/dashboard/cases-table/CasesTableHeader"; import CasesFilters from "@components/dashboard/cases-table/CasesFilters"; import CasesList from "@components/dashboard/cases-table/CasesList"; -import { - normalizeCaseTypeOptions, - mapSeverityToDisplay, - isS0Case, -} from "@utils/support"; +import { mapSeverityToDisplay, isS0Case } from "@utils/support"; import { isS0SeverityLabel } from "@constants/dashboardConstants"; import type { CaseListItem, CaseSearchResponse } from "@models/responses"; @@ -105,12 +101,6 @@ const CasesTable = ({ value: d.id, })) || [], }, - { - id: "caseTypes", - label: "Case Type", - type: "select", - options: normalizeCaseTypeOptions(filtersMetadata?.caseTypes || []), - }, ]; const caseSearchRequest = useMemo( @@ -121,7 +111,6 @@ const CasesTable = ({ severityId: filters.severityId ? Number(filters.severityId) : undefined, issueId: filters.issueTypes ? Number(filters.issueTypes) : undefined, deploymentId: filters.deploymentId || undefined, - caseTypes: filters.caseTypes?.length ? [filters.caseTypes] : undefined, }, sortBy: { field: "createdOn", diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTableHeader.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTableHeader.tsx index ebb68a546d..d0884174a6 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTableHeader.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTableHeader.tsx @@ -59,9 +59,9 @@ const CasesTableHeader = ({ > {/* Title and description */} - Outstanding Engagements + Outstanding Support Cases - Manage and track all your open engagements + Track and manage all active support tickets {/* Filter and Create buttons */} diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesList.test.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesList.test.tsx index 9cf9424dc7..2c999fc994 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesList.test.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesList.test.tsx @@ -158,33 +158,6 @@ describe("CasesList", () => { expect(screen.getByText("No cases found.")).toBeInTheDocument(); }); - it("should render Case chip when type is Incident", () => { - const dataWithType = { - cases: [ - { - ...mockData.cases[0], - type: { id: "1", label: "Incident" }, - }, - ], - totalRecords: 1, - offset: 0, - limit: 10, - } as typeof mockData; - - render( - , - ); - - expect(screen.getByText("Case")).toBeInTheDocument(); - }); - it("should render list of cases", () => { render( { it("should render title and buttons", () => { render(); - expect(screen.getByText("Outstanding Engagements")).toBeInTheDocument(); + expect(screen.getByText("Outstanding Support Cases")).toBeInTheDocument(); expect(screen.getByText("Create")).toBeInTheDocument(); expect(screen.getByText("Filters")).toBeInTheDocument(); }); diff --git a/apps/customer-portal/webapp/src/components/dashboard/charts/ActiveCasesChart.tsx b/apps/customer-portal/webapp/src/components/dashboard/charts/ActiveCasesChart.tsx index dedb738434..e6d2b5e63f 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/charts/ActiveCasesChart.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/charts/ActiveCasesChart.tsx @@ -28,12 +28,8 @@ import { ACTIVE_CASES_CHART_DATA } from "@constants/dashboardConstants"; interface ActiveCasesChartProps { data: { - open: number; - workInProgress: number; - awaitingInfo: number; - waitingOnWso2: number; - solutionProposed: number; - reopened: number; + serviceRequests: number; + changeRequests: number; total: number; }; isLoading?: boolean; @@ -53,12 +49,8 @@ export const ActiveCasesChart = ({ isError, }: ActiveCasesChartProps): JSX.Element => { const safeData = data ?? { - open: 0, - workInProgress: 0, - awaitingInfo: 0, - waitingOnWso2: 0, - solutionProposed: 0, - reopened: 0, + serviceRequests: 0, + changeRequests: 0, total: 0, }; @@ -80,7 +72,7 @@ export const ActiveCasesChart = ({ {/* Title */} - Active Engagements + Outstanding Operations {/* Chart state */} {isLoading ? ( diff --git a/apps/customer-portal/webapp/src/components/dashboard/charts/CasesTrendChart.tsx b/apps/customer-portal/webapp/src/components/dashboard/charts/CasesTrendChart.tsx index a9d24af49f..84513b23b2 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/charts/CasesTrendChart.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/charts/CasesTrendChart.tsx @@ -16,27 +16,21 @@ import { Card, Typography, Box, Skeleton, colors } from "@wso2/oxygen-ui"; import { - BarChart, - Bar, + PieChart, + Pie, + Cell, ResponsiveContainer, } from "@wso2/oxygen-ui-charts-react"; import type { JSX } from "react"; import ErrorIndicator from "@components/common/error-indicator/ErrorIndicator"; -import { CASES_TREND_CHART_DATA } from "@constants/dashboardConstants"; +import { + OUTSTANDING_ENGAGEMENTS_CATEGORY_CHART_DATA, +} from "@constants/dashboardConstants"; import { ChartLegend } from "@components/dashboard/charts/ChartLegend"; interface CasesTrendChartProps { - data: Array<{ - period: string; - critical: number; - high: number; - medium: number; - low: number; - catastrophic: number; - }>; isLoading?: boolean; isError?: boolean; - excludeS0?: boolean; } /** @@ -45,46 +39,56 @@ interface CasesTrendChartProps { * `@param` props - Component props */ export const CasesTrendChart = ({ - data, isLoading, isError, - excludeS0 = false, }: CasesTrendChartProps): JSX.Element => { - const chartData = isError ? [] : (data ?? []); - const trendChartItems = excludeS0 - ? CASES_TREND_CHART_DATA.filter((item) => item.key !== "catastrophic") - : CASES_TREND_CHART_DATA; + const chartSource = OUTSTANDING_ENGAGEMENTS_CATEGORY_CHART_DATA; + + // TODO(CasesTrendChart): Replace placeholder mock data with API-driven trend data. + const chartData = + isLoading || isError + ? chartSource.map((item) => ({ + name: item.name, + value: 0, + color: isError + ? colors.grey?.[300] ?? "#D1D5DB" + : item.color, + })) + : [ + { name: "Onboarding", value: 12, color: chartSource[0].color }, + { name: "Migration", value: 8, color: chartSource[1].color }, + { name: "Services", value: 15, color: chartSource[2].color }, + { name: "Improvements", value: 10, color: chartSource[3].color }, + ]; + + const total = !isError && !isLoading + ? chartData.reduce((sum, item) => sum + (item.value ?? 0), 0) + : 0; return ( {/* Title */} - Cases Trend + Outstanding Engagements {isLoading ? ( - - + + ) : ( - - {isError && ( - - - - )} + - - {trendChartItems.map((item) => ( - - ))} - + + + {chartData.map((entry, index) => ( + + ))} + + + {/* Center content: total value or error indicator */} + + {isError ? ( + + + Total + + ) : ( + <> + + {chartData.length > 0 ? total : "N/A"} + + Total + > + )} + )} - {/* Custom Trend Legend */} - {isLoading ? ( - - {trendChartItems.map((_, i) => ( - - ))} - - ) : ( + {/* Legend */} + {!isLoading && ( ({ + data={chartData.map((item) => ({ name: item.name, - value: 0, + value: item.value, color: item.color, }))} isError={isError} diff --git a/apps/customer-portal/webapp/src/components/dashboard/charts/ChartLayout.tsx b/apps/customer-portal/webapp/src/components/dashboard/charts/ChartLayout.tsx index 667f50896c..3cf2d84fb1 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/charts/ChartLayout.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/charts/ChartLayout.tsx @@ -27,27 +27,13 @@ interface ChartLayoutProps { high: number; critical: number; catastrophic: number; - serviceRequest: number; - securityReportAnalysis: number; total: number; }; activeCases: { - open: number; - workInProgress: number; - awaitingInfo: number; - waitingOnWso2: number; - solutionProposed: number; - reopened: number; + serviceRequests: number; + changeRequests: number; total: number; }; - casesTrend: Array<{ - period: string; - critical: number; - high: number; - medium: number; - low: number; - catastrophic: number; - }>; isLoading?: boolean; isErrorOutstanding?: boolean; isErrorActiveCases?: boolean; @@ -56,20 +42,18 @@ interface ChartLayoutProps { } /** - * ChartLayout component displays multiple chart sections including - * outstanding engagements (severities + case types), active cases, and cases trend. + * ChartLayout component displays the three dashboard charts: + * outstanding support cases, outstanding operations, and outstanding engagements. * * @param {ChartLayoutProps} props - Component props - * @param {Object} props.outstandingCases - Severity and case type counts for Outstanding Engagements chart. - * @param {Object} props.activeCases - State counts for Active Engagements chart. - * @param {Array} props.casesTrend - Array of trend data for Cases Trend chart. + * @param {Object} props.outstandingCases - Severity counts for Outstanding Support Cases chart. + * @param {Object} props.activeCases - Counts for Outstanding Operations chart. * @param {boolean} props.isLoading - Flag indicating if the data is loading. * @returns {JSX.Element} The chart layout element. */ const ChartLayout = ({ outstandingCases, activeCases, - casesTrend, isLoading, isErrorOutstanding, isErrorActiveCases, @@ -100,10 +84,8 @@ const ChartLayout = ({ {/* Cases Trend */} diff --git a/apps/customer-portal/webapp/src/components/dashboard/charts/OutstandingIncidentsChart.tsx b/apps/customer-portal/webapp/src/components/dashboard/charts/OutstandingIncidentsChart.tsx index cf74ce0a38..941bf5d03b 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/charts/OutstandingIncidentsChart.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/charts/OutstandingIncidentsChart.tsx @@ -24,7 +24,7 @@ import { import type { JSX } from "react"; import ErrorIndicator from "@components/common/error-indicator/ErrorIndicator"; import { ChartLegend } from "@components/dashboard/charts/ChartLegend"; -import { OUTSTANDING_ENGAGEMENTS_CHART_DATA } from "@constants/dashboardConstants"; +import { OUTSTANDING_INCIDENTS_CHART_DATA } from "@constants/dashboardConstants"; interface OutstandingIncidentsChartProps { data: { @@ -33,8 +33,6 @@ interface OutstandingIncidentsChartProps { high: number; critical: number; catastrophic: number; - serviceRequest: number; - securityReportAnalysis: number; total: number; }; isLoading?: boolean; @@ -62,16 +60,14 @@ export const OutstandingIncidentsChart = ({ high: 0, critical: 0, catastrophic: 0, - serviceRequest: 0, - securityReportAnalysis: 0, total: 0, }; const chartSource = excludeS0 - ? OUTSTANDING_ENGAGEMENTS_CHART_DATA.filter( + ? OUTSTANDING_INCIDENTS_CHART_DATA.filter( (item) => item.key !== "catastrophic", ) - : OUTSTANDING_ENGAGEMENTS_CHART_DATA; + : OUTSTANDING_INCIDENTS_CHART_DATA; const chartData = isError ? chartSource.map((item) => ({ @@ -91,7 +87,7 @@ export const OutstandingIncidentsChart = ({ {/* Title */} - Outstanding Engagements + Outstanding Support Cases {/* Chart state */} {isLoading ? ( diff --git a/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/ActiveCasesChart.test.tsx b/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/ActiveCasesChart.test.tsx index 31fa483871..1f89fb9396 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/ActiveCasesChart.test.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/ActiveCasesChart.test.tsx @@ -17,6 +17,7 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { ActiveCasesChart } from "@components/dashboard/charts/ActiveCasesChart"; +import { ACTIVE_CASES_CHART_DATA } from "@constants/dashboardConstants"; // Mock @wso2/oxygen-ui vi.mock("@wso2/oxygen-ui", () => ({ @@ -86,18 +87,14 @@ vi.mock("../ChartLegend", () => ({ describe("ActiveCasesChart", () => { const mockData = { - open: 5, - workInProgress: 10, - awaitingInfo: 3, - waitingOnWso2: 5, - solutionProposed: 0, - reopened: 0, - total: 23, + serviceRequests: 12, + changeRequests: 8, + total: 20, }; it("should render title correctly", () => { render(); - expect(screen.getByText("Active Engagements")).toBeInTheDocument(); + expect(screen.getByText("Outstanding Operations")).toBeInTheDocument(); }); it("should render skeleton when loading", () => { @@ -115,17 +112,15 @@ describe("ActiveCasesChart", () => { it("should render all chart segments even with missing data values", () => { const incompleteData = { - workInProgress: 10, - waitingOnClient: 5, - total: 15, + serviceRequests: 10, + total: 10, } as any; render(); - // Verify that all segments are still rendered (value will be undefined) + // Verify that all configured segments are still rendered const segments = screen.getAllByTestId("pie-segment"); - // We expect 6 segments because ACTIVE_CASES_CHART_DATA has 6 items - expect(segments).toHaveLength(6); + expect(segments).toHaveLength(ACTIVE_CASES_CHART_DATA.length); // Verify that the missing value was defaulted to 0 const values = segments.map((s) => s.getAttribute("data-value")); diff --git a/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/CasesTrendChart.test.tsx b/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/CasesTrendChart.test.tsx index f07575575d..3105c18d55 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/CasesTrendChart.test.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/CasesTrendChart.test.tsx @@ -44,10 +44,24 @@ vi.mock("@wso2/oxygen-ui", () => ({ // Mock @wso2/oxygen-ui-charts-react vi.mock("@wso2/oxygen-ui-charts-react", () => ({ - BarChart: ({ children }: any) => ( - {children} + PieChart: ({ children }: any) => ( + {children} ), - Bar: () => , + Pie: ({ data, paddingAngle, minAngle, children }: any) => ( + + {data.map((item: any, index: number) => ( + + {item.name} + + ))} + {children} + + ), + Cell: () => , ResponsiveContainer: ({ children }: any) => ( {children} ), @@ -64,45 +78,35 @@ vi.mock("../ChartLegend", () => ({ ), })); -describe("CasesTrendChart", () => { - const mockData = [ - { - period: "Jan", - critical: 10, - high: 20, - medium: 30, - low: 40, - catastrophic: 5, - }, - { - period: "Feb", - critical: 15, - high: 25, - medium: 35, - low: 45, - catastrophic: 0, - }, - ]; +vi.mock("@components/common/error-indicator/ErrorIndicator", () => ({ + __esModule: true, + default: ({ entityName }: { entityName: string }) => ( + Error: {entityName} + ), +})); +describe("CasesTrendChart", () => { it("should render title correctly", () => { - render(); - expect(screen.getByText(/Cases Trend/i)).toBeInTheDocument(); + render(); + expect(screen.getByText(/Outstanding Engagements/i)).toBeInTheDocument(); }); it("should render skeleton when loading", () => { - render(); + render(); const skeletons = screen.getAllByTestId("skeleton"); expect(skeletons.length).toBeGreaterThan(0); }); - it("should render chart and legend when data is loaded", () => { - render(); - expect(screen.getByTestId("bar-chart")).toBeInTheDocument(); + it("should render pie chart and legend when data is loaded", () => { + render(); + expect(screen.getByTestId("pie-chart")).toBeInTheDocument(); expect(screen.getByTestId("chart-legend")).toBeInTheDocument(); }); - it("should not crash and render empty chart when data is undefined", () => { - render(); - expect(screen.getByTestId("bar-chart")).toBeInTheDocument(); + it("should render error state correctly", () => { + render(); + + expect(screen.getByTestId("pie-chart")).toBeInTheDocument(); + expect(screen.getByTestId("error-indicator")).toBeInTheDocument(); }); }); diff --git a/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/ChartLayout.test.tsx b/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/ChartLayout.test.tsx index a7d36d0889..ea550f8001 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/ChartLayout.test.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/ChartLayout.test.tsx @@ -60,18 +60,12 @@ describe("ChartLayout", () => { high: 3, critical: 1, catastrophic: 0, - serviceRequest: 0, - securityReportAnalysis: 0, total: 11, }, activeCases: { - open: 5, - workInProgress: 10, - awaitingInfo: 3, - waitingOnWso2: 5, - solutionProposed: 0, - reopened: 0, - total: 23, + serviceRequests: 12, + changeRequests: 8, + total: 20, }, casesTrend: [ { diff --git a/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/OutstandingIncidentsChart.test.tsx b/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/OutstandingIncidentsChart.test.tsx index 4e3616c5c8..23278b2c8b 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/OutstandingIncidentsChart.test.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/charts/__tests__/OutstandingIncidentsChart.test.tsx @@ -85,14 +85,12 @@ describe("OutstandingIncidentsChart", () => { high: 3, critical: 1, catastrophic: 0, - serviceRequest: 1, - securityReportAnalysis: 2, - total: 14, + total: 11, }; it("should render title correctly", () => { render(); - expect(screen.getByText("Outstanding Engagements")).toBeInTheDocument(); + expect(screen.getByText("Outstanding Support Cases")).toBeInTheDocument(); }); it("should render skeleton when loading", () => { @@ -111,7 +109,7 @@ describe("OutstandingIncidentsChart", () => { it("should render all segments", () => { render(); const segments = screen.getAllByTestId("pie-segment"); - expect(segments.length).toBe(7); + expect(segments.length).toBe(5); }); it("should render all segments even with missing data values", () => { @@ -121,8 +119,6 @@ describe("OutstandingIncidentsChart", () => { high: 3, critical: 0, catastrophic: 0, - serviceRequest: 0, - securityReportAnalysis: 0, total: 8, }; @@ -131,7 +127,7 @@ describe("OutstandingIncidentsChart", () => { ); const segments = screen.getAllByTestId("pie-segment"); - expect(segments.length).toBe(7); + expect(segments.length).toBe(5); const values = segments.map((s) => s.getAttribute("data-value")); expect(values).toContain("0"); diff --git a/apps/customer-portal/webapp/src/components/support/cases-overview-stats/__tests__/CasesOverviewStatCard.test.tsx b/apps/customer-portal/webapp/src/components/support/cases-overview-stats/__tests__/CasesOverviewStatCard.test.tsx index fd6fe9c60b..6164183ef2 100644 --- a/apps/customer-portal/webapp/src/components/support/cases-overview-stats/__tests__/CasesOverviewStatCard.test.tsx +++ b/apps/customer-portal/webapp/src/components/support/cases-overview-stats/__tests__/CasesOverviewStatCard.test.tsx @@ -87,10 +87,10 @@ describe("CasesOverviewStatCard", () => { it("should render statistics correctly when data is loaded", () => { const mockStats = { - activeChats: 5, - resolvedChats: 20, - sessionChats: 15, ongoingCases: 10, + resolvedRecently: 15, + resolvedChats: 20, + activeChats: 5, }; render(); diff --git a/apps/customer-portal/webapp/src/constants/appLayoutConstants.ts b/apps/customer-portal/webapp/src/constants/appLayoutConstants.ts index abc9db547a..b8dffed349 100644 --- a/apps/customer-portal/webapp/src/constants/appLayoutConstants.ts +++ b/apps/customer-portal/webapp/src/constants/appLayoutConstants.ts @@ -16,12 +16,12 @@ import { Briefcase, + Cog, FolderOpen, Headset, LayoutDashboard, Megaphone, RefreshCw, - Settings, Shield, } from "@wso2/oxygen-ui-icons-react"; import type { ComponentType } from "react"; @@ -37,7 +37,7 @@ interface AppShellNavItem { export const APP_SHELL_NAV_ITEMS: AppShellNavItem[] = [ { id: "dashboard", label: "Dashboard", path: "dashboard", icon: LayoutDashboard }, { id: "support", label: "Support", path: "support", icon: Headset }, - { id: "operations", label: "Operations", path: "operations", icon: Settings }, + { id: "operations", label: "Operations", path: "operations", icon: Cog}, { id: "updates", label: "Updates", path: "updates", icon: RefreshCw }, { id: "security-center", diff --git a/apps/customer-portal/webapp/src/constants/dashboardConstants.ts b/apps/customer-portal/webapp/src/constants/dashboardConstants.ts index 06196fdee8..3275e4b9a0 100644 --- a/apps/customer-portal/webapp/src/constants/dashboardConstants.ts +++ b/apps/customer-portal/webapp/src/constants/dashboardConstants.ts @@ -41,7 +41,7 @@ export type StatCardColor = export interface StatConfigItem { id: Exclude; label: string; - icon: any; + icon: ComponentType<{ size?: number }>; iconColor: StatCardColor; tooltipText: string; } @@ -58,21 +58,21 @@ export const DASHBOARD_CASE_TYPE_LABELS = [ export const DASHBOARD_STATS: StatConfigItem[] = [ { id: "totalCases", - label: "Total Engagements", + label: "Total Interactions", icon: Clock, iconColor: "primary", tooltipText: "Total number of cases reported for this project", }, { id: "openCases", - label: "Active Engagements", + label: "Active Interactions", icon: AlertCircle, iconColor: "warning", tooltipText: "Currently active and unresolved cases", }, { id: "resolvedCases", - label: "Resolved This Month", + label: "Resolved support cases", icon: CheckCircle, iconColor: "success", tooltipText: "Successfully closed and resolved cases", @@ -86,14 +86,18 @@ export const DASHBOARD_STATS: StatConfigItem[] = [ }, ]; -// Configuration for Active Cases Chart data mapping (stateCount labels, exclude Closed). +// Configuration for Outstanding Operations Chart data mapping (Service Requests vs Change Requests). export const ACTIVE_CASES_CHART_DATA = [ - { name: "Open", key: "open", color: colors.blue[500] }, - { name: "Work In Progress", key: "workInProgress", color: colors.teal?.[600] ?? "#0D9488" }, - { name: "Awaiting Info", key: "awaitingInfo", color: colors.green[500] }, - { name: "Waiting On WSO2", key: "waitingOnWso2", color: colors.orange[500] }, - { name: "Solution Proposed", key: "solutionProposed", color: colors.grey?.[500] ?? "#9CA3AF" }, - { name: "Reopened", key: "reopened", color: colors.purple[500] }, + { + name: "Service Requests (SR)", + key: "serviceRequests", + color: colors.orange[500], + }, + { + name: "Change Requests (CR)", + key: "changeRequests", + color: colors.blue[500], + }, ] as const; /** Maps severity API label to display name (S0-S4) for charts and table. */ @@ -198,18 +202,17 @@ export function getSeverityLegendColor(label?: string): string { return entry?.color ?? colors.grey?.[500] ?? "#6B7280"; } -/** Case type entries for Outstanding Engagements chart (from caseTypeCount). */ -export const OUTSTANDING_CASE_TYPE_ENTRIES = [ - { key: "serviceRequest", label: "Service Request", displayName: "Service Request", color: colors.yellow[500] }, - { key: "securityReportAnalysis", label: "Security Report Analysis", displayName: "Security Report Analysis", color: colors.purple[500] }, +/** + * Static configuration for Outstanding Engagements category chart. + * Used for the Outstanding Engagements donut (Onboarding, Migration, Services, Improvements). + */ +export const OUTSTANDING_ENGAGEMENTS_CATEGORY_CHART_DATA = [ + { key: "onboarding", name: "Onboarding", color: colors.blue[500] }, + { key: "migration", name: "Migration", color: colors.orange[500] }, + { key: "services", name: "Services", color: colors.green[500] }, + { key: "improvements", name: "Improvements", color: colors.purple[500] }, ] as const; -/** Combined chart data: severities + Service Request + Security Report Analysis. */ -export const OUTSTANDING_ENGAGEMENTS_CHART_DATA = [ - ...OUTSTANDING_INCIDENTS_CHART_DATA, - ...OUTSTANDING_CASE_TYPE_ENTRIES, -]; - /** * Type definition for Cases Trend Chart data item. */ diff --git a/apps/customer-portal/webapp/src/constants/supportConstants.ts b/apps/customer-portal/webapp/src/constants/supportConstants.ts index 86232e5a81..b150a80987 100644 --- a/apps/customer-portal/webapp/src/constants/supportConstants.ts +++ b/apps/customer-portal/webapp/src/constants/supportConstants.ts @@ -45,6 +45,7 @@ import { ShieldCheck, UserCheck, CalendarCheck, + FileCheck, } from "@wso2/oxygen-ui-icons-react"; import { colors, alpha } from "@wso2/oxygen-ui"; import { type ComponentType } from "react"; @@ -275,10 +276,10 @@ export const SUPPORT_STAT_CONFIGS: SupportStatConfig[] = [ secondaryIcon: TrendingUp, }, { - icon: MessageSquare, + icon: FileCheck, iconColor: "success", - key: "sessionChats", - label: "Chat Sessions", + key: "resolvedRecently", + label: "Resolved Recently", secondaryIcon: Bot, }, { diff --git a/apps/customer-portal/webapp/src/layouts/AppLayout.tsx b/apps/customer-portal/webapp/src/layouts/AppLayout.tsx index 16edfcc783..deaa484ddf 100644 --- a/apps/customer-portal/webapp/src/layouts/AppLayout.tsx +++ b/apps/customer-portal/webapp/src/layouts/AppLayout.tsx @@ -17,7 +17,6 @@ import { notificationBannerConfig } from "@config/notificationBannerConfig"; import { AppShell, Box, useAppShell, LinearProgress } from "@wso2/oxygen-ui"; import { type JSX, type ReactNode, useRef, useEffect } from "react"; -import { useOldUrlRedirect } from "@hooks/useOldUrlRedirect"; import { useLoader } from "@context/linear-loader/LoaderContext"; import { useLocation, Outlet } from "react-router"; import IdleTimeoutProvider from "@providers/IdleTimeoutProvider"; @@ -42,8 +41,6 @@ export default function AppLayout({ children }: AppLayoutProps): JSX.Element { const location = useLocation(); const mainContentRef = useRef(null); - useOldUrlRedirect(); - useEffect(() => { if (mainContentRef.current) { mainContentRef.current.scrollTop = 0; diff --git a/apps/customer-portal/webapp/src/models/responses.ts b/apps/customer-portal/webapp/src/models/responses.ts index 9799c85905..d101763a8d 100644 --- a/apps/customer-portal/webapp/src/models/responses.ts +++ b/apps/customer-portal/webapp/src/models/responses.ts @@ -154,9 +154,9 @@ export interface CaseCreationMetadata { // Project support statistics. export interface ProjectSupportStats { ongoingCases: number; - activeChats: number; - sessionChats: number; + resolvedRecently: number; resolvedChats: number; + activeChats: number; } export interface CaseSeverity { diff --git a/apps/customer-portal/webapp/src/pages/DashboardPage.tsx b/apps/customer-portal/webapp/src/pages/DashboardPage.tsx index 9a3fa569e1..f7358bacf9 100644 --- a/apps/customer-portal/webapp/src/pages/DashboardPage.tsx +++ b/apps/customer-portal/webapp/src/pages/DashboardPage.tsx @@ -24,11 +24,7 @@ import { useErrorBanner } from "@context/error-banner/ErrorBannerContext"; import useGetProjectFilters from "@api/useGetProjectFilters"; import useGetProjectDetails from "@api/useGetProjectDetails"; import { useGetProjectCasesStats } from "@api/useGetProjectCasesStats"; -import { - DASHBOARD_STATS, - OUTSTANDING_ENGAGEMENTS_CHART_DATA, - SEVERITY_API_LABELS, -} from "@constants/dashboardConstants"; +import { DASHBOARD_STATS, SEVERITY_API_LABELS } from "@constants/dashboardConstants"; import { PROJECT_TYPE_LABELS } from "@constants/projectDetailsConstants"; import { getIncidentAndQueryIds } from "@utils/support"; import { StatCard } from "@components/dashboard/stats/StatCard"; @@ -124,61 +120,30 @@ export default function DashboardPage(): JSX.Element { } }, [isErrorCases, isErrorFilters, showError, logger, projectId]); - const activeCases = useMemo(() => { - const open = casesStats?.stateCount.find((s) => s.label === "Open")?.count ?? 0; - const workInProgress = casesStats?.stateCount.find((s) => s.label === "Work In Progress")?.count ?? 0; - const awaitingInfo = casesStats?.stateCount.find((s) => s.label === "Awaiting Info")?.count ?? 0; - const waitingOnWso2 = casesStats?.stateCount.find((s) => s.label === "Waiting On WSO2")?.count ?? 0; - const solutionProposed = casesStats?.stateCount.find((s) => s.label === "Solution Proposed")?.count ?? 0; - const reopened = casesStats?.stateCount.find((s) => s.label === "Reopened")?.count ?? 0; - const total = open + workInProgress + awaitingInfo + waitingOnWso2 + solutionProposed + reopened; - - return { - open, - workInProgress, - awaitingInfo, - waitingOnWso2, - solutionProposed, - reopened, - total, - }; - }, [casesStats]); - const outstandingCases = useMemo(() => { - const severityByKey: Record = {}; - for (const item of OUTSTANDING_ENGAGEMENTS_CHART_DATA) { - if (item.key === "serviceRequest" || item.key === "securityReportAnalysis") break; - severityByKey[item.key] = - casesStats?.outstandingSeverityCount.find((s) => s.label === item.label) - ?.count ?? 0; - } - const serviceRequest = - casesStats?.caseTypeCount.find( - (c) => /service\s*request/i.test(c.label), + const catastrophicCount = + casesStats?.outstandingSeverityCount.find( + (s) => s.label === SEVERITY_API_LABELS[0], )?.count ?? 0; - const securityReportAnalysis = - casesStats?.caseTypeCount.find( - (c) => /security\s*report\s*analysis/i.test(c.label), + const critical = + casesStats?.outstandingSeverityCount.find( + (s) => s.label === SEVERITY_API_LABELS[1], + )?.count ?? 0; + const high = + casesStats?.outstandingSeverityCount.find( + (s) => s.label === SEVERITY_API_LABELS[2], + )?.count ?? 0; + const medium = + casesStats?.outstandingSeverityCount.find( + (s) => s.label === SEVERITY_API_LABELS[3], + )?.count ?? 0; + const low = + casesStats?.outstandingSeverityCount.find( + (s) => s.label === SEVERITY_API_LABELS[4], )?.count ?? 0; - let catastrophic = severityByKey.catastrophic ?? 0; - const critical = severityByKey.critical ?? 0; - const high = severityByKey.high ?? 0; - const medium = severityByKey.medium ?? 0; - const low = severityByKey.low ?? 0; - - if (!isManagedCloudSubscription) { - catastrophic = 0; - } - - const total = - catastrophic + - critical + - high + - medium + - low + - serviceRequest + - securityReportAnalysis; + const catastrophic = isManagedCloudSubscription ? catastrophicCount : 0; + const total = catastrophic + critical + high + medium + low; return { catastrophic, @@ -186,33 +151,19 @@ export default function DashboardPage(): JSX.Element { high, medium, low, - serviceRequest, - securityReportAnalysis, total, }; }, [casesStats, isManagedCloudSubscription]); - const casesTrend = useMemo(() => { - const catastrophicCount = isManagedCloudSubscription - ? (s: { label: string; count?: number }[]) => - s.find((x) => x.label === SEVERITY_API_LABELS[0])?.count ?? 0 - : () => 0; - const mapped = (casesStats?.casesTrend ?? []).map(({ period, severities }) => ({ - period, - catastrophic: catastrophicCount(severities), - critical: severities.find((s) => s.label === SEVERITY_API_LABELS[1])?.count ?? 0, - high: severities.find((s) => s.label === SEVERITY_API_LABELS[2])?.count ?? 0, - medium: severities.find((s) => s.label === SEVERITY_API_LABELS[3])?.count ?? 0, - low: severities.find((s) => s.label === SEVERITY_API_LABELS[4])?.count ?? 0, - })); - return mapped.sort((a, b) => { - const parse = (p: string) => { - const m = p.match(/(\d{4})\D*[Qq](\d)/); - return m ? Number(m[1]) * 4 + Number(m[2]) : 0; - }; - return parse(a.period) - parse(b.period); - }); - }, [casesStats, isManagedCloudSubscription]); + // TODO(DashboardPage): Replace mock outstandingOperations counts with API-driven operations stats. + const outstandingOperations = useMemo( + () => ({ + serviceRequests: 12, + changeRequests: 8, + total: 20, + }), + [], + ); return ( @@ -285,8 +236,7 @@ export default function DashboardPage(): JSX.Element { {/* Charts row */} { data: { ongoingCases: 10, activeChats: 5, - sessionChats: 15, + resolvedRecently: 15, resolvedChats: 20, }, }); @@ -308,7 +308,7 @@ describe("SupportPage", () => { expect(screen.getByText("20")).toBeInTheDocument(); expect(screen.getByText("Ongoing Cases")).toBeInTheDocument(); expect(screen.getByText("Active Chats")).toBeInTheDocument(); - expect(screen.getByText("Chat Sessions")).toBeInTheDocument(); + expect(screen.getByText("Resolved Recently")).toBeInTheDocument(); expect(screen.getByText("Resolved via Chat")).toBeInTheDocument(); expect(mockLogger.debug).toHaveBeenCalledWith( "Support stats loaded for project: project-1", @@ -321,7 +321,7 @@ describe("SupportPage", () => { data: { ongoingCases: 10, activeChats: 5, - sessionChats: 15, + resolvedRecently: 15, resolvedChats: 20, }, }); @@ -345,7 +345,7 @@ describe("SupportPage", () => { data: { ongoingCases: 10, activeChats: 5, - sessionChats: 15, + resolvedRecently: 15, resolvedChats: 20, }, });