diff --git a/apps/customer-portal/webapp/src/App.tsx b/apps/customer-portal/webapp/src/App.tsx
index ca2a592962..9a8c16498e 100644
--- a/apps/customer-portal/webapp/src/App.tsx
+++ b/apps/customer-portal/webapp/src/App.tsx
@@ -49,7 +49,10 @@ import SettingsPage from "@features/settings/pages/SettingsPage";
import ServiceNowCaseRedirectPage from "@features/project-hub/pages/ServiceNowCaseRedirectPage";
import PartnerProjectsPage from "@features/project-hub/pages/PartnerProjectsPage";
import PartnerCasesPage from "@features/project-hub/pages/PartnerCasesPage";
+import UserProjectsPage from "@features/project-hub/pages/UserProjectsPage";
+import UserCasesPage from "@features/project-hub/pages/UserCasesPage";
import PartnerGuard from "@layouts/PartnerGuard";
+import NonPartnerGuard from "@layouts/NonPartnerGuard";
import Error401Page from "@components/error/Error401Page";
import Error403Page from "@components/error/Error403Page";
import Error404Page from "@components/error/Error404Page";
@@ -108,6 +111,12 @@ export default function App(): JSX.Element {
} />
+ {/* Home overview drill-down pages for non-partner users with multiple projects */}
+ }>
+ } />
+ } />
+
+
{/* Project Specific Routes */}
}>
{/* Dashboard */}
diff --git a/apps/customer-portal/webapp/src/features/project-hub/components/UserGlobalSearch.tsx b/apps/customer-portal/webapp/src/features/project-hub/components/UserGlobalSearch.tsx
new file mode 100644
index 0000000000..001b57c891
--- /dev/null
+++ b/apps/customer-portal/webapp/src/features/project-hub/components/UserGlobalSearch.tsx
@@ -0,0 +1,666 @@
+// 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 {
+ alpha,
+ Box,
+ Button,
+ Chip,
+ CircularProgress,
+ IconButton,
+ InputAdornment,
+ Menu,
+ MenuItem,
+ Paper,
+ Skeleton,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ TextField,
+ Typography,
+} from "@wso2/oxygen-ui";
+import { ChevronDown, Download, FileText, FolderOpen, Search, X } from "@wso2/oxygen-ui-icons-react";
+import { type JSX, type MouseEvent, useCallback, useRef, useState } from "react";
+import { useNavigate } from "react-router";
+import { useGetGlobalSearch } from "@api/useGetGlobalSearch";
+import { useDebouncedValue } from "@hooks/useDebouncedValue";
+import { useAuthApiClient } from "@/hooks/useAuthApiClient";
+import { useErrorBanner } from "@context/error-banner/ErrorBannerContext";
+import { PROJECT_HUB_SEARCH_DEBOUNCE_MS } from "@features/project-hub/constants/projectHubConstants";
+import {
+ downloadProjectListCsv,
+ downloadProjectListPdf,
+ fetchAllProjectsForExport,
+} from "@features/project-hub/utils/projectsExport";
+import {
+ downloadCaseListCsv,
+ downloadCaseListPdf,
+ fetchAllCasesForExport,
+} from "@features/project-hub/utils/casesExport";
+import { getSeverityLegendColor } from "@features/dashboard/utils/dashboard";
+import { formatCasesTableCaseIdentifier, getStatusColor } from "@features/dashboard/utils/casesTable";
+import { mapSeverityToDisplay } from "@features/support/utils/support";
+import type { GlobalSearchCase, GlobalSearchProject } from "@features/project-hub/types/globalSearch";
+import { getCaseNavigationPath, getCaseTypeChipProps } from "@features/project-hub/utils/globalSearchNavigation";
+
+type ExportFormat = "csv" | "pdf";
+
+const SUMMARY_PAGE_SIZE = 5;
+const SKELETON_ROW_COUNT = 5;
+
+function SkeletonRows({ cols }: { cols: number }): JSX.Element {
+ return (
+ <>
+ {Array.from({ length: SKELETON_ROW_COUNT }).map((_, i) => (
+
+ {Array.from({ length: cols }).map((_, j) => (
+
+
+
+ ))}
+
+ ))}
+ >
+ );
+}
+
+/**
+ * Home overview for users with more than one project.
+ * Same layout as the partner global search page — Projects and Cases table
+ * sections with export — but the Projects table shows Action Required,
+ * Outstanding and Active Chats counts instead of Start/End Date.
+ */
+export default function UserGlobalSearch(): JSX.Element {
+ const navigate = useNavigate();
+ const authFetch = useAuthApiClient();
+ const { showError } = useErrorBanner();
+ const [searchQuery, setSearchQuery] = useState("");
+ const debouncedSearchQuery = useDebouncedValue(searchQuery, PROJECT_HUB_SEARCH_DEBOUNCE_MS);
+
+ const [exportingFormat, setExportingFormat] = useState(null);
+ const isExportingRef = useRef(false);
+ const [exportAnchorEl, setExportAnchorEl] = useState(null);
+
+ const handleExportOpen = (e: MouseEvent) => {
+ if (!isExportingRef.current) setExportAnchorEl(e.currentTarget);
+ };
+ const handleExportClose = () => setExportAnchorEl(null);
+
+ const handleExport = useCallback(
+ async (format: ExportFormat) => {
+ if (isExportingRef.current) return;
+ handleExportClose();
+ isExportingRef.current = true;
+ setExportingFormat(format);
+ try {
+ const allProjects = await fetchAllProjectsForExport(authFetch, debouncedSearchQuery);
+ if (allProjects.length === 0) {
+ showError("No projects to export.");
+ return;
+ }
+ if (format === "csv") {
+ downloadProjectListCsv(allProjects);
+ } else {
+ downloadProjectListPdf(allProjects);
+ }
+ } catch {
+ showError("Failed to export projects.");
+ } finally {
+ isExportingRef.current = false;
+ setExportingFormat(null);
+ }
+ },
+ [authFetch, debouncedSearchQuery, showError],
+ );
+
+ const isExporting = exportingFormat !== null;
+
+ const [exportingCasesFormat, setExportingCasesFormat] = useState(null);
+ const isExportingCasesRef = useRef(false);
+ const [exportCasesAnchorEl, setExportCasesAnchorEl] = useState(null);
+
+ const handleCasesExportOpen = (e: MouseEvent) => {
+ if (!isExportingCasesRef.current) setExportCasesAnchorEl(e.currentTarget);
+ };
+ const handleCasesExportClose = () => setExportCasesAnchorEl(null);
+
+ const handleCasesExport = useCallback(
+ async (format: ExportFormat) => {
+ if (isExportingCasesRef.current) return;
+ handleCasesExportClose();
+ isExportingCasesRef.current = true;
+ setExportingCasesFormat(format);
+ try {
+ const allCases = await fetchAllCasesForExport(authFetch, debouncedSearchQuery);
+ if (allCases.length === 0) {
+ showError("No cases to export.");
+ return;
+ }
+ if (format === "csv") {
+ downloadCaseListCsv(allCases);
+ } else {
+ downloadCaseListPdf(allCases);
+ }
+ } catch {
+ showError("Failed to export cases.");
+ } finally {
+ isExportingCasesRef.current = false;
+ setExportingCasesFormat(null);
+ }
+ },
+ [authFetch, debouncedSearchQuery, showError],
+ );
+
+ const isExportingCases = exportingCasesFormat !== null;
+
+ // Both projects and cases come from the unified /search endpoint, same as
+ // the partner view — GlobalSearchProject includes the action required/
+ // outstanding/active chats counts this table needs.
+ const {
+ data: searchData,
+ isLoading: isLoadingSearch,
+ isError: isErrorSearch,
+ } = useGetGlobalSearch({
+ ...(debouncedSearchQuery ? { filters: { searchQuery: debouncedSearchQuery } } : {}),
+ projectsPagination: { offset: 0, limit: SUMMARY_PAGE_SIZE },
+ casesPagination: { offset: 0, limit: SUMMARY_PAGE_SIZE },
+ });
+
+ const isLoadingProjects = isLoadingSearch;
+ const isErrorProjects = isErrorSearch;
+ const projects: GlobalSearchProject[] = searchData?.projects ?? [];
+ const projectsTotal = searchData?.projectsTotal ?? 0;
+
+ const isLoadingCases = isLoadingSearch;
+ const isErrorCases = isErrorSearch;
+ const cases: GlobalSearchCase[] = searchData?.cases ?? [];
+ const casesTotal = searchData?.casesTotal ?? 0;
+
+ const projectsMoreCount = Math.max(0, projectsTotal - projects.length);
+ const casesMoreCount = Math.max(0, casesTotal - cases.length);
+
+ return (
+
+ {/* Page header + search bar */}
+
+
+
+ setSearchQuery("")}
+ size="small"
+ >
+
+
+
+ ) : undefined,
+ startAdornment: ,
+ }}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ placeholder="Search projects and cases..."
+ size="small"
+ sx={{
+ "& input:-webkit-autofill, & input:-webkit-autofill:focus, & input:-webkit-autofill:hover":
+ {
+ WebkitBoxShadow: "0 0 0 100px transparent inset",
+ WebkitTextFillColor: "inherit",
+ transition: "background-color 5000s ease-in-out 0s",
+ },
+ }}
+ value={searchQuery}
+ />
+ {debouncedSearchQuery.trim() && !isLoadingProjects && !isLoadingCases && (
+
+ Showing results for “{debouncedSearchQuery.trim()}”
+
+ )}
+
+
+
+ {/* Scrollable content: projects + cases sections */}
+
+ {/* Projects section */}
+
+
+
+
+
+ Projects
+ {!isLoadingProjects && (
+
+ {" "}
+ ({projectsTotal})
+
+ )}
+
+
+ }
+ onClick={handleExportOpen}
+ size="small"
+ startIcon={
+ isExporting ? (
+
+ ) : (
+
+ )
+ }
+ type="button"
+ variant="outlined"
+ >
+ {isExporting ? "Exporting..." : "Export"}
+
+
+
+
+
+
+
+
+ Project Key
+ Name
+ Status
+ Action Required
+ Outstanding
+ Active Chats
+
+
+
+ {isLoadingProjects ? (
+
+ ) : isErrorProjects ? (
+
+
+
+ Failed to load projects.
+
+
+
+ ) : projects.length === 0 ? (
+
+
+
+ No projects found.
+
+
+
+ ) : (
+ projects.map((project) => {
+ const isSuspended =
+ project.closureState?.toLowerCase() === "suspended";
+ return (
+ navigate(`/projects/${project.id}/dashboard`)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ navigate(`/projects/${project.id}/dashboard`);
+ }
+ }}
+ sx={{ cursor: "pointer" }}
+ tabIndex={0}
+ >
+
+
+ {project.key}
+
+
+
+ {project.name}
+
+
+
+
+
+
+ {project.actionRequiredCount ?? 0}
+
+
+
+
+ {project.outstandingCount ?? 0}
+
+
+
+
+ {project.activeChatsCount ?? 0}
+
+
+
+ );
+ })
+ )}
+
+
+
+
+ {projectsMoreCount > 0 && (
+
+ {
+ const params = debouncedSearchQuery
+ ? `?q=${encodeURIComponent(debouncedSearchQuery)}`
+ : "";
+ navigate(`/projects${params}`);
+ }}
+ size="small"
+ variant="text"
+ >
+ {`View More (${projectsMoreCount} more)`}
+
+
+ )}
+
+
+ {/* Cases section */}
+
+
+
+
+
+ Cases
+ {!isLoadingCases && (
+
+ {" "}
+ ({casesTotal})
+
+ )}
+
+
+ }
+ onClick={handleCasesExportOpen}
+ size="small"
+ startIcon={
+ isExportingCases ? (
+
+ ) : (
+
+ )
+ }
+ type="button"
+ variant="outlined"
+ >
+ {isExportingCases ? "Exporting..." : "Export"}
+
+
+
+
+
+
+
+
+ Details
+ Case Type
+ Severity
+ Status
+ Created by
+ Created on
+ Project
+
+
+
+ {isLoadingCases ? (
+
+ ) : isErrorCases ? (
+
+
+
+ Failed to load cases.
+
+
+
+ ) : cases.length === 0 ? (
+
+
+
+ No cases found.
+
+
+
+ ) : (
+ cases.map((c) => {
+ const severityLabel = c.severity?.label;
+ const severityDisplay = mapSeverityToDisplay(severityLabel);
+ const severityColor = getSeverityLegendColor(severityLabel);
+ const { color: caseTypeColor, displayLabel: caseTypeLabel } = getCaseTypeChipProps(c.caseType?.label);
+ const casePath = getCaseNavigationPath(c);
+ const handleNavigate = casePath ? () => navigate(casePath, { state: { returnTo: "/" } }) : undefined;
+ return (
+ {
+ if (handleNavigate && (e.key === "Enter" || e.key === " ")) {
+ e.preventDefault();
+ handleNavigate();
+ }
+ }}
+ sx={{ cursor: casePath ? "pointer" : "default" }}
+ tabIndex={casePath ? 0 : undefined}
+ >
+
+
+
+ {c.title ?? "--"}
+
+
+ {formatCasesTableCaseIdentifier(c.number, c.internalId)}
+
+
+
+
+ ({
+ bgcolor: alpha(caseTypeColor, theme.palette.mode === "dark" ? 0.05 : 0.1),
+ borderColor: alpha(caseTypeColor, theme.palette.mode === "dark" ? 0.18 : 0.3),
+ color: caseTypeColor,
+ fontSize: "0.75rem",
+ fontWeight: 500,
+ height: 20,
+ px: 0,
+ "& .MuiChip-label": { pl: "6px", pr: "6px" },
+ })}
+ />
+
+
+ {severityLabel ? (
+
+ ) : (
+
+ --
+
+ )}
+
+
+
+
+
+ {c.state?.label ?? "--"}
+
+
+
+
+
+ {c.createdBy ?? "--"}
+
+
+
+
+ {formatDateShort(c.createdOn)}
+
+
+
+
+ {c.project?.label ?? "--"}
+
+
+
+ );
+ })
+ )}
+
+
+
+
+ {casesMoreCount > 0 && (
+
+ {
+ const params = debouncedSearchQuery
+ ? `?q=${encodeURIComponent(debouncedSearchQuery)}`
+ : "";
+ navigate(`/cases${params}`);
+ }}
+ size="small"
+ variant="text"
+ >
+ {`View More (${casesMoreCount} more)`}
+
+
+ )}
+
+
+
+ );
+}
+
+const DATE_LOCALE = "en-US";
+const DATE_FORMAT_OPTIONS: Intl.DateTimeFormatOptions = {
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+};
+
+function formatDateShort(value: string | null | undefined): string {
+ if (!value) return "--";
+ const d = new Date(value);
+ return isNaN(d.getTime()) ? "--" : d.toLocaleDateString(DATE_LOCALE, DATE_FORMAT_OPTIONS);
+}
diff --git a/apps/customer-portal/webapp/src/features/project-hub/pages/ProjectHub.tsx b/apps/customer-portal/webapp/src/features/project-hub/pages/ProjectHub.tsx
index 6f1a456cb7..e2da7440bd 100644
--- a/apps/customer-portal/webapp/src/features/project-hub/pages/ProjectHub.tsx
+++ b/apps/customer-portal/webapp/src/features/project-hub/pages/ProjectHub.tsx
@@ -39,6 +39,7 @@ import { useDebouncedValue } from "@hooks/useDebouncedValue";
import ProjectCard from "@features/project-hub/components/ProjectCard";
import ProjectCardSkeleton from "@features/project-hub/components/project-card/ProjectCardSkeleton";
import PartnerGlobalSearch from "@features/project-hub/components/PartnerGlobalSearch";
+import UserGlobalSearch from "@features/project-hub/components/UserGlobalSearch";
import useGetUserDetails from "@features/settings/api/useGetUserDetails";
import { hasPartnerAccess } from "@features/settings/constants/settingsConstants";
import { clearLastSelectedProject } from "@features/settings/utils/settingsStorage";
@@ -265,6 +266,22 @@ export default function ProjectHub(): JSX.Element {
return ;
}
+ // Non-partner users with more than one project see the same table-based
+ // overview as partners, with per-project stat counts instead of dates.
+ const showUserGlobalSearch =
+ !isPartner &&
+ !isLoading &&
+ !isAuthLoading &&
+ !isLoadingUserDetails &&
+ !isError &&
+ !allProjectsSuspended &&
+ !isCheckingAllSuspended &&
+ projects.length > 1;
+
+ if (showUserGlobalSearch) {
+ return ;
+ }
+
const showSearchBar = shouldShowProjectHubSearchBar(
titleTotalRecords,
searchQuery,
diff --git a/apps/customer-portal/webapp/src/features/project-hub/pages/UserCasesPage.tsx b/apps/customer-portal/webapp/src/features/project-hub/pages/UserCasesPage.tsx
new file mode 100644
index 0000000000..ad52db5419
--- /dev/null
+++ b/apps/customer-portal/webapp/src/features/project-hub/pages/UserCasesPage.tsx
@@ -0,0 +1,475 @@
+// 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 {
+ alpha,
+ Box,
+ Button,
+ Chip,
+ CircularProgress,
+ IconButton,
+ InputAdornment,
+ LinearProgress,
+ Menu,
+ MenuItem,
+ Paper,
+ Skeleton,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TablePagination,
+ TableRow,
+ TextField,
+ Typography,
+} from "@wso2/oxygen-ui";
+import { ArrowLeft, ChevronDown, Download, FileText, Search, X } from "@wso2/oxygen-ui-icons-react";
+import { type ChangeEvent, type JSX, type MouseEvent, useCallback, useEffect, useRef, useState } from "react";
+import { useNavigate, useSearchParams } from "react-router";
+import { useGetGlobalSearch } from "@api/useGetGlobalSearch";
+import { useDebouncedValue } from "@hooks/useDebouncedValue";
+import { PROJECT_HUB_SEARCH_DEBOUNCE_MS } from "@features/project-hub/constants/projectHubConstants";
+import { getSeverityLegendColor } from "@features/dashboard/utils/dashboard";
+import { formatCasesTableCaseIdentifier, getStatusColor } from "@features/dashboard/utils/casesTable";
+import { mapSeverityToDisplay } from "@features/support/utils/support";
+import { getCaseNavigationPath, getCaseTypeChipProps } from "@features/project-hub/utils/globalSearchNavigation";
+import {
+ downloadCaseListCsv,
+ downloadCaseListPdf,
+ fetchAllCasesForExport,
+} from "@features/project-hub/utils/casesExport";
+import { useAuthApiClient } from "@/hooks/useAuthApiClient";
+import { useErrorBanner } from "@context/error-banner/ErrorBannerContext";
+
+const ROWS_PER_PAGE_OPTIONS = [10, 25, 50];
+const DEFAULT_ROWS_PER_PAGE = 10;
+const SKELETON_ROW_COUNT = 5;
+const COL_SPAN = 7;
+
+const DATE_LOCALE = "en-US";
+const DATE_FORMAT_OPTIONS: Intl.DateTimeFormatOptions = {
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+};
+
+function formatDate(value: string | null | undefined): string {
+ if (!value) return "--";
+ const d = new Date(value);
+ return isNaN(d.getTime()) ? "--" : d.toLocaleDateString(DATE_LOCALE, DATE_FORMAT_OPTIONS);
+}
+
+type ExportFormat = "csv" | "pdf";
+
+/**
+ * Full-page cases search for users with more than one project.
+ * Navigated to via "View More" on the home overview page.
+ * Pre-fills the search bar from the `?q=` URL parameter.
+ */
+export default function UserCasesPage(): JSX.Element {
+ const navigate = useNavigate();
+ const authFetch = useAuthApiClient();
+ const { showError } = useErrorBanner();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const [searchQuery, setSearchQuery] = useState(() => searchParams.get("q") ?? "");
+ const debouncedSearchQuery = useDebouncedValue(searchQuery, PROJECT_HUB_SEARCH_DEBOUNCE_MS);
+
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(DEFAULT_ROWS_PER_PAGE);
+
+ useEffect(() => {
+ setPage(0);
+ }, [debouncedSearchQuery]);
+
+ useEffect(() => {
+ setSearchParams(
+ (prev) => {
+ const next = new URLSearchParams(prev);
+ if (debouncedSearchQuery.trim()) {
+ next.set("q", debouncedSearchQuery.trim());
+ } else {
+ next.delete("q");
+ }
+ return next;
+ },
+ { replace: true },
+ );
+ }, [debouncedSearchQuery, setSearchParams]);
+
+ const { data, isLoading, isError } = useGetGlobalSearch({
+ filters: {
+ types: ["cases"],
+ ...(debouncedSearchQuery ? { searchQuery: debouncedSearchQuery } : {}),
+ },
+ casesPagination: { offset: page * rowsPerPage, limit: rowsPerPage },
+ });
+
+ const cases = data?.cases ?? [];
+ const totalRecords = data?.casesTotal ?? 0;
+
+ const [exportingFormat, setExportingFormat] = useState(null);
+ const isExportingRef = useRef(false);
+ const [exportAnchorEl, setExportAnchorEl] = useState(null);
+
+ const handleExportOpen = (e: MouseEvent) => {
+ if (!isExportingRef.current) setExportAnchorEl(e.currentTarget);
+ };
+ const handleExportClose = () => setExportAnchorEl(null);
+
+ const handleExport = useCallback(
+ async (format: ExportFormat) => {
+ if (isExportingRef.current) return;
+ handleExportClose();
+ isExportingRef.current = true;
+ setExportingFormat(format);
+ try {
+ const allCases = await fetchAllCasesForExport(authFetch, debouncedSearchQuery);
+ if (allCases.length === 0) {
+ showError("No cases to export.");
+ return;
+ }
+ if (format === "csv") {
+ downloadCaseListCsv(allCases);
+ } else {
+ downloadCaseListPdf(allCases);
+ }
+ } catch {
+ showError("Failed to export cases.");
+ } finally {
+ isExportingRef.current = false;
+ setExportingFormat(null);
+ }
+ },
+ [authFetch, debouncedSearchQuery, showError],
+ );
+
+ const isExporting = exportingFormat !== null;
+
+ const handleChangePage = (_: unknown, newPage: number) => setPage(newPage);
+ const handleChangeRowsPerPage = (e: ChangeEvent) => {
+ setRowsPerPage(parseInt(e.target.value, 10));
+ setPage(0);
+ };
+
+ return (
+
+ {/* Header */}
+
+ navigate("/")}
+ size="small"
+ >
+
+
+
+
+ Cases
+ {!isLoading && (
+
+ {" "}({totalRecords})
+
+ )}
+ {isLoading && (
+
+ )}
+
+ }
+ onClick={handleExportOpen}
+ size="small"
+ startIcon={
+ isExporting ? (
+
+ ) : (
+
+ )
+ }
+ type="button"
+ variant="outlined"
+ >
+ {isExporting ? "Exporting..." : "Export"}
+
+
+
+
+ {/* Filtered-results hint */}
+ {debouncedSearchQuery.trim() && !isLoading && (
+
+
+ Showing results for “{debouncedSearchQuery.trim()}”
+
+
+ )}
+
+ {/* Search bar */}
+
+
+ setSearchQuery("")}
+ size="small"
+ >
+
+
+
+ ) : undefined,
+ startAdornment: ,
+ }}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ placeholder="Search cases..."
+ size="small"
+ sx={{
+ maxWidth: 560,
+ "& input:-webkit-autofill, & input:-webkit-autofill:focus, & input:-webkit-autofill:hover":
+ {
+ WebkitBoxShadow: "0 0 0 100px transparent inset",
+ WebkitTextFillColor: "inherit",
+ transition: "background-color 5000s ease-in-out 0s",
+ },
+ }}
+ value={searchQuery}
+ />
+
+
+ {/* Loading indicator */}
+ {isLoading && (
+
+ )}
+
+ {/* Scrollable table */}
+
+
+
+
+
+ Details
+ Case Type
+ Severity
+ Status
+ Created by
+ Created on
+ Project
+
+
+
+ {isLoading ? (
+ Array.from({ length: SKELETON_ROW_COUNT }).map((_, i) => (
+
+ {Array.from({ length: COL_SPAN }).map((_, j) => (
+
+
+
+ ))}
+
+ ))
+ ) : isError ? (
+
+
+
+ Failed to load cases. Please try again.
+
+
+
+ ) : cases.length === 0 ? (
+
+
+
+ No cases found.
+
+
+
+ ) : (
+ cases.map((c) => {
+ const severityLabel = c.severity?.label;
+ const severityDisplay = mapSeverityToDisplay(severityLabel);
+ const severityColor = getSeverityLegendColor(severityLabel);
+ const { color: caseTypeColor, displayLabel: caseTypeLabel } = getCaseTypeChipProps(c.caseType?.label);
+ const casePath = getCaseNavigationPath(c);
+ const returnTo = `/cases${searchParams.toString() ? `?${searchParams.toString()}` : ""}`;
+ const handleNavigate = casePath ? () => navigate(casePath, { state: { returnTo } }) : undefined;
+ return (
+ {
+ if (handleNavigate && (e.key === "Enter" || e.key === " ")) {
+ e.preventDefault();
+ handleNavigate();
+ }
+ }}
+ sx={{ cursor: casePath ? "pointer" : "default" }}
+ tabIndex={casePath ? 0 : undefined}
+ >
+
+
+
+ {c.title ?? "--"}
+
+
+ {formatCasesTableCaseIdentifier(c.number, c.internalId)}
+
+
+
+
+ ({
+ bgcolor: alpha(caseTypeColor, theme.palette.mode === "dark" ? 0.05 : 0.1),
+ borderColor: alpha(caseTypeColor, theme.palette.mode === "dark" ? 0.18 : 0.3),
+ color: caseTypeColor,
+ fontSize: "0.75rem",
+ fontWeight: 500,
+ height: 20,
+ px: 0,
+ "& .MuiChip-label": { pl: "6px", pr: "6px" },
+ })}
+ />
+
+
+ {severityLabel ? (
+
+ ) : (
+
+ --
+
+ )}
+
+
+
+
+
+ {c.state?.label ?? "--"}
+
+
+
+
+
+ {c.createdBy ?? "--"}
+
+
+
+
+ {formatDate(c.createdOn)}
+
+
+
+
+ {c.project?.label ?? "--"}
+
+
+
+ );
+ })
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/customer-portal/webapp/src/features/project-hub/pages/UserProjectsPage.tsx b/apps/customer-portal/webapp/src/features/project-hub/pages/UserProjectsPage.tsx
new file mode 100644
index 0000000000..4ae9fe74ff
--- /dev/null
+++ b/apps/customer-portal/webapp/src/features/project-hub/pages/UserProjectsPage.tsx
@@ -0,0 +1,402 @@
+// 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,
+ Button,
+ Chip,
+ CircularProgress,
+ IconButton,
+ InputAdornment,
+ LinearProgress,
+ Menu,
+ MenuItem,
+ Paper,
+ Skeleton,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TablePagination,
+ TableRow,
+ TextField,
+ Typography,
+} from "@wso2/oxygen-ui";
+import { ArrowLeft, ChevronDown, Download, FolderOpen, Search, X } from "@wso2/oxygen-ui-icons-react";
+import { type ChangeEvent, type JSX, type MouseEvent, useCallback, useEffect, useRef, useState } from "react";
+import { useNavigate, useSearchParams } from "react-router";
+import { useGetGlobalSearch } from "@api/useGetGlobalSearch";
+import { useAuthApiClient } from "@/hooks/useAuthApiClient";
+import { useErrorBanner } from "@context/error-banner/ErrorBannerContext";
+import { useDebouncedValue } from "@hooks/useDebouncedValue";
+import { PROJECT_HUB_SEARCH_DEBOUNCE_MS } from "@features/project-hub/constants/projectHubConstants";
+import {
+ downloadProjectListCsv,
+ downloadProjectListPdf,
+ fetchAllProjectsForExport,
+} from "@features/project-hub/utils/projectsExport";
+
+const ROWS_PER_PAGE_OPTIONS = [10, 25, 50];
+const DEFAULT_ROWS_PER_PAGE = 10;
+const SKELETON_ROW_COUNT = 5;
+const COL_SPAN = 6;
+
+type ExportFormat = "csv" | "pdf";
+
+/**
+ * Full-page projects search for users with more than one project.
+ * Navigated to via "View More" on the home overview page.
+ * Pre-fills the search bar from the `?q=` URL parameter.
+ */
+export default function UserProjectsPage(): JSX.Element {
+ const navigate = useNavigate();
+ const authFetch = useAuthApiClient();
+ const { showError } = useErrorBanner();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const [searchQuery, setSearchQuery] = useState(() => searchParams.get("q") ?? "");
+ const debouncedSearchQuery = useDebouncedValue(searchQuery, PROJECT_HUB_SEARCH_DEBOUNCE_MS);
+
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(DEFAULT_ROWS_PER_PAGE);
+
+ const normalizedQuery = debouncedSearchQuery.trim();
+
+ useEffect(() => {
+ setPage(0);
+ }, [normalizedQuery]);
+
+ useEffect(() => {
+ setSearchParams(
+ (prev) => {
+ const next = new URLSearchParams(prev);
+ if (normalizedQuery) {
+ next.set("q", normalizedQuery);
+ } else {
+ next.delete("q");
+ }
+ return next;
+ },
+ { replace: true },
+ );
+ }, [normalizedQuery, setSearchParams]);
+
+ const { data, isLoading, isError } = useGetGlobalSearch({
+ filters: {
+ types: ["projects"],
+ ...(normalizedQuery ? { searchQuery: normalizedQuery } : {}),
+ },
+ projectsPagination: { offset: page * rowsPerPage, limit: rowsPerPage },
+ });
+
+ const projects = data?.projects ?? [];
+ const totalRecords = data?.projectsTotal ?? 0;
+
+ const [exportingFormat, setExportingFormat] = useState(null);
+ const isExportingRef = useRef(false);
+ const [exportAnchorEl, setExportAnchorEl] = useState(null);
+
+ const handleExportOpen = (e: MouseEvent) => {
+ if (!isExportingRef.current) setExportAnchorEl(e.currentTarget);
+ };
+ const handleExportClose = () => setExportAnchorEl(null);
+
+ const handleExport = useCallback(
+ async (format: ExportFormat) => {
+ if (isExportingRef.current) return;
+ handleExportClose();
+ isExportingRef.current = true;
+ setExportingFormat(format);
+ try {
+ const allProjects = await fetchAllProjectsForExport(authFetch, normalizedQuery);
+ if (allProjects.length === 0) {
+ showError("No projects to export.");
+ return;
+ }
+ if (format === "csv") {
+ downloadProjectListCsv(allProjects);
+ } else {
+ downloadProjectListPdf(allProjects);
+ }
+ } catch {
+ showError("Failed to export projects.");
+ } finally {
+ isExportingRef.current = false;
+ setExportingFormat(null);
+ }
+ },
+ [authFetch, normalizedQuery, showError],
+ );
+
+ const isExporting = exportingFormat !== null;
+
+ const handleChangePage = (_: unknown, newPage: number) => setPage(newPage);
+ const handleChangeRowsPerPage = (e: ChangeEvent) => {
+ setRowsPerPage(parseInt(e.target.value, 10));
+ setPage(0);
+ };
+
+ return (
+
+ {/* Header */}
+
+ navigate("/")}
+ size="small"
+ >
+
+
+
+
+ Projects
+ {!isLoading && (
+
+ {" "}({totalRecords})
+
+ )}
+ {isLoading && (
+
+ )}
+
+ }
+ onClick={handleExportOpen}
+ size="small"
+ startIcon={
+ isExporting ? (
+
+ ) : (
+
+ )
+ }
+ type="button"
+ variant="outlined"
+ >
+ {isExporting ? "Exporting..." : "Export"}
+
+
+
+
+ {/* Filtered-results hint */}
+ {normalizedQuery && !isLoading && (
+
+
+ Showing results for “{normalizedQuery}”
+
+
+ )}
+
+ {/* Search bar */}
+
+
+ setSearchQuery("")}
+ size="small"
+ >
+
+
+
+ ) : undefined,
+ startAdornment: ,
+ }}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ placeholder="Search projects..."
+ size="small"
+ sx={{
+ maxWidth: 560,
+ "& input:-webkit-autofill, & input:-webkit-autofill:focus, & input:-webkit-autofill:hover":
+ {
+ WebkitBoxShadow: "0 0 0 100px transparent inset",
+ WebkitTextFillColor: "inherit",
+ transition: "background-color 5000s ease-in-out 0s",
+ },
+ }}
+ value={searchQuery}
+ />
+
+
+ {/* Loading indicator */}
+ {isLoading && (
+
+ )}
+
+ {/* Scrollable table */}
+
+
+
+
+
+ Project Key
+ Name
+ Status
+ Action Required
+ Outstanding
+ Active Chats
+
+
+
+ {isLoading ? (
+ Array.from({ length: SKELETON_ROW_COUNT }).map((_, i) => (
+
+ {Array.from({ length: COL_SPAN }).map((_, j) => (
+
+
+
+ ))}
+
+ ))
+ ) : isError ? (
+
+
+
+ Failed to load projects. Please try again.
+
+
+
+ ) : projects.length === 0 ? (
+
+
+
+ No projects found.
+
+
+
+ ) : (
+ projects.map((project) => {
+ const isSuspended =
+ project.closureState?.toLowerCase() === "suspended";
+ return (
+
+ navigate(`/projects/${project.id}/dashboard`)
+ }
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ navigate(`/projects/${project.id}/dashboard`);
+ }
+ }}
+ sx={{ cursor: "pointer" }}
+ tabIndex={0}
+ >
+
+
+ {project.key}
+
+
+
+ {project.name}
+
+
+
+
+
+
+ {project.actionRequiredCount ?? 0}
+
+
+
+
+ {project.outstandingCount ?? 0}
+
+
+
+
+ {project.activeChatsCount ?? 0}
+
+
+
+ );
+ })
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/customer-portal/webapp/src/features/project-hub/types/globalSearch.ts b/apps/customer-portal/webapp/src/features/project-hub/types/globalSearch.ts
index bc64f661a6..3dd4460663 100644
--- a/apps/customer-portal/webapp/src/features/project-hub/types/globalSearch.ts
+++ b/apps/customer-portal/webapp/src/features/project-hub/types/globalSearch.ts
@@ -49,6 +49,9 @@ export interface GlobalSearchProject {
hasPdpSubscription: boolean;
closureState?: string | null;
account: ReferenceItem;
+ activeChatsCount: number;
+ actionRequiredCount: number;
+ outstandingCount: number;
}
export interface GlobalSearchCase {
diff --git a/apps/customer-portal/webapp/src/layouts/NonPartnerGuard.tsx b/apps/customer-portal/webapp/src/layouts/NonPartnerGuard.tsx
new file mode 100644
index 0000000000..4a272d3a97
--- /dev/null
+++ b/apps/customer-portal/webapp/src/layouts/NonPartnerGuard.tsx
@@ -0,0 +1,57 @@
+// 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 { type JSX } from "react";
+import { Navigate, Outlet } from "react-router";
+import { Box, LinearProgress, Typography } from "@wso2/oxygen-ui";
+import useGetUserDetails from "@features/settings/api/useGetUserDetails";
+import { hasPartnerAccess } from "@features/settings/constants/settingsConstants";
+
+/**
+ * Guards routes meant only for non-partner users (e.g. the user overview
+ * drill-down pages). Partner users are redirected to the home page, which
+ * renders their own global search overview.
+ * On fetch error the guard shows an error message rather than falsely redirecting.
+ */
+export default function NonPartnerGuard(): JSX.Element {
+ const { data: userDetails, isLoading, isError } = useGetUserDetails();
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (isError || !userDetails) {
+ return (
+
+
+ Unable to verify access. Please refresh and try again.
+
+
+ );
+ }
+
+ const isPartner = hasPartnerAccess(userDetails.roles ?? []);
+
+ if (isPartner) {
+ return ;
+ }
+
+ return ;
+}