diff --git a/web/src/components/admin/LineItemEditor.tsx b/web/src/components/admin/LineItemEditor.tsx
index 53535e6..42e0674 100644
--- a/web/src/components/admin/LineItemEditor.tsx
+++ b/web/src/components/admin/LineItemEditor.tsx
@@ -15,6 +15,7 @@ import {
Tooltip,
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
+import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import DeleteIcon from '@mui/icons-material/Delete';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
@@ -88,6 +89,25 @@ export default function LineItemEditor({ items, onChange, disabled = false }: Li
onChange(updated.map((item, i) => ({ ...item, sortOrder: i + 1 })));
};
+ const duplicateItem = (index: number) => {
+ const source = items[index];
+ if (!source) return;
+ const newItem: EditableLineItem = {
+ id: null,
+ description: source.description,
+ quantity: source.quantity,
+ unit: source.unit,
+ unitPrice: source.unitPrice,
+ totalPrice: source.totalPrice,
+ pricingMode: source.pricingMode,
+ sortOrder: 0,
+ source: 'Manual',
+ };
+ const updated = [...items];
+ updated.splice(index + 1, 0, newItem);
+ onChange(updated.map((item, i) => ({ ...item, sortOrder: i + 1 })));
+ };
+
const moveItem = (index: number, direction: -1 | 1) => {
const targetIndex = index + direction;
if (targetIndex < 0 || targetIndex >= items.length) return;
@@ -109,8 +129,8 @@ export default function LineItemEditor({ items, onChange, disabled = false }: Li
Line Items ({items.length})
{!disabled && (
- } onClick={addItem}>
- Add Row
+ } onClick={addItem}>
+ Add Line Item
)}
@@ -126,7 +146,7 @@ export default function LineItemEditor({ items, onChange, disabled = false }: Li
Unit Price
Total
Source
- {!disabled && Actions}
+ {!disabled && Actions}
@@ -217,6 +237,11 @@ export default function LineItemEditor({ items, onChange, disabled = false }: Li
{!disabled && (
+
+ duplicateItem(index)}>
+
+
+
removeItem(index)}>
@@ -228,10 +253,16 @@ export default function LineItemEditor({ items, onChange, disabled = false }: Li
))}
{items.length === 0 && (
-
-
- No line items yet. {!disabled && 'Click "Add Row" to start.'}
+
+ No line items yet
+
+ Add labor, material, equipment, or other cost items.
+ {!disabled && (
+ } onClick={addItem}>
+ Add Line Item
+
+ )}
)}
@@ -241,7 +272,7 @@ export default function LineItemEditor({ items, onChange, disabled = false }: Li
- Total: {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(subtotal)}
+ Proposal Total: {subtotal === 0 ? 'Pending pricing' : new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(subtotal)}
diff --git a/web/src/pages/admin/workspace/AdminWorkspace.tsx b/web/src/pages/admin/workspace/AdminWorkspace.tsx
index 5eff598..9a398bf 100644
--- a/web/src/pages/admin/workspace/AdminWorkspace.tsx
+++ b/web/src/pages/admin/workspace/AdminWorkspace.tsx
@@ -17,10 +17,13 @@ import {
DialogActions,
CircularProgress,
Alert,
+ Tooltip,
+ Link,
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import SaveIcon from '@mui/icons-material/Save';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import SendIcon from '@mui/icons-material/Send';
import RefreshIcon from '@mui/icons-material/Refresh';
import HistoryIcon from '@mui/icons-material/History';
@@ -59,6 +62,14 @@ export default function AdminWorkspace() {
enabled: !!id,
});
+ const { data: similarProposals } = useQuery({
+ queryKey: ['similarProposals', id],
+ queryFn: () => adminApi.getSimilar(id!),
+ enabled: !!id,
+ });
+
+ const hasSimilar = similarProposals && similarProposals.length > 0;
+
useEffect(() => {
if (fetchedLineItems) {
setLineItems(
@@ -214,13 +225,21 @@ export default function AdminWorkspace() {
return (
{/* Header */}
-
- } onClick={() => navigate('/admin')}>
- Back
-
-
- {proposal.proposalNumber}
-
+
+
+ navigate('/admin')}
+ sx={{ fontSize: 14, color: '#0B5A73', cursor: 'pointer' }}
+ >
+ Admin Queue
+
+
+
+ {proposal.proposalNumber}
+
+
@@ -255,7 +274,7 @@ export default function AdminWorkspace() {
{/* Left Panel: Reference */}
-
+
Submission Details
@@ -290,7 +309,7 @@ export default function AdminWorkspace() {
{/* Center Panel: Editor */}
-
+
Refined Scope
@@ -315,28 +334,30 @@ export default function AdminWorkspace() {
- {/* Right Panel: Similar Proposals */}
-
-
-
- {
- const maxSort = lineItems.length > 0 ? Math.max(...lineItems.map((i) => i.sortOrder)) : 0;
- handleLineItemsChange([
- ...lineItems,
- { ...item, id: null, sortOrder: maxSort + 1, source: 'Historical' },
- ]);
- }}
- disabled={!isEditable}
- />
-
-
-
+ {/* Right Panel: Similar Proposals (hidden when empty) */}
+ {hasSimilar && (
+
+
+
+ {
+ const maxSort = lineItems.length > 0 ? Math.max(...lineItems.map((i) => i.sortOrder)) : 0;
+ handleLineItemsChange([
+ ...lineItems,
+ { ...item, id: null, sortOrder: maxSort + 1, source: 'Historical' },
+ ]);
+ }}
+ disabled={!isEditable}
+ />
+
+
+
+ )}
{/* Action Bar */}
-
+
{dirty && (
Unsaved changes
@@ -353,23 +374,59 @@ export default function AdminWorkspace() {
>
Regenerate
- }
- onClick={() => saveMutation.mutate()}
- disabled={saveMutation.isPending || !dirty}
+
- Save
-
- }
- onClick={() => setApproveDialogOpen(true)}
- disabled={lineItems.length === 0 || bidTotal <= 0}
+
+ }
+ onClick={() => saveMutation.mutate()}
+ disabled={saveMutation.isPending || !dirty}
+ >
+ Save
+
+
+
+ 0 && bidTotal > 0}
>
- Approve
-
+
+ }
+ onClick={() => setApproveDialogOpen(true)}
+ disabled={lineItems.length === 0 || bidTotal <= 0}
+ >
+ Approve & Send
+
+
+
+ 0 && bidTotal > 0}
+ >
+
+ }
+ onClick={() => {
+ approveMutation.mutate(undefined, {
+ onSuccess: () => {
+ navigate('/admin');
+ },
+ });
+ }}
+ disabled={lineItems.length === 0 || bidTotal <= 0 || approveMutation.isPending}
+ >
+ Approve & Next
+
+
+
>
)}