From 85bed9a16184dba57983b9d50320789e062ceaea Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Fri, 22 May 2026 17:29:37 -0400 Subject: [PATCH] Apply UX quick wins from external review - Replace $0.00 with "Not priced" via formatBidAmount helper - Consistent login buttons with role descriptions - Context-aware empty states (filter mismatch vs no data) - Clear Filters button on proposal list and admin dashboard - Rename "Regenerate" to "Regenerate Suggested Line Items" - Add tooltips explaining disabled Save/Approve buttons - Replace "RAG engine" jargon with plain language - Improve User Management placeholder with Cognito guidance - Add Puppeteer screenshot script for all roles/pages --- scripts/screenshot-pages.mjs | 131 ++++++++++++++++++ web/src/App.tsx | 23 +-- .../admin/SimilarProposalsPanel.tsx | 2 +- web/src/lib/format.ts | 4 + .../pages/admin/dashboard/AdminDashboard.tsx | 24 +++- .../pages/admin/workspace/AdminWorkspace.tsx | 45 +++--- web/src/pages/auth/LoginPage.tsx | 25 ++-- .../proposals/detail/ProposalDetailPage.tsx | 4 +- .../pages/proposals/list/ProposalListPage.tsx | 28 +++- 9 files changed, 240 insertions(+), 46 deletions(-) create mode 100644 scripts/screenshot-pages.mjs diff --git a/scripts/screenshot-pages.mjs b/scripts/screenshot-pages.mjs new file mode 100644 index 0000000..ab62763 --- /dev/null +++ b/scripts/screenshot-pages.mjs @@ -0,0 +1,131 @@ +import puppeteer from 'puppeteer'; + +const BASE = 'http://localhost:5173'; +const OUT = process.env.HOME + '/Desktop/proposal-system-screenshots'; + +const ROLES = [ + { label: 'SysAdmin', prefix: 'sysadmin', isAdmin: true, isSysAdmin: true }, + { label: 'Admin', prefix: 'admin', isAdmin: true, isSysAdmin: false }, + { label: 'Dispatcher', prefix: 'dispatcher', isAdmin: false, isSysAdmin: false }, +]; + +async function wait(ms) { return new Promise(r => setTimeout(r, ms)); } + +async function screenshot(page, name) { + await wait(1500); + await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: true }); + console.log(` captured ${name}.png`); +} + +async function getToken(page) { + return page.evaluate(() => { + const raw = localStorage.getItem('proposal_system_token'); + if (!raw) return null; + try { + const parsed = JSON.parse(raw); + return parsed?.token ?? null; + } catch { return null; } + }); +} + +async function fetchProposalId(page) { + const token = await getToken(page); + if (!token) return null; + return page.evaluate(async (t) => { + try { + const r = await fetch('/api/proposals?page=1&pageSize=1', { + headers: { 'Authorization': `Bearer ${t}` }, + }); + if (!r.ok) return null; + const data = await r.json(); + return data.items?.[0]?.id ?? null; + } catch { return null; } + }, token); +} + +async function run() { + const browser = await puppeteer.launch({ + headless: true, + defaultViewport: { width: 1440, height: 900 }, + }); + + for (const role of ROLES) { + console.log(`\n--- ${role.prefix} ---`); + + const context = await browser.createBrowserContext(); + const page = await context.newPage(); + + // Login page + await page.goto(BASE + '/login', { waitUntil: 'networkidle2' }); + await screenshot(page, `${role.prefix}-01-login`); + + // Click the role button + const buttons = await page.$$('button'); + let clicked = false; + for (const b of buttons) { + const text = await b.evaluate(el => el.textContent.trim()); + if (text.startsWith(role.label)) { + await b.click(); + clicked = true; + break; + } + } + if (!clicked) { + console.log(` ERROR: could not find button "${role.label}"`); + await context.close(); + continue; + } + + await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 }).catch(() => {}); + await wait(1000); + + // Dashboard + await screenshot(page, `${role.prefix}-02-dashboard`); + + // My Proposals list + await page.goto(BASE + '/proposals', { waitUntil: 'networkidle2' }); + await screenshot(page, `${role.prefix}-03-proposals-list`); + + // New Proposal form + await page.goto(BASE + '/proposals/new', { waitUntil: 'networkidle2' }); + await screenshot(page, `${role.prefix}-04-new-proposal`); + + // Detail page + const proposalId = await fetchProposalId(page); + if (proposalId) { + await page.goto(BASE + `/proposals/${proposalId}`, { waitUntil: 'networkidle2' }); + await screenshot(page, `${role.prefix}-05-proposal-detail`); + } else { + console.log(' no proposals found, skipping detail'); + } + + if (role.isAdmin) { + await page.goto(BASE + '/admin', { waitUntil: 'networkidle2' }); + await screenshot(page, `${role.prefix}-06-admin-queue`); + + await page.goto(BASE + '/admin/proposals', { waitUntil: 'networkidle2' }); + await screenshot(page, `${role.prefix}-07-admin-all-proposals`); + + const workspaceId = proposalId ?? await fetchProposalId(page); + if (workspaceId) { + await page.goto(BASE + `/admin/proposals/${workspaceId}`, { waitUntil: 'networkidle2' }); + await wait(2000); + await screenshot(page, `${role.prefix}-08-admin-workspace`); + } else { + console.log(' no proposals for workspace'); + } + + if (role.isSysAdmin) { + await page.goto(BASE + '/admin/users', { waitUntil: 'networkidle2' }); + await screenshot(page, `${role.prefix}-09-user-management`); + } + } + + await context.close(); + } + + await browser.close(); + console.log(`\nDone — screenshots in ${OUT}`); +} + +run().catch(e => { console.error(e); process.exit(1); }); diff --git a/web/src/App.tsx b/web/src/App.tsx index 66eb219..b8a30d8 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,8 +1,5 @@ import { Routes, Route, Navigate } from 'react-router-dom'; -import { useSelector } from 'react-redux'; import { Box, Toolbar, Typography } from '@mui/material'; -import { selectSidebarOpen } from './app/slices/uiSlice'; -import type { RootState } from './app/store'; import ProtectedRoute, { RoleGuard } from './components/ProtectedRoute'; import Topbar from './components/Topbar'; import Sidebar from './components/Sidebar'; @@ -14,11 +11,7 @@ import ProposalDetailPage from './pages/proposals/detail/ProposalDetailPage'; import ProposalListPage from './pages/proposals/list/ProposalListPage'; import AdminDashboard from './pages/admin/dashboard/AdminDashboard'; import AdminWorkspace from './pages/admin/workspace/AdminWorkspace'; -import { DRAWER_WIDTH } from './constants'; - export default function App() { - const sidebarOpen = useSelector((state: RootState) => selectSidebarOpen(state)); - return ( } /> @@ -36,8 +29,6 @@ export default function App() { sx={{ flexGrow: 1, p: 1.25, - ml: sidebarOpen ? `${DRAWER_WIDTH}px` : 0, - transition: 'margin-left 250ms ease', minHeight: '100vh', }} > @@ -52,7 +43,19 @@ export default function App() { } /> } /> } /> - User Management — Coming Soon} /> + + + + User Management + Coming Soon + + Users and roles are currently managed in Cognito. Contact the system administrator to update access. + + + + + } /> } /> diff --git a/web/src/components/admin/SimilarProposalsPanel.tsx b/web/src/components/admin/SimilarProposalsPanel.tsx index fdc4d6c..82b59c7 100644 --- a/web/src/components/admin/SimilarProposalsPanel.tsx +++ b/web/src/components/admin/SimilarProposalsPanel.tsx @@ -62,7 +62,7 @@ export default function SimilarProposalsPanel({ proposalId, onPullLineItem, disa {(!similar || similar.length === 0) && ( - No similar proposals found. AI suggestions will populate this panel once the RAG engine is active. + Similar proposals will appear here when available. )} diff --git a/web/src/lib/format.ts b/web/src/lib/format.ts index 3a72acc..d11a9cf 100644 --- a/web/src/lib/format.ts +++ b/web/src/lib/format.ts @@ -2,6 +2,10 @@ export function formatCurrency(amount: number): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); } +export function formatBidAmount(amount: number): string { + return amount > 0 ? formatCurrency(amount) : 'Not priced'; +} + export function formatDate(iso: string): string { return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } diff --git a/web/src/pages/admin/dashboard/AdminDashboard.tsx b/web/src/pages/admin/dashboard/AdminDashboard.tsx index 648e3ad..87efdb8 100644 --- a/web/src/pages/admin/dashboard/AdminDashboard.tsx +++ b/web/src/pages/admin/dashboard/AdminDashboard.tsx @@ -18,6 +18,7 @@ import { TextField, InputAdornment, MenuItem, + Button, Skeleton, } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; @@ -32,7 +33,7 @@ import { proposalsApi, type ProposalListItem } from '../../../lib/api/proposals' import { adminApi, type DashboardStats } from '../../../lib/api/admin'; import { QUERY_KEYS } from '../../../constants/queryKeys'; import { SERVICE_CATEGORIES, PRIORITIES, PROPOSAL_STATUSES, STATUS_COLORS, STATUS_LABELS, PRIORITY_LABELS, PRIORITY_COLORS, PRIORITY_ROW_SX } from '../../../constants'; -import { formatCurrency, formatDate } from '../../../lib/format'; +import { formatBidAmount, formatDate } from '../../../lib/format'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle'; function StatCard({ icon, label, value, color }: { icon: React.ReactNode; label: string; value: string; color: string }) { @@ -194,6 +195,14 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri {STATUS_LABELS[s] || s} ))} + {(statusFilter || categoryFilter || priorityFilter || search) && ( + + )} {err && ( @@ -246,7 +255,7 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri - {formatCurrency(row.totalBidAmount)} + {formatBidAmount(row.totalBidAmount)} {formatDate(row.submittedAt)} {row.submittedByName || '-'} @@ -254,7 +263,16 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri {!loading && rows.length === 0 && ( - No proposals in queue + + {(statusFilter || categoryFilter || priorityFilter || search) + ? 'No proposals match your current filters.' + : 'No proposals in queue.'} + + {(statusFilter || categoryFilter || priorityFilter || search) && ( + + )} )} diff --git a/web/src/pages/admin/workspace/AdminWorkspace.tsx b/web/src/pages/admin/workspace/AdminWorkspace.tsx index e2bff15..851a04a 100644 --- a/web/src/pages/admin/workspace/AdminWorkspace.tsx +++ b/web/src/pages/admin/workspace/AdminWorkspace.tsx @@ -31,6 +31,7 @@ import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; import DownloadIcon from '@mui/icons-material/Download'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import UndoIcon from '@mui/icons-material/Undo'; +import Tooltip from '@mui/material/Tooltip'; import { toast } from 'react-toastify'; import { proposalsApi, type ProposalDetail } from '../../../lib/api/proposals'; import { lineItemsApi, type LineItem } from '../../../lib/api/lineItems'; @@ -517,25 +518,33 @@ export default function AdminWorkspace() { onClick={() => regenerateMutation.mutate()} disabled={regenerateMutation.isPending} > - Regenerate - - - + + + + + + + + + + )} diff --git a/web/src/pages/auth/LoginPage.tsx b/web/src/pages/auth/LoginPage.tsx index ad9ff68..6aac422 100644 --- a/web/src/pages/auth/LoginPage.tsx +++ b/web/src/pages/auth/LoginPage.tsx @@ -111,7 +111,7 @@ export default function LoginPage() { {isDevMode ? ( <> - Development Mode — Select a role to sign in as: + Development Sign-In — Select a role to preview the app. - Dev login is only available when VITE_COGNITO_CLIENT_ID is not set + Dev login is only shown when Cognito is not configured. ) : ( diff --git a/web/src/pages/proposals/detail/ProposalDetailPage.tsx b/web/src/pages/proposals/detail/ProposalDetailPage.tsx index ac2e6a3..8707cd4 100644 --- a/web/src/pages/proposals/detail/ProposalDetailPage.tsx +++ b/web/src/pages/proposals/detail/ProposalDetailPage.tsx @@ -24,7 +24,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import DownloadIcon from '@mui/icons-material/Download'; import { proposalsApi, type ProposalDetail, type PdfVersion } from '../../../lib/api/proposals'; import { STATUS_COLORS, STATUS_LABELS, PRIORITY_LABELS } from '../../../constants'; -import { formatCurrency, formatDateTime } from '../../../lib/format'; +import { formatBidAmount, formatCurrency, formatDateTime } from '../../../lib/format'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle'; const STATUS_ORDER = ['Draft', 'InReview', 'Approved', 'Sent']; @@ -181,7 +181,7 @@ export default function ProposalDetailPage() { - + diff --git a/web/src/pages/proposals/list/ProposalListPage.tsx b/web/src/pages/proposals/list/ProposalListPage.tsx index 2929bcc..188dc64 100644 --- a/web/src/pages/proposals/list/ProposalListPage.tsx +++ b/web/src/pages/proposals/list/ProposalListPage.tsx @@ -27,7 +27,7 @@ import ReportIcon from '@mui/icons-material/Report'; import { usePaginatedList } from '../../../hooks/usePaginatedList'; import { proposalsApi, type ProposalListItem } from '../../../lib/api/proposals'; import { STATUS_COLORS, STATUS_LABELS, PROPOSAL_STATUSES, SERVICE_CATEGORIES, PRIORITIES, PRIORITY_LABELS, PRIORITY_COLORS, PRIORITY_ROW_SX } from '../../../constants'; -import { formatCurrency, formatDate } from '../../../lib/format'; +import { formatBidAmount, formatDate } from '../../../lib/format'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle'; type SortKey = keyof ProposalListItem; @@ -161,6 +161,14 @@ export default function ProposalListPage() { {PRIORITY_LABELS[p] || p} ))} + {(filterStatus || filterCategory || filterPriority || search) && ( + + )} {err && ( @@ -223,16 +231,28 @@ export default function ProposalListPage() { color={STATUS_COLORS[row.status] || 'default'} /> - {formatCurrency(row.totalBidAmount)} + {formatBidAmount(row.totalBidAmount)} {formatDate(row.submittedAt)} ))} {!loading && rows.length === 0 && ( - - No proposals found. Create your first one! + + {(filterStatus || filterCategory || filterPriority || search) + ? 'No proposals match your current filters.' + : 'You have not submitted any proposals yet.'} + + {(filterStatus || filterCategory || filterPriority || search) && ( + + )} + + )}