From 3617b9ebb4763a7d09689d742d27f32104759675 Mon Sep 17 00:00:00 2001 From: jeffgicharu Date: Tue, 19 May 2026 01:28:24 +0300 Subject: [PATCH] fix(web): repair portal earnings chart, dead header search, bare 404 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three customer-facing UI issues found during a full walkthrough of the live app: - Portal "Monthly Earnings" rendered as a single full-width bar that swallowed the whole plot area whenever a contractor's paid invoices fell in one month — it read as a broken solid block. Switched it to the same gradient AreaChart the admin "Monthly Revenue" card already uses, so it degrades gracefully with sparse data and the two dashboards now share one chart treatment. - The header magnifying-glass was a button with no handler and no accessible name — clicking it did nothing and screen readers announced only "button". Made it a real, context-aware link to the searchable directory (contractors for admins, invoices in the portal) and gave the mobile nav toggle an accessible name. - The 404 page was unbranded bare text and pointed logged-out users at an auth-gated route. Rebranded it to match the auth shell and pointed the primary action at "/", which resolves correctly in any auth state. Adds regression tests for the header search/menu affordances and the not-found pages. --- apps/web/src/app/(admin)/not-found.tsx | 8 +-- .../app/(portal)/portal/dashboard/page.tsx | 19 +++++-- apps/web/src/app/not-found.test.tsx | 30 ++++++++++ apps/web/src/app/not-found.tsx | 35 ++++++++---- .../web/src/components/layout/header.test.tsx | 56 +++++++++++++++++++ apps/web/src/components/layout/header.tsx | 15 ++++- 6 files changed, 139 insertions(+), 24 deletions(-) create mode 100644 apps/web/src/app/not-found.test.tsx create mode 100644 apps/web/src/components/layout/header.test.tsx diff --git a/apps/web/src/app/(admin)/not-found.tsx b/apps/web/src/app/(admin)/not-found.tsx index 67525b2..3674be2 100644 --- a/apps/web/src/app/(admin)/not-found.tsx +++ b/apps/web/src/app/(admin)/not-found.tsx @@ -2,16 +2,16 @@ import Link from 'next/link'; export default function AdminNotFound() { return ( -
-
+
+

404

Page not found

- The page you are looking for does not exist. + The page you are looking for doesn't exist or may have been moved.

Go to Dashboard diff --git a/apps/web/src/app/(portal)/portal/dashboard/page.tsx b/apps/web/src/app/(portal)/portal/dashboard/page.tsx index 67791fb..0a5e6d0 100644 --- a/apps/web/src/app/(portal)/portal/dashboard/page.tsx +++ b/apps/web/src/app/(portal)/portal/dashboard/page.tsx @@ -5,8 +5,8 @@ import Link from 'next/link'; import { api } from '@/lib/api-client'; import { useAuth } from '@/hooks/use-auth'; import { - BarChart, - Bar, + AreaChart, + Area, XAxis, YAxis, CartesianGrid, @@ -152,21 +152,28 @@ export default function PortalDashboardPage() { {earnings.length > 0 ? (
- + + + + + + + v >= 1000 ? `$${(v / 1000).toFixed(0)}k` : `$${v}`} + width={48} + tickFormatter={(v: number) => (v >= 1000 ? `$${(v / 1000).toFixed(0)}k` : `$${v}`)} /> [`$${Number(value ?? 0).toLocaleString('en-US', { minimumFractionDigits: 2 })}`, 'Earnings']} contentStyle={{ borderRadius: '8px', border: '1px solid #e2e8f0', fontSize: '13px' }} /> - - + +
) : ( diff --git a/apps/web/src/app/not-found.test.tsx b/apps/web/src/app/not-found.test.tsx new file mode 100644 index 0000000..c558634 --- /dev/null +++ b/apps/web/src/app/not-found.test.tsx @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import NotFound from './not-found'; +import AdminNotFound from './(admin)/not-found'; + +describe('NotFound (root)', () => { + it('shows branded 404 content', () => { + render(); + expect(screen.getByText('404')).toBeInTheDocument(); + expect(screen.getByText('Page not found')).toBeInTheDocument(); + // Brand mark is present so a logged-out 404 still looks on-brand. + expect(screen.getByText('ContractorOS')).toBeInTheDocument(); + }); + + it('routes the primary action to "/" so it is safe in any auth state', () => { + render(); + const cta = screen.getByRole('link', { name: /back to home/i }); + expect(cta).toHaveAttribute('href', '/'); + }); +}); + +describe('AdminNotFound', () => { + it('renders inside the admin shell and links back to the dashboard', () => { + render(); + expect(screen.getByText('404')).toBeInTheDocument(); + expect( + screen.getByRole('link', { name: /go to dashboard/i }), + ).toHaveAttribute('href', '/dashboard'); + }); +}); diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx index 2740a85..073da0c 100644 --- a/apps/web/src/app/not-found.tsx +++ b/apps/web/src/app/not-found.tsx @@ -2,19 +2,30 @@ import Link from 'next/link'; export default function NotFound() { return ( -
-
-

404

-

Page not found

-

- The page you are looking for does not exist. -

- - Go to Dashboard +
+
+ +
+ C +
+

ContractorOS

+

+ Unified contractor lifecycle platform +

+
+

404

+

Page not found

+

+ The page you are looking for doesn't exist or may have been moved. +

+ + Back to home + +
); diff --git a/apps/web/src/components/layout/header.test.tsx b/apps/web/src/components/layout/header.test.tsx new file mode 100644 index 0000000..665138d --- /dev/null +++ b/apps/web/src/components/layout/header.test.tsx @@ -0,0 +1,56 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; + +let mockPathname = '/dashboard'; + +vi.mock('next/navigation', () => ({ + usePathname: () => mockPathname, + useRouter: () => ({ push: vi.fn(), back: vi.fn(), replace: vi.fn() }), +})); + +vi.mock('@/hooks/use-auth', () => ({ + useAuth: () => ({ + user: { firstName: 'Sarah', lastName: 'Chen', email: 'sarah@acme.test' }, + isLoading: false, + login: vi.fn(), + logout: vi.fn(), + }), +})); + +// The notification dropdown fetches over the network on mount; stub it so the +// Header renders in isolation. +vi.mock('@/components/notifications/notification-dropdown', () => ({ + NotificationDropdown: () => null, +})); + +import { Header } from './header'; + +describe('Header', () => { + beforeEach(() => { + mockPathname = '/dashboard'; + }); + + it('renders the search control as an accessible link to the contractor directory on admin routes', () => { + mockPathname = '/dashboard'; + render(
); + + const search = screen.getByRole('link', { name: /search contractors/i }); + expect(search).toHaveAttribute('href', '/contractors'); + }); + + it('scopes the search control to invoices inside the contractor portal', () => { + mockPathname = '/portal/dashboard'; + render(
); + + const search = screen.getByRole('link', { name: /search invoices/i }); + expect(search).toHaveAttribute('href', '/portal/invoices'); + }); + + it('exposes an accessible name on the mobile navigation toggle', () => { + render(
); + + expect( + screen.getByRole('button', { name: /open navigation menu/i }), + ).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/components/layout/header.tsx b/apps/web/src/components/layout/header.tsx index 8a550ed..7b6ed68 100644 --- a/apps/web/src/components/layout/header.tsx +++ b/apps/web/src/components/layout/header.tsx @@ -66,6 +66,10 @@ interface HeaderProps { export function Header({ onMenuToggle }: HeaderProps) { const breadcrumbs = useBreadcrumbs(); + const pathname = usePathname(); + const isPortal = pathname.startsWith('/portal'); + const searchHref = isPortal ? '/portal/invoices' : '/contractors'; + const searchLabel = isPortal ? 'Search invoices' : 'Search contractors'; return (
@@ -73,7 +77,9 @@ export function Header({ onMenuToggle }: HeaderProps) {
{onMenuToggle && ( +