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
))}
+ {(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
-
- }
- onClick={() => saveMutation.mutate()}
- disabled={saveMutation.isPending || !dirty}
- >
- Save
-
- }
- onClick={() => setApproveDialogOpen(true)}
- disabled={lineItems.length === 0 || bidTotal <= 0}
- >
- Approve
+ Regenerate Suggested Line Items
+
+
+ }
+ onClick={() => saveMutation.mutate()}
+ disabled={saveMutation.isPending || !dirty}
+ >
+ Save
+
+
+
+
+
+ }
+ onClick={() => setApproveDialogOpen(true)}
+ disabled={lineItems.length === 0 || bidTotal <= 0}
+ >
+ Approve
+
+
+
>
)}
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.
}
onClick={() => handleDevLogin('Admin')}
disabled={loading}
- sx={{ py: 1.5, bgcolor: '#0c4f6f', '&:hover': { bgcolor: '#083a52' } }}
+ sx={{ py: 1.5, bgcolor: '#003A5D', '&:hover': { bgcolor: '#00527d' } }}
>
- Sign in as Admin
+
+ Admin
+ Review and approve proposals
+
}
onClick={() => handleDevLogin('Dispatcher')}
disabled={loading}
- sx={{ py: 1.5 }}
+ sx={{ py: 1.5, bgcolor: '#003A5D', '&:hover': { bgcolor: '#00527d' } }}
>
- Sign in as Dispatcher
+
+ Dispatcher
+ Submit and track proposals
+
- 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() {
))}
+ {(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) && (
+
+ )}
+ } onClick={() => navigate('/proposals/new')}>
+ New Proposal
+
+
)}