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
This commit is contained in:
Adam Moussa 2026-05-22 17:29:37 -04:00
parent ef052a81ac
commit 85bed9a161
9 changed files with 240 additions and 46 deletions

View file

@ -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); });

View file

@ -1,8 +1,5 @@
import { Routes, Route, Navigate } from 'react-router-dom'; import { Routes, Route, Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { Box, Toolbar, Typography } from '@mui/material'; 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 ProtectedRoute, { RoleGuard } from './components/ProtectedRoute';
import Topbar from './components/Topbar'; import Topbar from './components/Topbar';
import Sidebar from './components/Sidebar'; import Sidebar from './components/Sidebar';
@ -14,11 +11,7 @@ import ProposalDetailPage from './pages/proposals/detail/ProposalDetailPage';
import ProposalListPage from './pages/proposals/list/ProposalListPage'; import ProposalListPage from './pages/proposals/list/ProposalListPage';
import AdminDashboard from './pages/admin/dashboard/AdminDashboard'; import AdminDashboard from './pages/admin/dashboard/AdminDashboard';
import AdminWorkspace from './pages/admin/workspace/AdminWorkspace'; import AdminWorkspace from './pages/admin/workspace/AdminWorkspace';
import { DRAWER_WIDTH } from './constants';
export default function App() { export default function App() {
const sidebarOpen = useSelector((state: RootState) => selectSidebarOpen(state));
return ( return (
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
@ -36,8 +29,6 @@ export default function App() {
sx={{ sx={{
flexGrow: 1, flexGrow: 1,
p: 1.25, p: 1.25,
ml: sidebarOpen ? `${DRAWER_WIDTH}px` : 0,
transition: 'margin-left 250ms ease',
minHeight: '100vh', minHeight: '100vh',
}} }}
> >
@ -52,7 +43,19 @@ export default function App() {
<Route path="/admin" element={<RoleGuard roles={['Admin', 'SysAdmin']}><AdminDashboard defaultStatus="InReview" /></RoleGuard>} /> <Route path="/admin" element={<RoleGuard roles={['Admin', 'SysAdmin']}><AdminDashboard defaultStatus="InReview" /></RoleGuard>} />
<Route path="/admin/proposals" element={<RoleGuard roles={['Admin', 'SysAdmin']}><AdminDashboard /></RoleGuard>} /> <Route path="/admin/proposals" element={<RoleGuard roles={['Admin', 'SysAdmin']}><AdminDashboard /></RoleGuard>} />
<Route path="/admin/proposals/:id" element={<RoleGuard roles={['Admin', 'SysAdmin']}><AdminWorkspace /></RoleGuard>} /> <Route path="/admin/proposals/:id" element={<RoleGuard roles={['Admin', 'SysAdmin']}><AdminWorkspace /></RoleGuard>} />
<Route path="/admin/users" element={<RoleGuard roles={['SysAdmin']}><Typography variant="h5" sx={{ p: 2 }}>User Management — Coming Soon</Typography></RoleGuard>} /> <Route path="/admin/users" element={
<RoleGuard roles={['SysAdmin']}>
<Box sx={{ display: 'flex', justifyContent: 'center', pt: 6 }}>
<Box sx={{ textAlign: 'center', maxWidth: 400 }}>
<Typography variant="h5" sx={{ mb: 1 }}>User Management</Typography>
<Typography variant="h6" color="text.secondary" sx={{ mb: 2, fontWeight: 400 }}>Coming Soon</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Users and roles are currently managed in Cognito. Contact the system administrator to update access.
</Typography>
</Box>
</Box>
</RoleGuard>
} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>

View file

@ -62,7 +62,7 @@ export default function SimilarProposalsPanel({ proposalId, onPullLineItem, disa
{(!similar || similar.length === 0) && ( {(!similar || similar.length === 0) && (
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
No similar proposals found. AI suggestions will populate this panel once the RAG engine is active. Similar proposals will appear here when available.
</Typography> </Typography>
)} )}

View file

@ -2,6 +2,10 @@ export function formatCurrency(amount: number): string {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); 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 { export function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
} }

View file

@ -18,6 +18,7 @@ import {
TextField, TextField,
InputAdornment, InputAdornment,
MenuItem, MenuItem,
Button,
Skeleton, Skeleton,
} from '@mui/material'; } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search'; 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 { adminApi, type DashboardStats } from '../../../lib/api/admin';
import { QUERY_KEYS } from '../../../constants/queryKeys'; 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 { 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'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle';
function StatCard({ icon, label, value, color }: { icon: React.ReactNode; label: string; value: string; color: string }) { 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
<MenuItem key={s} value={s}>{STATUS_LABELS[s] || s}</MenuItem> <MenuItem key={s} value={s}>{STATUS_LABELS[s] || s}</MenuItem>
))} ))}
</TextField> </TextField>
{(statusFilter || categoryFilter || priorityFilter || search) && (
<Button
size="small"
onClick={() => { setStatusFilter(defaultStatus ?? ''); setCategoryFilter(''); setPriorityFilter(''); setSearch(''); }}
>
Clear Filters
</Button>
)}
</Box> </Box>
{err && ( {err && (
@ -246,7 +255,7 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
<TableCell> <TableCell>
<Chip label={STATUS_LABELS[row.status] || row.status} size="small" color={STATUS_COLORS[row.status] || 'default'} /> <Chip label={STATUS_LABELS[row.status] || row.status} size="small" color={STATUS_COLORS[row.status] || 'default'} />
</TableCell> </TableCell>
<TableCell align="right">{formatCurrency(row.totalBidAmount)}</TableCell> <TableCell align="right">{formatBidAmount(row.totalBidAmount)}</TableCell>
<TableCell>{formatDate(row.submittedAt)}</TableCell> <TableCell>{formatDate(row.submittedAt)}</TableCell>
<TableCell>{row.submittedByName || '-'}</TableCell> <TableCell>{row.submittedByName || '-'}</TableCell>
</TableRow> </TableRow>
@ -254,7 +263,16 @@ export default function AdminDashboard({ defaultStatus }: { defaultStatus?: stri
{!loading && rows.length === 0 && ( {!loading && rows.length === 0 && (
<TableRow> <TableRow>
<TableCell colSpan={9} align="center" sx={{ py: 4 }}> <TableCell colSpan={9} align="center" sx={{ py: 4 }}>
<Typography color="text.secondary">No proposals in queue</Typography> <Typography color="text.secondary" sx={{ mb: 1 }}>
{(statusFilter || categoryFilter || priorityFilter || search)
? 'No proposals match your current filters.'
: 'No proposals in queue.'}
</Typography>
{(statusFilter || categoryFilter || priorityFilter || search) && (
<Button size="small" onClick={() => { setStatusFilter(defaultStatus ?? ''); setCategoryFilter(''); setPriorityFilter(''); setSearch(''); }}>
Clear Filters
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}

View file

@ -31,6 +31,7 @@ import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from '@mui/icons-material/Download';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import UndoIcon from '@mui/icons-material/Undo'; import UndoIcon from '@mui/icons-material/Undo';
import Tooltip from '@mui/material/Tooltip';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { proposalsApi, type ProposalDetail } from '../../../lib/api/proposals'; import { proposalsApi, type ProposalDetail } from '../../../lib/api/proposals';
import { lineItemsApi, type LineItem } from '../../../lib/api/lineItems'; import { lineItemsApi, type LineItem } from '../../../lib/api/lineItems';
@ -517,25 +518,33 @@ export default function AdminWorkspace() {
onClick={() => regenerateMutation.mutate()} onClick={() => regenerateMutation.mutate()}
disabled={regenerateMutation.isPending} disabled={regenerateMutation.isPending}
> >
Regenerate Regenerate Suggested Line Items
</Button>
<Button
variant="contained"
startIcon={<SaveIcon />}
onClick={() => saveMutation.mutate()}
disabled={saveMutation.isPending || !dirty}
>
Save
</Button>
<Button
variant="contained"
color="success"
startIcon={<CheckCircleIcon />}
onClick={() => setApproveDialogOpen(true)}
disabled={lineItems.length === 0 || bidTotal <= 0}
>
Approve
</Button> </Button>
<Tooltip title={!dirty ? 'No unsaved changes' : ''}>
<span>
<Button
variant="contained"
startIcon={<SaveIcon />}
onClick={() => saveMutation.mutate()}
disabled={saveMutation.isPending || !dirty}
>
Save
</Button>
</span>
</Tooltip>
<Tooltip title={lineItems.length === 0 ? 'Add at least one line item before approving' : bidTotal <= 0 ? 'Total bid must be greater than $0' : ''}>
<span>
<Button
variant="contained"
color="success"
startIcon={<CheckCircleIcon />}
onClick={() => setApproveDialogOpen(true)}
disabled={lineItems.length === 0 || bidTotal <= 0}
>
Approve
</Button>
</span>
</Tooltip>
</> </>
)} )}

View file

@ -111,7 +111,7 @@ export default function LoginPage() {
{isDevMode ? ( {isDevMode ? (
<> <>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Development Mode — Select a role to sign in as: Development Sign-In — Select a role to preview the app.
</Typography> </Typography>
<Stack spacing={2}> <Stack spacing={2}>
<Button <Button
@ -123,7 +123,10 @@ export default function LoginPage() {
disabled={loading} disabled={loading}
sx={{ py: 1.5, bgcolor: '#003A5D', '&:hover': { bgcolor: '#00527d' } }} sx={{ py: 1.5, bgcolor: '#003A5D', '&:hover': { bgcolor: '#00527d' } }}
> >
Sign in as SysAdmin <Box sx={{ textAlign: 'left', flex: 1 }}>
<Box>SysAdmin</Box>
<Box sx={{ fontSize: '11px', fontWeight: 400, opacity: 0.8 }}>Full access</Box>
</Box>
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
@ -132,25 +135,31 @@ export default function LoginPage() {
startIcon={<EngineeringIcon />} startIcon={<EngineeringIcon />}
onClick={() => handleDevLogin('Admin')} onClick={() => handleDevLogin('Admin')}
disabled={loading} disabled={loading}
sx={{ py: 1.5, bgcolor: '#0c4f6f', '&:hover': { bgcolor: '#083a52' } }} sx={{ py: 1.5, bgcolor: '#003A5D', '&:hover': { bgcolor: '#00527d' } }}
> >
Sign in as Admin <Box sx={{ textAlign: 'left', flex: 1 }}>
<Box>Admin</Box>
<Box sx={{ fontSize: '11px', fontWeight: 400, opacity: 0.8 }}>Review and approve proposals</Box>
</Box>
</Button> </Button>
<Button <Button
variant="outlined" variant="contained"
size="large" size="large"
fullWidth fullWidth
startIcon={<PersonIcon />} startIcon={<PersonIcon />}
onClick={() => handleDevLogin('Dispatcher')} onClick={() => handleDevLogin('Dispatcher')}
disabled={loading} disabled={loading}
sx={{ py: 1.5 }} sx={{ py: 1.5, bgcolor: '#003A5D', '&:hover': { bgcolor: '#00527d' } }}
> >
Sign in as Dispatcher <Box sx={{ textAlign: 'left', flex: 1 }}>
<Box>Dispatcher</Box>
<Box sx={{ fontSize: '11px', fontWeight: 400, opacity: 0.8 }}>Submit and track proposals</Box>
</Box>
</Button> </Button>
</Stack> </Stack>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
Dev login is only available when VITE_COGNITO_CLIENT_ID is not set Dev login is only shown when Cognito is not configured.
</Typography> </Typography>
</> </>
) : ( ) : (

View file

@ -24,7 +24,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from '@mui/icons-material/Download';
import { proposalsApi, type ProposalDetail, type PdfVersion } from '../../../lib/api/proposals'; import { proposalsApi, type ProposalDetail, type PdfVersion } from '../../../lib/api/proposals';
import { STATUS_COLORS, STATUS_LABELS, PRIORITY_LABELS } from '../../../constants'; 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'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle';
const STATUS_ORDER = ['Draft', 'InReview', 'Approved', 'Sent']; const STATUS_ORDER = ['Draft', 'InReview', 'Approved', 'Sent'];
@ -181,7 +181,7 @@ export default function ProposalDetailPage() {
<InfoRow label="Revision" value={`#${proposal.currentRevision}`} /> <InfoRow label="Revision" value={`#${proposal.currentRevision}`} />
<InfoRow label="Submitted By" value={proposal.submittedByName} /> <InfoRow label="Submitted By" value={proposal.submittedByName} />
<InfoRow label="Submitted" value={formatDateTime(proposal.submittedAt)} /> <InfoRow label="Submitted" value={formatDateTime(proposal.submittedAt)} />
<InfoRow label="Bid Amount" value={formatCurrency(proposal.totalBidAmount)} /> <InfoRow label="Bid Amount" value={formatBidAmount(proposal.totalBidAmount)} />
</Grid> </Grid>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />

View file

@ -27,7 +27,7 @@ import ReportIcon from '@mui/icons-material/Report';
import { usePaginatedList } from '../../../hooks/usePaginatedList'; import { usePaginatedList } from '../../../hooks/usePaginatedList';
import { proposalsApi, type ProposalListItem } from '../../../lib/api/proposals'; 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 { 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'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle';
type SortKey = keyof ProposalListItem; type SortKey = keyof ProposalListItem;
@ -161,6 +161,14 @@ export default function ProposalListPage() {
<MenuItem key={p} value={p}>{PRIORITY_LABELS[p] || p}</MenuItem> <MenuItem key={p} value={p}>{PRIORITY_LABELS[p] || p}</MenuItem>
))} ))}
</TextField> </TextField>
{(filterStatus || filterCategory || filterPriority || search) && (
<Button
size="small"
onClick={() => { setFilterStatus(''); setFilterCategory(''); setFilterPriority(''); setSearch(''); }}
>
Clear Filters
</Button>
)}
</Box> </Box>
{err && ( {err && (
@ -223,16 +231,28 @@ export default function ProposalListPage() {
color={STATUS_COLORS[row.status] || 'default'} color={STATUS_COLORS[row.status] || 'default'}
/> />
</TableCell> </TableCell>
<TableCell align="right">{formatCurrency(row.totalBidAmount)}</TableCell> <TableCell align="right">{formatBidAmount(row.totalBidAmount)}</TableCell>
<TableCell>{formatDate(row.submittedAt)}</TableCell> <TableCell>{formatDate(row.submittedAt)}</TableCell>
</TableRow> </TableRow>
))} ))}
{!loading && rows.length === 0 && ( {!loading && rows.length === 0 && (
<TableRow> <TableRow>
<TableCell colSpan={8} align="center" sx={{ py: 4 }}> <TableCell colSpan={8} align="center" sx={{ py: 4 }}>
<Typography color="text.secondary"> <Typography color="text.secondary" sx={{ mb: 1 }}>
No proposals found. Create your first one! {(filterStatus || filterCategory || filterPriority || search)
? 'No proposals match your current filters.'
: 'You have not submitted any proposals yet.'}
</Typography> </Typography>
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'center' }}>
{(filterStatus || filterCategory || filterPriority || search) && (
<Button size="small" onClick={() => { setFilterStatus(''); setFilterCategory(''); setFilterPriority(''); setSearch(''); }}>
Clear Filters
</Button>
)}
<Button size="small" variant="contained" startIcon={<AddCircleIcon />} onClick={() => navigate('/proposals/new')}>
New Proposal
</Button>
</Box>
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}