From 9859d898957656833806c365e28123d805d1f6bd Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Sat, 16 May 2026 22:11:08 -0400 Subject: [PATCH] Fix frontend navigation bugs, differentiate Dashboard from Proposals list - Fix double nav selection by adding isNavActive() with ALL_NAV_PATHS set - Fix /admin/users routing to placeholder instead of redirect to / - Fix ProposalDetailPage Back button navigating to / instead of /proposals - Differentiate Dashboard (KPI cards + recent 5) from ProposalListPage (full paginated table) - Dashboard now uses dedicated /proposals/stats endpoint for accurate counts - Fix adminApi.getPdf dead code (axios rejects before status check) - Wire up PDF generation button in AdminWorkspace - Adjust layout: 220px drawer, 10px content padding, 64px toolbar height --- web/src/App.tsx | 12 +- web/src/lib/api/admin.ts | 9 + web/src/lib/api/proposals.ts | 12 ++ .../pages/admin/workspace/AdminWorkspace.tsx | 20 ++- web/src/pages/dashboard/Dashboard.tsx | 162 +++++++++-------- .../proposals/detail/ProposalDetailPage.tsx | 6 +- .../pages/proposals/list/ProposalListPage.tsx | 167 +++++++++++++++++- 7 files changed, 305 insertions(+), 83 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 46d7756..99a77e4 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,6 +1,6 @@ import { Routes, Route, Navigate } from 'react-router-dom'; import { useSelector } from 'react-redux'; -import { Box, Toolbar } from '@mui/material'; +import { Box, Toolbar, Typography } from '@mui/material'; import { selectSidebarOpen } from './app/slices/uiSlice'; import type { RootState } from './app/store'; import ProtectedRoute from './components/ProtectedRoute'; @@ -15,7 +15,7 @@ import ProposalListPage from './pages/proposals/list/ProposalListPage'; import AdminDashboard from './pages/admin/dashboard/AdminDashboard'; import AdminWorkspace from './pages/admin/workspace/AdminWorkspace'; -const DRAWER_WIDTH = 240; +const DRAWER_WIDTH = 220; export default function App() { const sidebarOpen = useSelector((state: RootState) => selectSidebarOpen(state)); @@ -36,12 +36,13 @@ export default function App() { component="main" sx={{ flexGrow: 1, - p: 3, + p: '10px', ml: sidebarOpen ? `${DRAWER_WIDTH}px` : 0, - transition: 'margin-left 225ms cubic-bezier(0.4, 0, 0.6, 1)', + transition: 'margin-left 250ms ease', + minHeight: '100vh', }} > - + } /> } /> @@ -52,6 +53,7 @@ export default function App() { } /> } /> } /> + User Management — Coming Soon} /> } /> diff --git a/web/src/lib/api/admin.ts b/web/src/lib/api/admin.ts index be4ca1e..f7dfc06 100644 --- a/web/src/lib/api/admin.ts +++ b/web/src/lib/api/admin.ts @@ -64,4 +64,13 @@ export const adminApi = { const res = await apiClient.get(`/proposals/${id}/similar`); return res.data; }, + + getPdf: async (id: string): Promise<{ downloadUrl: string; expiresAt: string } | null> => { + try { + const res = await apiClient.get(`/proposals/${id}/pdf`); + return res.data; + } catch { + return null; + } + }, }; diff --git a/web/src/lib/api/proposals.ts b/web/src/lib/api/proposals.ts index a617eb4..09fdbb9 100644 --- a/web/src/lib/api/proposals.ts +++ b/web/src/lib/api/proposals.ts @@ -97,4 +97,16 @@ export const proposalsApi = { const res = await apiClient.post(`/proposals/${proposalId}/attachments?fileName=${encodeURIComponent(fileName)}`); return res.data; }, + + getStats: async (): Promise => { + const res = await apiClient.get('/proposals/stats'); + return res.data; + }, }; + +export interface ProposalStats { + totalCount: number; + inReviewCount: number; + approvedCount: number; + sentCount: number; +} diff --git a/web/src/pages/admin/workspace/AdminWorkspace.tsx b/web/src/pages/admin/workspace/AdminWorkspace.tsx index 488396d..680d916 100644 --- a/web/src/pages/admin/workspace/AdminWorkspace.tsx +++ b/web/src/pages/admin/workspace/AdminWorkspace.tsx @@ -146,6 +146,21 @@ export default function AdminWorkspace() { }, }); + const pdfMutation = useMutation({ + mutationFn: () => adminApi.getPdf(id!), + onSuccess: (data) => { + if (data?.downloadUrl) { + window.open(data.downloadUrl, '_blank'); + toast.success('PDF ready — downloading'); + } else { + toast.info('PDF generation queued — try again in a few seconds'); + } + }, + onError: () => { + toast.error('Failed to generate PDF'); + }, + }); + const handleLineItemsChange = (updated: EditableLineItem[]) => { setLineItems(updated); setDirty(true); @@ -342,9 +357,10 @@ export default function AdminWorkspace() { - - - setSearch(e.target.value)} - sx={{ mb: 2, width: 320 }} - slotProps={{ - input: { - startAdornment: ( - - - - ), - }, - }} - /> - - {err && ( - - {err} - + + + {isLoading ? ( + + ) : ( + } label="Total Submitted" value={String(total)} /> )} + + + {isLoading ? ( + + ) : ( + } label="In Review" value={String(counts.inReview)} /> + )} + + + {isLoading ? ( + + ) : ( + } label="Approved" value={String(counts.approved)} /> + )} + + + {isLoading ? ( + + ) : ( + } label="Sent" value={String(counts.sent)} /> + )} + + + + + + + Recent Proposals + + @@ -97,26 +137,22 @@ export default function Dashboard() { Proposal # Customer - WO # Category - Priority Status Bid Amount Submitted - {loading + {isLoading ? Array.from({ length: 5 }).map((_, i) => ( - {Array.from({ length: 8 }).map((_, j) => ( - - - + {Array.from({ length: 6 }).map((_, j) => ( + ))} )) - : rows.map((row) => ( + : proposals.map((row) => ( {row.proposalNumber} {row.customerName} - {row.workOrderNumber} {row.serviceCategory} - {row.priority} - + {formatCurrency(row.totalBidAmount)} {formatDate(row.submittedAt)} ))} - {!loading && rows.length === 0 && ( + {!isLoading && proposals.length === 0 && ( - + - No proposals found. Create your first one! + No proposals yet. Submit your first one! @@ -151,16 +181,6 @@ export default function Dashboard() {
- - setPage(newPage + 1)} - rowsPerPage={pageSize} - onRowsPerPageChange={(e) => setPageSize(parseInt(e.target.value, 10))} - rowsPerPageOptions={[12, 24, 48]} - />
diff --git a/web/src/pages/proposals/detail/ProposalDetailPage.tsx b/web/src/pages/proposals/detail/ProposalDetailPage.tsx index 276192a..cacbecc 100644 --- a/web/src/pages/proposals/detail/ProposalDetailPage.tsx +++ b/web/src/pages/proposals/detail/ProposalDetailPage.tsx @@ -77,8 +77,8 @@ export default function ProposalDetailPage() { {error instanceof Error ? error.message : 'Proposal not found'} - ); @@ -89,7 +89,7 @@ export default function ProposalDetailPage() { return ( - diff --git a/web/src/pages/proposals/list/ProposalListPage.tsx b/web/src/pages/proposals/list/ProposalListPage.tsx index 8196a26..a2197d9 100644 --- a/web/src/pages/proposals/list/ProposalListPage.tsx +++ b/web/src/pages/proposals/list/ProposalListPage.tsx @@ -1,5 +1,168 @@ -import Dashboard from '../../dashboard/Dashboard'; +import { useNavigate } from 'react-router-dom'; +import { + Box, + Typography, + Card, + CardContent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TablePagination, + Chip, + TextField, + InputAdornment, + Button, + Skeleton, +} from '@mui/material'; +import SearchIcon from '@mui/icons-material/Search'; +import AddCircleIcon from '@mui/icons-material/AddCircle'; +import { usePaginatedList } from '../../../hooks/usePaginatedList'; +import { proposalsApi, type ProposalListItem } from '../../../lib/api/proposals'; + +const STATUS_COLORS: Record = { + Draft: 'default', + InReview: 'info', + Approved: 'success', + Sent: 'success', + Revised: 'warning', +}; + +function formatCurrency(amount: number): string { + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); +} + +function formatDate(iso: string): string { + return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +} export default function ProposalListPage() { - return ; + const navigate = useNavigate(); + const { + rows, + search, + setSearch, + page, + setPage, + pageSize, + setPageSize, + totalCount, + loading, + err, + } = usePaginatedList(proposalsApi.getAll); + + return ( + + + My Proposals + + + + + + setSearch(e.target.value)} + sx={{ mb: 2, width: 320 }} + slotProps={{ + input: { + startAdornment: ( + + + + ), + }, + }} + /> + + {err && ( + + {err} + + )} + + + + + + Proposal # + Customer + WO # + Category + Priority + Status + Bid Amount + Submitted + + + + {loading + ? Array.from({ length: 5 }).map((_, i) => ( + + {Array.from({ length: 8 }).map((_, j) => ( + + + + ))} + + )) + : rows.map((row) => ( + navigate(`/proposals/${row.id}`)} + > + {row.proposalNumber} + {row.customerName} + {row.workOrderNumber} + {row.serviceCategory} + {row.priority} + + + + {formatCurrency(row.totalBidAmount)} + {formatDate(row.submittedAt)} + + ))} + {!loading && rows.length === 0 && ( + + + + No proposals found. Create your first one! + + + + )} + +
+
+ + setPage(newPage + 1)} + rowsPerPage={pageSize} + onRowsPerPageChange={(e) => setPageSize(parseInt(e.target.value, 10))} + rowsPerPageOptions={[12, 24, 48]} + /> +
+
+
+ ); }