diff --git a/e2e/vendors/vendor-operations.spec.ts b/e2e/vendors/vendor-operations.spec.ts
index edfaf191..13c7b1eb 100644
--- a/e2e/vendors/vendor-operations.spec.ts
+++ b/e2e/vendors/vendor-operations.spec.ts
@@ -38,14 +38,16 @@ test.describe("Vendor operations", () => {
await page.goto("/vendors/insights");
- await expect(page.getByRole("link", { name: "Vendors", exact: true })).toHaveAttribute(
- "aria-current",
- "page",
- );
- await expect(page.getByRole("link", { name: "Sites", exact: true })).not.toHaveAttribute(
- "aria-current",
- "page",
- );
+ const vendorLinks = page.locator('a[href^="/vendors"]');
+ await expect(
+ vendorLinks.filter({ has: page.getByText("Vendor Insights", { exact: true }) }),
+ ).toHaveAttribute("aria-current", "page");
+ await expect(
+ vendorLinks.filter({ has: page.getByText("Vendors", { exact: true }) }),
+ ).not.toHaveAttribute("aria-current", "page");
+ await expect(
+ vendorLinks.filter({ has: page.getByText("Site Preferences", { exact: true }) }),
+ ).not.toHaveAttribute("aria-current", "page");
});
test("keeps Vendor PO controls usable at a constrained viewport", async ({ page }) => {
@@ -252,7 +254,7 @@ test.describe("Vendor operations", () => {
});
await page.goto("/vendors/preferences");
- await page.getByLabel("Site", { exact: true }).click();
+ await page.getByLabel("Site").click();
await page.getByRole("option", { name: "St. Louis HQ" }).click();
await expect(page.getByText(/1\. Gateway Plumbing/)).toBeVisible();
await page.getByRole("button", { name: "Move Metro HVAC up" }).click();
diff --git a/e2e/vendors/vendors.spec.ts b/e2e/vendors/vendors.spec.ts
index a426d97a..e8f54bef 100644
--- a/e2e/vendors/vendors.spec.ts
+++ b/e2e/vendors/vendors.spec.ts
@@ -795,9 +795,9 @@ test.describe("Vendor directory prototype parity", () => {
for (const destination of [
{ path: "/workorders", label: "Work Orders" },
- { path: "/vendors", label: "Vendors" },
+ { path: "/accounts", label: "Accounts" },
{ path: "/locations", label: "Sites" },
- { path: "/account", label: "My Account" },
+ { path: "/notifications", label: "Notifications" },
]) {
await page.goto(destination.path);
await expect(page.getByRole("link", { name: destination.label })).toHaveAttribute(
diff --git a/src/app/(protected)/account/index.tsx b/src/app/(protected)/account/index.tsx
deleted file mode 100644
index 47d7e04b..00000000
--- a/src/app/(protected)/account/index.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { PlaceholderPage } from "@/components/common/placeholder-page";
-
-export default function AccountPage() {
- return ;
-}
diff --git a/src/app/(protected)/completion-docs/index.tsx b/src/app/(protected)/completion-docs/index.tsx
deleted file mode 100644
index e5c9ea2f..00000000
--- a/src/app/(protected)/completion-docs/index.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { PlaceholderPage } from "@/components/common/placeholder-page";
-
-export default function CompletionDocsPage() {
- return ;
-}
diff --git a/src/app/(protected)/services/index.tsx b/src/app/(protected)/services/index.tsx
deleted file mode 100644
index e86bc94b..00000000
--- a/src/app/(protected)/services/index.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { PlaceholderPage } from "@/components/common/placeholder-page";
-
-export default function ServicesPage() {
- return ;
-}
diff --git a/src/components/common/placeholder-page.tsx b/src/components/common/placeholder-page.tsx
deleted file mode 100644
index 3d110268..00000000
--- a/src/components/common/placeholder-page.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Box } from "@mui/material";
-
-import { Text } from "@/components/ui/text";
-
-type PlaceholderPageProps = {
- title: string;
-};
-
-export function PlaceholderPage({ title }: PlaceholderPageProps) {
- return (
-
-
- {title}
-
-
- This page is under construction.
-
-
- );
-}
diff --git a/src/components/layout/app-sidebar.tsx b/src/components/layout/app-sidebar.tsx
index 99d92161..84aeb606 100644
--- a/src/components/layout/app-sidebar.tsx
+++ b/src/components/layout/app-sidebar.tsx
@@ -1,66 +1,120 @@
-import { useLocation } from "react-router";
+import { useState } from "react";
+import { Link, useLocation } from "react-router";
import { Box, Drawer } from "@mui/material";
import { BrandLockup, BrandMark } from "@/components/common/brand-lockup";
-import { SidebarNavItem } from "@/components/layout/sidebar-nav-item";
-import { Text } from "@/components/ui/text";
-import {
- accountNavItem,
- getActiveNavPath,
- getSidebarWidth,
- getVisibleNavGroups,
-} from "@/config/menu";
-import { useAuthContext } from "@/providers/auth-context";
+import { getActiveNavPath, getSidebarWidth, navGroups, type NavItemDef } from "@/config/menu";
-type NavGroupHeadingProps = {
- title: string;
+function getNavItemAppearance(isActive: boolean, isHover: boolean) {
+ if (isActive) {
+ return {
+ itemColor: "var(--primary)",
+ iconColor: "var(--primary)",
+ itemBg: "var(--color-sidebar-item-active-bg)",
+ };
+ }
+
+ if (isHover) {
+ return {
+ itemColor: "var(--primary-hover)",
+ iconColor: "var(--primary-hover)",
+ itemBg: "var(--sidebar-accent)",
+ };
+ }
+
+ return {
+ itemColor: "var(--foreground)",
+ iconColor: "var(--muted-foreground)",
+ itemBg: "transparent",
+ };
+}
+
+type NavItemProps = NavItemDef & {
collapsed: boolean;
- showDivider: boolean;
+ active: boolean;
+ onNavigate?: () => void;
};
-function NavGroupHeading({ title, collapsed, showDivider }: NavGroupHeadingProps) {
- if (collapsed) {
- if (!showDivider) {
- return null;
- }
-
- return (
-
- );
- }
-
- if (!title) {
- return ;
- }
+function NavItem({ label, icon: Icon, path, collapsed, active, badge, onNavigate }: NavItemProps) {
+ const [hovered, setHovered] = useState(false);
+ const isHover = hovered && !active;
+ const { itemColor, iconColor, itemBg } = getNavItemAppearance(active, isHover);
return (
-
- {title}
-
+ setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ sx={{
+ position: "relative",
+ display: "flex",
+ alignItems: "center",
+ width: "100%",
+ height: 40,
+ gap: "12px",
+ px: collapsed ? 0 : "12px",
+ justifyContent: collapsed ? "center" : "flex-start",
+ borderRadius: "6px",
+ cursor: "pointer",
+ fontFamily: "var(--font-sans)",
+ fontSize: 14,
+ fontWeight: active ? 600 : 500,
+ color: itemColor,
+ backgroundColor: itemBg,
+ boxShadow: active ? "var(--color-sidebar-item-active-shadow)" : "none",
+ transition: "all var(--duration-fast, 150ms) var(--ease-default, ease)",
+ mb: "4px",
+ textAlign: "left",
+ }}
+ >
+ {active && (
+
+ )}
+
+ {!collapsed && (
+ <>
+ {label}
+ {badge != null && (
+
+ {badge}
+
+ )}
+ >
+ )}
+
+
);
}
@@ -74,10 +128,7 @@ type AppSidebarProps = {
export function AppSidebar({ collapsed, mobile = false, open = true, onClose }: AppSidebarProps) {
const width = getSidebarWidth(collapsed);
const { pathname } = useLocation();
- const { user } = useAuthContext();
const activePath = getActiveNavPath(pathname);
- const visibleGroups = getVisibleNavGroups(user?.userRoles);
- const onNavigate = mobile ? onClose : undefined;
return (
- {visibleGroups.map((group, groupIndex) => (
+ {navGroups.map((group, groupIndex) => (
- 0}
- />
+ {collapsed ? (
+ groupIndex > 0 && (
+
+ )
+ ) : group.title ? (
+
+ {group.title}
+
+ ) : (
+
+ )}
{group.items.map(({ label, icon, path, badge }) => (
-
))}
@@ -147,15 +220,6 @@ export function AppSidebar({ collapsed, mobile = false, open = true, onClose }:
))}
-
-
-
-
);
diff --git a/src/components/layout/sidebar-nav-item.tsx b/src/components/layout/sidebar-nav-item.tsx
deleted file mode 100644
index eb1f98b0..00000000
--- a/src/components/layout/sidebar-nav-item.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import { useState } from "react";
-import { Link } from "react-router";
-import { Box } from "@mui/material";
-
-import { getVisibleBadge, type NavItemDef } from "@/config/menu";
-
-function getNavItemAppearance(isActive: boolean, isHover: boolean) {
- if (isActive) {
- return {
- itemColor: "var(--primary)",
- iconColor: "var(--primary)",
- itemBg: "var(--color-sidebar-item-active-bg)",
- };
- }
-
- if (isHover) {
- return {
- itemColor: "var(--primary-hover)",
- iconColor: "var(--primary-hover)",
- itemBg: "var(--sidebar-accent)",
- };
- }
-
- return {
- itemColor: "var(--foreground)",
- iconColor: "var(--muted-foreground)",
- itemBg: "transparent",
- };
-}
-
-type SidebarNavItemProps = NavItemDef & {
- collapsed: boolean;
- active: boolean;
- onNavigate?: () => void;
-};
-
-export function SidebarNavItem({
- label,
- icon: Icon,
- path,
- collapsed,
- active,
- badge,
- onNavigate,
-}: SidebarNavItemProps) {
- const [hovered, setHovered] = useState(false);
- const isHover = hovered && !active;
- const { itemColor, iconColor, itemBg } = getNavItemAppearance(active, isHover);
- const visibleBadge = getVisibleBadge(badge);
-
- return (
-
- setHovered(true)}
- onMouseLeave={() => setHovered(false)}
- sx={{
- position: "relative",
- display: "flex",
- alignItems: "center",
- width: "100%",
- height: 40,
- gap: "12px",
- px: collapsed ? 0 : "12px",
- justifyContent: collapsed ? "center" : "flex-start",
- borderRadius: "6px",
- cursor: "pointer",
- fontFamily: "var(--font-sans)",
- fontSize: 14,
- fontWeight: active ? 600 : 500,
- color: itemColor,
- backgroundColor: itemBg,
- boxShadow: active ? "var(--color-sidebar-item-active-shadow)" : "none",
- transition: "all var(--duration-fast, 150ms) var(--ease-default, ease)",
- mb: "4px",
- textAlign: "left",
- }}
- >
- {active && (
-
- )}
-
- {Boolean(!collapsed) && (
- <>
- {label}
- {visibleBadge != null && (
-
- {visibleBadge}
-
- )}
- >
- )}
-
-
- );
-}
diff --git a/src/config/menu.ts b/src/config/menu.ts
index 01303e59..3974c0dc 100644
--- a/src/config/menu.ts
+++ b/src/config/menu.ts
@@ -1,18 +1,17 @@
import type { LucideIcon } from "lucide-react";
import {
+ // BarChart2,
+ BriefcaseBusiness,
Building2,
ClipboardList,
- DollarSign,
FileText,
LayoutDashboard,
MapPin,
- Settings,
- UserRound,
- Wrench,
+ Bell,
+ BarChart3,
+ Star,
} from "lucide-react";
-import { isAdminUser } from "@/lib/auth/user-utils";
-
export type NavItemDef = {
label: string;
icon: LucideIcon;
@@ -23,66 +22,52 @@ export type NavItemDef = {
export type NavGroup = {
title: string;
items: NavItemDef[];
- visible?: (userRoles: string | null | undefined) => boolean;
};
export const SIDEBAR_WIDTH = 244;
export const SIDEBAR_COLLAPSED_WIDTH = 76;
-export const accountNavItem: NavItemDef = {
- label: "My Account",
- icon: Settings,
- path: "/account",
-};
-
export function getSidebarWidth(collapsed: boolean): number {
return collapsed ? SIDEBAR_COLLAPSED_WIDTH : SIDEBAR_WIDTH;
}
export const navGroups: NavGroup[] = [
{
- title: "OPERATIONS",
+ title: "",
items: [
- { label: "Work Orders", icon: ClipboardList, path: "/workorders" },
+ { label: "Work Orders", icon: ClipboardList, path: "/workorders", badge: 11 },
{ label: "Dashboard", icon: LayoutDashboard, path: "/" },
+ { label: "Notifications", icon: Bell, path: "/notifications" },
+ // { label: "Reports", icon: BarChart2, path: "/reports" },
],
},
{
- title: "DATA MANAGEMENT",
+ title: "VENDORS",
items: [
{ label: "Vendors", icon: Building2, path: "/vendors" },
- { label: "Sites", icon: MapPin, path: "/locations" },
- { label: "Services", icon: Wrench, path: "/services" },
- { label: "Completion Docs", icon: FileText, path: "/completion-docs" },
+ { label: "Vendor POs", icon: FileText, path: "/vendor-pos" },
+ { label: "Vendor Insights", icon: BarChart3, path: "/vendors/insights" },
+ { label: "Site Preferences", icon: Star, path: "/vendors/preferences" },
],
},
{
- title: "TEAM",
- visible: isAdminUser,
- items: [{ label: "Team Members", icon: UserRound, path: "/employees" }],
- },
- {
- title: "APPROVALS",
- visible: isAdminUser,
- items: [{ label: "Uplift Approvals", icon: DollarSign, path: "/uplifts" }],
+ title: "CUSTOMERS & SITES",
+ items: [
+ { label: "Accounts", icon: BriefcaseBusiness, path: "/accounts" },
+ { label: "Sites", icon: MapPin, path: "/locations" },
+ ],
},
+ // {
+ // title: "INTERNAL",
+ // items: [{ label: "Documents", icon: FileText, path: "/documents" }],
+ // },
];
-export function getVisibleNavGroups(userRoles: string | null | undefined): NavGroup[] {
- return navGroups.filter((group) => group.visible == null || group.visible(userRoles));
-}
-
-export function getVisibleBadge(badge?: number): number | undefined {
- return badge != null && badge > 0 ? badge : undefined;
-}
-
-function collectNavPaths(): string[] {
- return [...navGroups.flatMap((group) => group.items), accountNavItem].map((item) => item.path);
-}
-
export function getActiveNavPath(pathname: string): string | undefined {
- return collectNavPaths()
+ return navGroups
+ .flatMap((group) => group.items)
+ .map((item) => item.path)
.filter((path) =>
path === "/" ? pathname === path : pathname === path || pathname.startsWith(`${path}/`),
)
diff --git a/src/styles/typography.css b/src/styles/typography.css
index ed591c42..34d61f9e 100644
--- a/src/styles/typography.css
+++ b/src/styles/typography.css
@@ -73,7 +73,7 @@ h6 {
}
.text-nav-group {
- font-family: var(--font-display);
+ font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
line-height: normal;
diff --git a/src/test/components/common/placeholder-page.test.tsx b/src/test/components/common/placeholder-page.test.tsx
deleted file mode 100644
index 0b4f7056..00000000
--- a/src/test/components/common/placeholder-page.test.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { screen } from "@testing-library/react";
-import { describe, expect, it } from "vitest";
-
-import { PlaceholderPage } from "@/components/common/placeholder-page";
-import { renderWithProviders } from "@/test/test-utils";
-
-describe("PlaceholderPage", () => {
- it("renders the AAP under-construction copy", () => {
- renderWithProviders(, { withAuth: false });
-
- expect(screen.getByRole("heading", { name: "Services" })).toBeInTheDocument();
- expect(screen.getByText("This page is under construction.")).toBeInTheDocument();
- });
-});
diff --git a/src/test/components/layout/app-sidebar.test.tsx b/src/test/components/layout/app-sidebar.test.tsx
deleted file mode 100644
index 390256cf..00000000
--- a/src/test/components/layout/app-sidebar.test.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { screen } from "@testing-library/react";
-import type { ReactElement } from "react";
-import { describe, expect, it, vi } from "vitest";
-
-import { AppSidebar } from "@/components/layout/app-sidebar";
-import type { AuthUser } from "@/domain/auth/types/auth-user";
-import { AuthContext, type AuthContextValue } from "@/providers/auth-context";
-import { renderWithProviders } from "@/test/test-utils";
-
-function createUser(userRoles: string): AuthUser {
- return {
- token: "test-jwt-token",
- expiration: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
- email: "user@seahavenind.com",
- userRoles,
- phoneNumber: "5551234567",
- fullname: "Test User",
- id: 1,
- };
-}
-
-function createAuthValue(userRoles: string): AuthContextValue {
- return {
- user: createUser(userRoles),
- isAuthenticated: true,
- isLoading: false,
- error: null,
- login: vi.fn(),
- logout: vi.fn(),
- changePassword: vi.fn(),
- isLoggingIn: false,
- isLoggingOut: false,
- isChangingPassword: false,
- loginError: null,
- };
-}
-
-function renderSidebar(ui: ReactElement, userRoles: string, route = "/workorders") {
- return renderWithProviders(
- {ui},
- {
- withAuth: false,
- route,
- },
- );
-}
-
-describe("AppSidebar", () => {
- it("renders the AAP tree plus My Account for Admin", () => {
- renderSidebar(, "Admin");
-
- expect(screen.getByText("OPERATIONS")).toBeInTheDocument();
- expect(screen.getByText("DATA MANAGEMENT")).toBeInTheDocument();
- expect(screen.getByText("TEAM")).toBeInTheDocument();
- expect(screen.getByText("APPROVALS")).toBeInTheDocument();
- expect(screen.getByRole("link", { name: "Work Orders" })).toBeInTheDocument();
- expect(screen.getByRole("link", { name: "Team Members" })).toBeInTheDocument();
- expect(screen.getByRole("link", { name: "Uplift Approvals" })).toBeInTheDocument();
- expect(screen.getByRole("link", { name: "My Account" })).toBeInTheDocument();
- expect(screen.queryByRole("link", { name: "Notifications" })).not.toBeInTheDocument();
- expect(screen.queryByText("11")).not.toBeInTheDocument();
- });
-
- it("hides TEAM and APPROVALS for non-admin and keeps My Account", () => {
- renderSidebar(, "Dispatcher");
-
- expect(screen.getByText("OPERATIONS")).toBeInTheDocument();
- expect(screen.queryByText("TEAM")).not.toBeInTheDocument();
- expect(screen.queryByText("APPROVALS")).not.toBeInTheDocument();
- expect(screen.queryByRole("link", { name: "Team Members" })).not.toBeInTheDocument();
- expect(screen.queryByRole("link", { name: "Uplift Approvals" })).not.toBeInTheDocument();
- expect(screen.getByRole("link", { name: "My Account" })).toBeInTheDocument();
- });
-
- it("hides group labels and item text when collapsed", () => {
- renderSidebar(, "Admin");
-
- expect(screen.queryByText("OPERATIONS")).not.toBeInTheDocument();
- expect(screen.queryByText("Work Orders")).not.toBeInTheDocument();
- expect(screen.getByRole("link", { name: "Work Orders" })).toHaveAttribute(
- "title",
- "Work Orders",
- );
- expect(screen.getByRole("link", { name: "My Account" })).toHaveAttribute("title", "My Account");
- });
-
- it("marks the active leaf from the current route", () => {
- renderSidebar(, "Admin", "/uplifts");
-
- expect(screen.getByRole("link", { name: "Uplift Approvals" })).toHaveAttribute(
- "aria-current",
- "page",
- );
- expect(screen.getByRole("link", { name: "Work Orders" })).not.toHaveAttribute("aria-current");
- });
-});
diff --git a/src/test/components/layout/sidebar-nav-item.test.tsx b/src/test/components/layout/sidebar-nav-item.test.tsx
deleted file mode 100644
index cefae72d..00000000
--- a/src/test/components/layout/sidebar-nav-item.test.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { ClipboardList } from "lucide-react";
-import { screen } from "@testing-library/react";
-import { describe, expect, it } from "vitest";
-
-import { SidebarNavItem } from "@/components/layout/sidebar-nav-item";
-import { renderWithProviders } from "@/test/test-utils";
-
-describe("SidebarNavItem", () => {
- it("omits a zero badge", () => {
- renderWithProviders(
- ,
- { withAuth: false, route: "/workorders" },
- );
-
- expect(screen.getByRole("link", { name: "Work Orders" })).toBeInTheDocument();
- expect(screen.queryByText("0")).not.toBeInTheDocument();
- });
-
- it("shows a positive badge", () => {
- renderWithProviders(
- ,
- { withAuth: false, route: "/uplifts" },
- );
-
- expect(screen.getByText("2")).toBeInTheDocument();
- });
-});
diff --git a/src/test/config/menu.test.ts b/src/test/config/menu.test.ts
index a5afd99d..ac75b38c 100644
--- a/src/test/config/menu.test.ts
+++ b/src/test/config/menu.test.ts
@@ -1,79 +1,20 @@
-import { getActiveNavPath, getVisibleBadge, getVisibleNavGroups, navGroups } from "@/config/menu";
+import { getActiveNavPath } from "@/config/menu";
import { describe, expect, it } from "vitest";
-describe("navGroups", () => {
- it("exposes the AAP flat tree in order", () => {
- expect(navGroups.map((group) => group.title)).toEqual([
- "OPERATIONS",
- "DATA MANAGEMENT",
- "TEAM",
- "APPROVALS",
- ]);
- expect(navGroups.flatMap((group) => group.items.map((item) => item.label))).toEqual([
- "Work Orders",
- "Dashboard",
- "Vendors",
- "Sites",
- "Services",
- "Completion Docs",
- "Team Members",
- "Uplift Approvals",
- ]);
- });
-
- it("does not hardcode a Work Orders badge", () => {
- const workOrders = navGroups
- .flatMap((group) => group.items)
- .find((item) => item.label === "Work Orders");
-
- expect(workOrders?.badge).toBeUndefined();
- });
-});
-
-describe("getVisibleNavGroups", () => {
- it("shows TEAM and APPROVALS only for Admin", () => {
- expect(getVisibleNavGroups("Admin").map((group) => group.title)).toEqual([
- "OPERATIONS",
- "DATA MANAGEMENT",
- "TEAM",
- "APPROVALS",
- ]);
- expect(getVisibleNavGroups("Dispatcher").map((group) => group.title)).toEqual([
- "OPERATIONS",
- "DATA MANAGEMENT",
- ]);
- });
-});
-
-describe("getVisibleBadge", () => {
- it("hides missing or zero counts", () => {
- expect(getVisibleBadge(undefined)).toBeUndefined();
- expect(getVisibleBadge(0)).toBeUndefined();
- expect(getVisibleBadge(2)).toBe(2);
- });
-});
-
describe("getActiveNavPath", () => {
it.each([
["/", "/"],
- ["/workorders", "/workorders"],
- ["/workorders/42", "/workorders"],
["/vendors", "/vendors"],
["/vendors/42", "/vendors"],
- ["/vendors/insights", "/vendors"],
- ["/locations", "/locations"],
- ["/services", "/services"],
- ["/completion-docs", "/completion-docs"],
- ["/employees", "/employees"],
- ["/employees/12", "/employees"],
- ["/uplifts", "/uplifts"],
- ["/account", "/account"],
+ ["/vendors/42/edit", "/vendors"],
+ ["/vendors/insights", "/vendors/insights"],
+ ["/vendors/preferences", "/vendors/preferences"],
+ ["/vendor-pos/42", "/vendor-pos"],
])("selects one longest boundary-matching route for %s", (pathname, expectedPath) => {
expect(getActiveNavPath(pathname)).toBe(expectedPath);
});
it("does not match partial route segments", () => {
expect(getActiveNavPath("/vendor-posts")).toBeUndefined();
- expect(getActiveNavPath("/accounts")).toBeUndefined();
});
});