diff --git a/apps/customer-portal/webapp/src/App.tsx b/apps/customer-portal/webapp/src/App.tsx index e32793dead..750fc9bdac 100644 --- a/apps/customer-portal/webapp/src/App.tsx +++ b/apps/customer-portal/webapp/src/App.tsx @@ -26,7 +26,7 @@ import CreateCasePage from "@pages/CreateCasePage"; import AppLayout from "@layouts/AppLayout"; import { LoaderProvider } from "@context/linear-loader/LoaderContext"; import LoginPage from "@pages/LoginPage"; -import AuthGuard from "@/AuthGuard"; +import AuthGuard from "./AuthGuard"; export default function App(): JSX.Element { return ( diff --git a/apps/customer-portal/webapp/src/AppWithConfig.tsx b/apps/customer-portal/webapp/src/AppWithConfig.tsx index a78e0bc510..cfa630cf3e 100644 --- a/apps/customer-portal/webapp/src/AppWithConfig.tsx +++ b/apps/customer-portal/webapp/src/AppWithConfig.tsx @@ -18,7 +18,7 @@ import type { JSX } from "react"; import { OxygenUIThemeProvider } from "@wso2/oxygen-ui"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import App from "@/App"; +import App from "./App"; import { AsgardeoProvider } from "@asgardeo/react"; import { themeConfig } from "@config/themeConfig"; import { loggerConfig } from "@config/loggerConfig"; diff --git a/apps/customer-portal/webapp/src/__tests__/AuthGuard.test.tsx b/apps/customer-portal/webapp/src/__tests__/AuthGuard.test.tsx index d5fedcf8a7..d84ea68eb7 100644 --- a/apps/customer-portal/webapp/src/__tests__/AuthGuard.test.tsx +++ b/apps/customer-portal/webapp/src/__tests__/AuthGuard.test.tsx @@ -17,7 +17,7 @@ import { render, screen } from "@testing-library/react"; import { useAsgardeo } from "@asgardeo/react"; import { MemoryRouter, Route, Routes } from "react-router"; -import AuthGuard from "@AuthGuard"; +import AuthGuard from "../AuthGuard"; import { describe, it, expect, vi, beforeEach } from "vitest"; import React from "react"; import { useLoader } from "@context/linear-loader/LoaderContext"; diff --git a/apps/customer-portal/webapp/src/components/support/cases-overview-stats/CasesOverviewStatCard.tsx b/apps/customer-portal/webapp/src/components/support/cases-overview-stats/CasesOverviewStatCard.tsx index a119fd3e9b..7210f51e4b 100644 --- a/apps/customer-portal/webapp/src/components/support/cases-overview-stats/CasesOverviewStatCard.tsx +++ b/apps/customer-portal/webapp/src/components/support/cases-overview-stats/CasesOverviewStatCard.tsx @@ -36,13 +36,7 @@ export default function CasesOverviewStatCard({ }: CasesOverviewStatCardProps): JSX.Element { return ( - + {SUPPORT_STAT_CONFIGS.map((stat) => { const SecondaryIcon = stat.secondaryIcon; diff --git a/apps/customer-portal/webapp/src/components/support/novera-ai-assistant/novera-chat-banner/NoveraChatBanner.tsx b/apps/customer-portal/webapp/src/components/support/novera-ai-assistant/novera-chat-banner/NoveraChatBanner.tsx index f01df79b98..204861e612 100644 --- a/apps/customer-portal/webapp/src/components/support/novera-ai-assistant/novera-chat-banner/NoveraChatBanner.tsx +++ b/apps/customer-portal/webapp/src/components/support/novera-ai-assistant/novera-chat-banner/NoveraChatBanner.tsx @@ -30,7 +30,6 @@ export default function NoveraChatBanner(): JSX.Element { return ( void; +} + +/** + * Renders a list of chat history rows for the support overview card. + * + * @param {ChatHistoryListProps} props - Items and optional action handler. + * @returns {JSX.Element} The list of chat rows. + */ +export default function ChatHistoryList({ + items, + isLoading, + onItemAction, +}: ChatHistoryListProps): JSX.Element { + if (isLoading) { + return ; + } + + if (items.length === 0) { + return ( + + No chat history. + + ); + } + + return ( + + {items.map((item) => { + const action = getChatStatusAction(item.status); + const StatusIcon = action === ChatAction.VIEW ? CircleCheck : Clock; + const chipColorPath = getChatStatusColor(item.status); + + return ( + onItemAction?.(item.chatId, action)} + sx={{ + p: 2, + display: "flex", + flexDirection: "column", + alignItems: "stretch", + gap: 1, + cursor: "pointer", + "&:hover": { + bgcolor: "action.hover", + }, + }} + > + + + + + + + + {item.title} + + + + + + + {item.startedTime} + + + • + + + {item.messages}{" "} + {item.messages === 1 ? "message" : "messages"} + + + • + + + {item.kbArticles}{" "} + {item.kbArticles === 1 ? "KB article" : "KB articles"} + + + + + + + + alpha(resolveColorFromTheme(chipColorPath, theme), 0.1), + color: chipColorPath, + "& .MuiChip-icon": { + color: "inherit", + }, + }} + icon={} + /> + + + + ); + })} + + ); +} diff --git a/apps/customer-portal/webapp/src/components/support/support-overview-cards/ChatHistorySkeleton.tsx b/apps/customer-portal/webapp/src/components/support/support-overview-cards/ChatHistorySkeleton.tsx new file mode 100644 index 0000000000..053c8913d3 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/support-overview-cards/ChatHistorySkeleton.tsx @@ -0,0 +1,81 @@ +// 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 { Box, Form, Skeleton } from "@wso2/oxygen-ui"; +import { type JSX } from "react"; +import { SUPPORT_OVERVIEW_CHAT_LIMIT } from "@constants/supportConstants"; + +/** + * Renders a list of loading skeletons for the chat history list. + * + * @returns {JSX.Element} The skeleton list. + */ +export default function ChatHistorySkeleton(): JSX.Element { + return ( + + {Array.from({ length: SUPPORT_OVERVIEW_CHAT_LIMIT }).map((_, index) => ( + + + + + + + + + + + + + + + + + + + + + + + + ))} + + ); +} diff --git a/apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesList.tsx b/apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesList.tsx index fc069d3e51..ec9fcef3de 100644 --- a/apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesList.tsx +++ b/apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesList.tsx @@ -61,7 +61,9 @@ export default function OutstandingCasesList({ } return ( - + {cases.map((c) => { const colorPath = getStatusColor(c.status?.label); const resolvedColor = resolveColorFromTheme(colorPath, theme); @@ -105,15 +107,21 @@ export default function OutstandingCasesList({ /> - - {c.title} - + + + {c.title} + + @@ -126,7 +134,6 @@ export default function OutstandingCasesList({ px: 0.5, bgcolor: alpha(resolvedColor, 0.1), color: resolvedColor, - borderColor: "transparent", "& .MuiChip-icon": { color: "inherit", }, diff --git a/apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesSkeleton.tsx b/apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesSkeleton.tsx index 8e9c629771..b0db64feef 100644 --- a/apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesSkeleton.tsx +++ b/apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesSkeleton.tsx @@ -25,7 +25,9 @@ import { type JSX } from "react"; */ export default function OutstandingCasesSkeleton(): JSX.Element { return ( - + {Array.from({ length: SUPPORT_OVERVIEW_CASES_LIMIT }).map((_, index) => ( void; + sx?: SxProps; } /** @@ -51,6 +54,7 @@ export default function SupportOverviewCard({ children, footerButtonLabel, onFooterClick, + sx, }: SupportOverviewCardProps): JSX.Element { const theme = useTheme(); const paletteKey = iconVariant === "blue" ? "info" : "warning"; @@ -59,12 +63,17 @@ export default function SupportOverviewCard({ return ( - + {children} diff --git a/apps/customer-portal/webapp/src/components/support/support-overview-cards/__tests__/ChatHistoryList.test.tsx b/apps/customer-portal/webapp/src/components/support/support-overview-cards/__tests__/ChatHistoryList.test.tsx new file mode 100644 index 0000000000..cc28884eb2 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/support-overview-cards/__tests__/ChatHistoryList.test.tsx @@ -0,0 +1,80 @@ +// 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 { render, screen, fireEvent } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import ChatHistoryList from "@components/support/support-overview-cards/ChatHistoryList"; +import type { ChatHistoryItem } from "@models/responses"; + +const mockItems: ChatHistoryItem[] = [ + { + chatId: "1628192673", + title: "How do I configure custom claims in JWT tokens?", + startedTime: "2 hours ago", + messages: 8, + kbArticles: 3, + status: "Resolved", + }, + { + chatId: "1628192674", + title: "Getting error 401 when calling the API endpoint...", + startedTime: "1 day ago", + messages: 5, + kbArticles: 2, + status: "Still Open", + }, +]; + +describe("ChatHistoryList", () => { + it("should render empty state when no items", () => { + render(); + expect(screen.getByText("No chat history.")).toBeInTheDocument(); + }); + + it("should render chat title, meta and status", () => { + render(); + expect( + screen.getByText("How do I configure custom claims in JWT tokens?"), + ).toBeInTheDocument(); + expect( + screen.getByText("Getting error 401 when calling the API endpoint..."), + ).toBeInTheDocument(); + expect(screen.getByText("Resolved")).toBeInTheDocument(); + expect(screen.getByText("Still Open")).toBeInTheDocument(); + expect(screen.getByText("2 hours ago")).toBeInTheDocument(); + expect(screen.getByText(/8 messages/)).toBeInTheDocument(); + expect(screen.getByText(/3 KB articles/)).toBeInTheDocument(); + }); + + it("should show View for Resolved and Resume for Still Open", () => { + render(); + const viewButtons = screen.getAllByRole("button", { name: /view/i }); + const resumeButtons = screen.getAllByRole("button", { name: /resume/i }); + expect(viewButtons.length).toBeGreaterThanOrEqual(1); + expect(resumeButtons.length).toBeGreaterThanOrEqual(1); + }); + + it("should call onItemAction with chatId and action when View/Resume clicked", () => { + const onItemAction = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("button", { name: "View" })); + expect(onItemAction).toHaveBeenCalledWith("1628192673", "view"); + + fireEvent.click(screen.getByRole("button", { name: "Resume" })); + expect(onItemAction).toHaveBeenCalledWith("1628192674", "resume"); + }); +}); diff --git a/apps/customer-portal/webapp/src/constants/supportConstants.ts b/apps/customer-portal/webapp/src/constants/supportConstants.ts index 5a4d76dd27..64e3821cef 100644 --- a/apps/customer-portal/webapp/src/constants/supportConstants.ts +++ b/apps/customer-portal/webapp/src/constants/supportConstants.ts @@ -25,6 +25,23 @@ import { import { type ComponentType } from "react"; import type { ProjectSupportStats } from "@models/responses"; +// Chat actions for the history list. +export const ChatAction = { + VIEW: "view", + RESUME: "resume", +} as const; + +export type ChatAction = (typeof ChatAction)[keyof typeof ChatAction]; + +// Chat status types. +export const ChatStatus = { + RESOLVED: "Resolved", + STILL_OPEN: "Still Open", + ABANDONED: "Abandoned", +} as const; + +export type ChatStatus = (typeof ChatStatus)[keyof typeof ChatStatus]; + // Interface for support statistics card configuration. export interface SupportStatConfig { iconColor: "primary" | "secondary" | "success" | "error" | "info" | "warning"; @@ -67,6 +84,9 @@ export const SUPPORT_STAT_CONFIGS: SupportStatConfig[] = [ // Number of outstanding cases to show on support overview cards. export const SUPPORT_OVERVIEW_CASES_LIMIT = 5; +// Number of chat history items to show on support overview cards. +export const SUPPORT_OVERVIEW_CHAT_LIMIT = 5; + // Rich text editor constants export const RICH_TEXT_HISTORY_LIMIT = 50; export const RICH_TEXT_UNDO_DEBOUNCE_MS = 600; diff --git a/apps/customer-portal/webapp/src/main.tsx b/apps/customer-portal/webapp/src/main.tsx index 68d52a3689..4d9280f271 100644 --- a/apps/customer-portal/webapp/src/main.tsx +++ b/apps/customer-portal/webapp/src/main.tsx @@ -14,9 +14,9 @@ // specific language governing permissions and limitations // under the License. -import AppWithConfig from "@/AppWithConfig"; import React from "react"; import { createRoot } from "react-dom/client"; +import AppWithConfig from "./AppWithConfig"; createRoot(document.getElementById("root")!).render( diff --git a/apps/customer-portal/webapp/src/pages/SupportPage.tsx b/apps/customer-portal/webapp/src/pages/SupportPage.tsx index 0a38441b0c..ec24c2bba3 100644 --- a/apps/customer-portal/webapp/src/pages/SupportPage.tsx +++ b/apps/customer-portal/webapp/src/pages/SupportPage.tsx @@ -16,18 +16,21 @@ import { useParams, useNavigate } from "react-router"; import { useEffect, type JSX } from "react"; -import { Typography, Box, Grid } from "@wso2/oxygen-ui"; +import { Typography, Box, Grid, Stack } from "@wso2/oxygen-ui"; import { FileText, MessageSquare } from "@wso2/oxygen-ui-icons-react"; import { useAsgardeo } from "@asgardeo/react"; import CasesOverviewStatCard from "@components/support/cases-overview-stats/CasesOverviewStatCard"; import NoveraChatBanner from "@components/support/novera-ai-assistant/novera-chat-banner/NoveraChatBanner"; import SupportOverviewCard from "@components/support/support-overview-cards/SupportOverviewCard"; import OutstandingCasesList from "@components/support/support-overview-cards/OutstandingCasesList"; +import ChatHistoryList from "@components/support/support-overview-cards/ChatHistoryList"; import { useGetProjectSupportStats } from "@api/useGetProjectSupportStats"; import useGetProjectCases from "@api/useGetProjectCases"; +import { useGetChatHistory } from "@api/useGetChatHistory"; import { useLogger } from "@hooks/useLogger"; import { SUPPORT_OVERVIEW_CASES_LIMIT, + SUPPORT_OVERVIEW_CHAT_LIMIT, } from "@constants/supportConstants"; /** @@ -53,9 +56,16 @@ export default function SupportPage(): JSX.Element { sortBy: { field: "createdOn", order: "desc" }, }, ); + + const { data: chatHistoryData, isFetching: isChatLoading } = + useGetChatHistory(projectId || ""); + const { isLoading: isAuthLoading } = useAsgardeo(); const cases = casesData?.cases ?? []; + const chatItems = + chatHistoryData?.chatHistory?.slice(0, SUPPORT_OVERVIEW_CHAT_LIMIT) ?? []; + const isActuallyLoading = isAuthLoading || isFetching || (!stats && !isError); useEffect(() => { @@ -81,14 +91,14 @@ export default function SupportPage(): JSX.Element { } return ( - + - - + + + + navigate("chat")} + > + { + navigate("chat", { state: { chatId } }); + }} + /> + + - + ); } diff --git a/apps/customer-portal/webapp/src/pages/__tests__/SupportPage.test.tsx b/apps/customer-portal/webapp/src/pages/__tests__/SupportPage.test.tsx index 3a5edde019..515d5041ef 100644 --- a/apps/customer-portal/webapp/src/pages/__tests__/SupportPage.test.tsx +++ b/apps/customer-portal/webapp/src/pages/__tests__/SupportPage.test.tsx @@ -14,6 +14,7 @@ // specific language governing permissions and limitations // under the License. +import type { ReactElement } from "react"; import { render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -29,6 +30,11 @@ vi.mock("react-router", async () => { }; }); +// Mock @asgardeo/react to avoid ESM buffer import errors in vitest +vi.mock("@asgardeo/react", () => ({ + useAsgardeo: () => ({ isLoading: false, isSignedIn: true }), +})); + // Mock useLogger const mockLogger = { debug: vi.fn(), @@ -51,6 +57,11 @@ vi.mock("@asgardeo/react", () => ({ // Mock @wso2/oxygen-ui components vi.mock("@wso2/oxygen-ui", () => ({ Box: ({ children }: any) =>
{children}
, + Stack: ({ children, spacing }: any) => ( +
+ {children} +
+ ), Grid: ({ children, container, spacing, size, sx }: any) => (
({
), Paper: ({ children }: any) =>
{children}
, + alpha: (color: string, opacity: number) => `alpha(${color}, ${opacity})`, + useTheme: () => ({ + palette: { + primary: { main: "#0070F3" }, + secondary: { main: "#71717A" }, + error: { main: "#EF4444" }, + warning: { main: "#F59E0B" }, + info: { main: "#3B82F6" }, + success: { main: "#10B981" }, + text: { primary: "#000000", secondary: "#6B7280" }, + }, + }), })); // Mock icons @@ -126,6 +149,48 @@ vi.mock("@api/useGetProjectSupportStats", () => ({ useGetProjectSupportStats: (id: string) => mockUseGetProjectSupportStats(id), })); +// Mock useGetProjectCases (avoids pulling in useAsgardeo) +vi.mock("@api/useGetProjectCases", () => ({ + __esModule: true, + default: () => ({ data: { cases: [] }, isLoading: false }), +})); + +// Mock useGetChatHistory +vi.mock("@api/useGetChatHistory", () => ({ + useGetChatHistory: () => ({ data: { chatHistory: [] } }), +})); + +// Mock support overview card components so SupportPage renders without full Oxygen UI tree +vi.mock( + "@components/support/support-overview-cards/SupportOverviewCard", + () => ({ + __esModule: true, + default: ({ + title, + children, + }: { + title: string; + children: ReactElement; + }) => ( +
+ {title} + {children} +
+ ), + }), +); +vi.mock( + "@components/support/support-overview-cards/OutstandingCasesList", + () => ({ + __esModule: true, + default: () =>
Cases list
, + }), +); +vi.mock("@components/support/support-overview-cards/ChatHistoryList", () => ({ + __esModule: true, + default: () =>
Chat list
, +})); + beforeEach(() => { vi.clearAllMocks(); }); @@ -226,4 +291,25 @@ describe("SupportPage", () => { expect(screen.getByText("Start New Chat")).toBeInTheDocument(); expect(screen.getAllByTestId("icon-bot")).toHaveLength(2); }); + + it("should render Outstanding Cases and Chat History overview cards when data is loaded", () => { + mockUseGetProjectSupportStats.mockReturnValue({ + isLoading: false, + data: { + totalCases: 10, + activeChats: 5, + sessionChats: 15, + resolvedChats: 20, + }, + }); + + render( + + + , + ); + + expect(screen.getByText("Outstanding Cases")).toBeInTheDocument(); + expect(screen.getByText("Chat History")).toBeInTheDocument(); + }); }); diff --git a/apps/customer-portal/webapp/src/utils/__tests__/support.test.ts b/apps/customer-portal/webapp/src/utils/__tests__/support.test.ts new file mode 100644 index 0000000000..eb36c393fd --- /dev/null +++ b/apps/customer-portal/webapp/src/utils/__tests__/support.test.ts @@ -0,0 +1,97 @@ +// 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 { describe, expect, it } from "vitest"; +import { ChatAction, ChatStatus } from "@constants/supportConstants"; +import { + getChatStatusAction, + getChatStatusColor, + getChatActionColor, + formatRelativeTime, +} from "@utils/support"; + +describe("support utils", () => { + describe("getChatStatusAction", () => { + it("should return resume for Still Open", () => { + expect(getChatStatusAction(ChatStatus.STILL_OPEN)).toBe( + ChatAction.RESUME, + ); + expect(getChatStatusAction(ChatStatus.STILL_OPEN.toLowerCase())).toBe( + ChatAction.RESUME, + ); + }); + + it("should return view for Resolved and Abandoned", () => { + expect(getChatStatusAction(ChatStatus.RESOLVED)).toBe(ChatAction.VIEW); + expect(getChatStatusAction(ChatStatus.ABANDONED)).toBe(ChatAction.VIEW); + expect(getChatStatusAction("")).toBe(ChatAction.VIEW); + }); + }); + + describe("getChatActionColor", () => { + it("should return info for RESUME", () => { + expect(getChatActionColor(ChatAction.RESUME)).toBe("info"); + }); + + it("should return primary for VIEW", () => { + expect(getChatActionColor(ChatAction.VIEW)).toBe("primary"); + }); + }); + + describe("getChatStatusColor", () => { + it("should return success.main for Resolved", () => { + expect(getChatStatusColor(ChatStatus.RESOLVED)).toBe("success.main"); + }); + + it("should return info.main for Still Open", () => { + expect(getChatStatusColor(ChatStatus.STILL_OPEN)).toBe("info.main"); + }); + + it("should return error.main for Abandoned", () => { + expect(getChatStatusColor(ChatStatus.ABANDONED)).toBe("error.main"); + }); + + it("should return secondary.main for others", () => { + expect(getChatStatusColor("")).toBe("secondary.main"); + }); + }); + + describe("formatRelativeTime", () => { + it("should return '--' for undefined date", () => { + expect(formatRelativeTime(undefined)).toBe("--"); + }); + + it("should return 'just now' for very recent dates", () => { + const now = new Date(); + expect(formatRelativeTime(now)).toBe("just now"); + }); + + it("should format minutes ago", () => { + const fiveMinsAgo = new Date(Date.now() - 5 * 60 * 1000); + expect(formatRelativeTime(fiveMinsAgo)).toBe("5 minutes ago"); + }); + + it("should format hours ago", () => { + const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000); + expect(formatRelativeTime(twoHoursAgo)).toBe("2 hours ago"); + }); + + it("should format days ago", () => { + const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); + expect(formatRelativeTime(threeDaysAgo)).toBe("3 days ago"); + }); + }); +}); diff --git a/apps/customer-portal/webapp/src/utils/support.ts b/apps/customer-portal/webapp/src/utils/support.ts index 3cb02b09fd..77d46fcc24 100644 --- a/apps/customer-portal/webapp/src/utils/support.ts +++ b/apps/customer-portal/webapp/src/utils/support.ts @@ -14,11 +14,71 @@ // specific language governing permissions and limitations // under the License. -import { colors, type SxProps, type Theme } from "@wso2/oxygen-ui"; +import { ChatAction, ChatStatus } from "@constants/supportConstants"; +import type { Theme } from "@wso2/oxygen-ui"; + +export type ChatActionState = + | "primary" + | "info" + | "success" + | "warning" + | "error"; + +/** + * Returns whether to show View or Resume for a chat status. + * + * @param status - Chat status string (e.g. Resolved, Still Open, Abandoned). + * @returns {ChatActionType} "view" or "resume". + */ +export function getChatStatusAction(status: string): ChatAction { + const normalized = status?.toLowerCase() || ""; + if (normalized.includes("open")) return ChatAction.RESUME; + return ChatAction.VIEW; +} + +/** + * Returns the color for a chat action button. + * + * @param action - The action type ("view" or "resume"). + * @returns {ChatActionState} Palette color path. + */ +export function getChatActionColor(action: ChatAction): ChatActionState { + if (action === ChatAction.RESUME) { + return "info"; + } + return "primary"; +} + +/** + * Returns the color path for a chat status. + * + * @param status - Chat status string. + * @returns {string} Palette color path. + */ +/** + * Returns the color for a chat status chip. + * + * @param status - Chat status string (e.g. Resolved, Still Open, Abandoned). + * @returns {string} Palette color path. + */ +export function getChatStatusColor(status: string): string { + const normalized = status?.toLowerCase() || ""; + if (normalized.includes(ChatStatus.RESOLVED.toLowerCase())) { + return "success.main"; + } + if (normalized.includes(ChatStatus.STILL_OPEN.toLowerCase())) { + return "info.main"; + } + if (normalized.includes(ChatStatus.ABANDONED.toLowerCase())) { + return "error.main"; + } + return "secondary.main"; +} + /** * Resolves a color from the theme palette for the alpha() utility. * - * @param path - Color path (e.g., 'primary.main'). + * @param path - Color path. * @param theme - Oxygen UI theme. * @returns {string} The resolved color string. */ @@ -37,7 +97,7 @@ export function resolveColorFromTheme(path: string, theme: Theme): string { * Formats a date string or Date object into a relative time (e.g., "2 days ago", "1 hour ago"). * * @param date - Date string or Date object. - * @returns {string} Human-readable relative time. + * @returns {string} Human readable relative time. */ export function formatRelativeTime(date: string | Date | undefined): string { if (!date) return "--";