Implement Phases 2-5: Frontend, AI/RAG, PDF Generation (#22)
* Fix NuGet versions and add InitialCreate EF Core migration
- Update AWSSDK.SQS and AWSSDK.SecretsManager to 3.7.500.0 (actual available versions)
- Update AWSSDK.Extensions.NETCore.Setup to 3.7.400
- Generate InitialCreate migration for PostgreSQL (all 8 entities)
- Build verified: 0 errors, 0 warnings
* Implement Dispatcher Frontend (Phase 2)
React 19 + MUI v7 + TypeScript + Vite SPA matching SHOC patterns:
Redux Toolkit (auth/ui slices), TanStack React Query, axios interceptors,
react-toastify, Cognito OAuth PKCE login flow, paginated proposal list,
new proposal form with customer autocomplete and vendor PDF upload,
read-only proposal detail with status stepper timeline.
* Add AuthController for Cognito code exchange and .env.example
Backend endpoint POST /api/auth/callback exchanges the OAuth
authorization code with Cognito's token endpoint, auto-provisions
the user in the DB, and returns the access token to the frontend.
* Implement Admin Frontend Experience (Phase 3)
Three-panel admin workspace: left reference panel (submission details,
vendor data), center editor (refined scope, inline line item table with
reorder/add/remove/pricing), right similar proposals panel (KB results
with pull-to-editor). Admin dashboard with stats cards and proposal
queue table. Approval flow with confirmation dialog, mark-as-sent,
and create-revision actions. Role-based sidebar navigation.
* Implement backend dev mode, internal API auth, and service layer enhancements
- Add dev-login endpoint with local JWT signing for local development
- Add InternalApiKeyMiddleware with timing-safe comparison for Lambda-to-API auth
- Add DevS3Service and NoOpJobPublisher for running without AWS services
- Implement CurrentUserService cascading user resolution (ID → sub → email → create)
- Add async ResolveAsync() to avoid synchronous DB calls in request pipeline
- Add /proposals/stats endpoint for efficient server-side status counts
- Guard status transitions: only allow Draft → InReview via update endpoint
- Add vendor proposals, generated PDFs, and similar proposals controllers
- Add ISimilarProposalService and SimilarProposalService
- Add [Authorize] to AddSimilarReference endpoint
* Implement Lambda functions for PDF processing, suggestions, and library ingest
- pdf-extract: Parse vendor PDFs with pdfplumber, fallback to Claude multimodal
- pdf-generate: Generate branded proposal PDFs with reportlab Platypus
- library-ingest: Format approved proposals as markdown and sync to Bedrock KB
- suggestions: Query KB for similar proposals, generate line items via Claude
- All Lambdas use internal API key auth and cold-start secret caching
- Fix pdf_path unbound variable in pdf-extract error handling
* Add Bedrock Knowledge Base, OpenSearch Serverless, and SQS message filtering
- Provision OpenSearch Serverless collection for vector search
- Create Bedrock Knowledge Base with Titan embedding model
- Configure S3 data source with fixed-size chunking (512 tokens, 20% overlap)
- Add suggestions Lambda with SQS event source filtering
- Scope bedrock:InvokeModel IAM to specific model ARN patterns
- Add internal API key secret in Secrets Manager
- Add log retention (2 months) to all Lambda functions
- Add docker-compose.yml for local PostgreSQL
* Apply SHOC design system styling across frontend
- Rewrite theme with SHOC palette (#0c4f6f primary, Nunito font, 4px radius)
- Add global CSS with Google Fonts import for Nunito
- Redesign Topbar with avatar initials, role subtitle, gradient header
- Redesign Sidebar with 220px width, section headers, active state border
- Restyle LoginPage with SHOC branded card and dev-mode role selector
- Update AdminDashboard KPI cards to centered SHOC style
- Add devLogin API method for local development auth flow
* 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
* Add appsettings.Development.json to gitignore
Prevent dev-only signing keys and connection strings from being committed.
* Fix CI failures: unused Python imports and CDK synth asset path
CDK synth job needs the .NET API published first so the Lambda asset
path exists. Python lint had 3 unused imports in pdf-generate.
* Apply ruff formatting to all Lambda Python files
2026-05-17 13:06:23 -04:00
|
|
|
import {
|
|
|
|
|
Box,
|
|
|
|
|
Table,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableContainer,
|
|
|
|
|
TableHead,
|
|
|
|
|
TableRow,
|
|
|
|
|
TextField,
|
|
|
|
|
IconButton,
|
|
|
|
|
MenuItem,
|
|
|
|
|
Chip,
|
|
|
|
|
Typography,
|
|
|
|
|
Button,
|
|
|
|
|
Tooltip,
|
|
|
|
|
} from '@mui/material';
|
|
|
|
|
import AddIcon from '@mui/icons-material/Add';
|
|
|
|
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
|
|
|
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
|
|
|
|
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
|
|
|
|
import type { PricingMode, LineItemSource } from '../../lib/api/lineItems';
|
|
|
|
|
|
|
|
|
|
export interface EditableLineItem {
|
|
|
|
|
id: string | null;
|
|
|
|
|
description: string;
|
|
|
|
|
quantity: number;
|
|
|
|
|
unit: string;
|
|
|
|
|
unitPrice: number | null;
|
|
|
|
|
totalPrice: number;
|
|
|
|
|
pricingMode: PricingMode;
|
|
|
|
|
sortOrder: number;
|
|
|
|
|
source: LineItemSource;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface LineItemEditorProps {
|
|
|
|
|
items: EditableLineItem[];
|
|
|
|
|
onChange: (items: EditableLineItem[]) => void;
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SOURCE_COLORS: Record<LineItemSource, 'primary' | 'secondary' | 'default' | 'success'> = {
|
|
|
|
|
AI: 'primary',
|
|
|
|
|
Vendor: 'secondary',
|
|
|
|
|
Manual: 'default',
|
|
|
|
|
Historical: 'success',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const UNITS = ['each', 'sq ft', 'linear ft', 'hours', 'days', 'lot', 'gallon', 'ton'];
|
|
|
|
|
|
|
|
|
|
function createEmptyItem(sortOrder: number): EditableLineItem {
|
|
|
|
|
return {
|
|
|
|
|
id: null,
|
|
|
|
|
description: '',
|
|
|
|
|
quantity: 1,
|
|
|
|
|
unit: 'each',
|
|
|
|
|
unitPrice: 0,
|
|
|
|
|
totalPrice: 0,
|
|
|
|
|
pricingMode: 'UnitPrice',
|
|
|
|
|
sortOrder,
|
|
|
|
|
source: 'Manual',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function LineItemEditor({ items, onChange, disabled = false }: LineItemEditorProps) {
|
|
|
|
|
const updateItem = (index: number, field: keyof EditableLineItem, value: unknown) => {
|
|
|
|
|
const updated = [...items];
|
|
|
|
|
const current = updated[index];
|
|
|
|
|
if (!current) return;
|
|
|
|
|
updated[index] = { ...current, [field]: value };
|
|
|
|
|
|
|
|
|
|
if (field === 'quantity' || field === 'unitPrice') {
|
|
|
|
|
const item = updated[index]!;
|
|
|
|
|
if (item.pricingMode === 'UnitPrice' || item.pricingMode === 'Both') {
|
|
|
|
|
item.totalPrice = (item.quantity || 0) * (item.unitPrice || 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onChange(updated);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const addItem = () => {
|
|
|
|
|
const maxSort = items.length > 0 ? Math.max(...items.map((i) => i.sortOrder)) : 0;
|
|
|
|
|
onChange([...items, createEmptyItem(maxSort + 1)]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const removeItem = (index: number) => {
|
|
|
|
|
const updated = items.filter((_, i) => i !== index);
|
|
|
|
|
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;
|
|
|
|
|
const updated = [...items];
|
|
|
|
|
const a = updated[index];
|
|
|
|
|
const b = updated[targetIndex];
|
|
|
|
|
if (!a || !b) return;
|
|
|
|
|
updated[index] = b;
|
|
|
|
|
updated[targetIndex] = a;
|
|
|
|
|
onChange(updated.map((item, i) => ({ ...item, sortOrder: i + 1 })));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const subtotal = items.reduce((sum, item) => sum + item.totalPrice, 0);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box>
|
|
|
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
|
|
|
|
<Typography variant="subtitle2">
|
|
|
|
|
Line Items ({items.length})
|
|
|
|
|
</Typography>
|
|
|
|
|
{!disabled && (
|
|
|
|
|
<Button size="small" startIcon={<AddIcon />} onClick={addItem}>
|
|
|
|
|
Add Row
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<TableContainer sx={{ maxHeight: 480 }}>
|
|
|
|
|
<Table size="small" stickyHeader>
|
|
|
|
|
<TableHead>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell sx={{ width: 60 }}>#</TableCell>
|
|
|
|
|
<TableCell sx={{ minWidth: 200 }}>Description</TableCell>
|
|
|
|
|
<TableCell sx={{ width: 80 }}>Qty</TableCell>
|
|
|
|
|
<TableCell sx={{ width: 100 }}>Unit</TableCell>
|
|
|
|
|
<TableCell sx={{ width: 110 }}>Unit Price</TableCell>
|
|
|
|
|
<TableCell sx={{ width: 110 }}>Total</TableCell>
|
|
|
|
|
<TableCell sx={{ width: 90 }}>Source</TableCell>
|
|
|
|
|
{!disabled && <TableCell sx={{ width: 100 }}>Actions</TableCell>}
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{items.map((item, index) => (
|
|
|
|
|
<TableRow key={item.id ?? `new-${index}`} hover>
|
|
|
|
|
<TableCell>
|
|
|
|
|
{!disabled && (
|
|
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
Phase 3 audit fixes: FIX-01–47, accessibility NITs, code quality NITs [skip deploy]
## Summary
Implements Phase 3 of the AUDIT-2026-05-20 findings:
- 29 FIX-severity items across API, web, infra, and lambdas
- 7 accessibility NITs (aria-labels, document titles)
- 4 code quality NITs (deduplication, constants extraction)
Key changes:
- API: N+1 fix, pagination clamping, idempotent transitions, upload confirm endpoint, revision TotalBidAmount carry-forward
- Web: confirmation dialogs, currency formatting, error states, date range filters, document titles
- Infra: S3 CORS lockdown, API Gateway throttling, AOSS network policy fix, CI concurrency
- Lambdas: skip empty suggestions, remove status side-effect
- Scripts: post-deploy health check
## Test plan
- [x] tsc --noEmit (web + infra)
- [x] dotnet build (api)
- [x] ruff check + format (lambdas)
- [x] Cross-review via orchestrator (no blockers)
[skip deploy]
2026-05-20 19:38:36 -04:00
|
|
|
<IconButton size="small" onClick={() => moveItem(index, -1)} disabled={index === 0} aria-label={`Move item ${index + 1} up`}>
|
Implement Phases 2-5: Frontend, AI/RAG, PDF Generation (#22)
* Fix NuGet versions and add InitialCreate EF Core migration
- Update AWSSDK.SQS and AWSSDK.SecretsManager to 3.7.500.0 (actual available versions)
- Update AWSSDK.Extensions.NETCore.Setup to 3.7.400
- Generate InitialCreate migration for PostgreSQL (all 8 entities)
- Build verified: 0 errors, 0 warnings
* Implement Dispatcher Frontend (Phase 2)
React 19 + MUI v7 + TypeScript + Vite SPA matching SHOC patterns:
Redux Toolkit (auth/ui slices), TanStack React Query, axios interceptors,
react-toastify, Cognito OAuth PKCE login flow, paginated proposal list,
new proposal form with customer autocomplete and vendor PDF upload,
read-only proposal detail with status stepper timeline.
* Add AuthController for Cognito code exchange and .env.example
Backend endpoint POST /api/auth/callback exchanges the OAuth
authorization code with Cognito's token endpoint, auto-provisions
the user in the DB, and returns the access token to the frontend.
* Implement Admin Frontend Experience (Phase 3)
Three-panel admin workspace: left reference panel (submission details,
vendor data), center editor (refined scope, inline line item table with
reorder/add/remove/pricing), right similar proposals panel (KB results
with pull-to-editor). Admin dashboard with stats cards and proposal
queue table. Approval flow with confirmation dialog, mark-as-sent,
and create-revision actions. Role-based sidebar navigation.
* Implement backend dev mode, internal API auth, and service layer enhancements
- Add dev-login endpoint with local JWT signing for local development
- Add InternalApiKeyMiddleware with timing-safe comparison for Lambda-to-API auth
- Add DevS3Service and NoOpJobPublisher for running without AWS services
- Implement CurrentUserService cascading user resolution (ID → sub → email → create)
- Add async ResolveAsync() to avoid synchronous DB calls in request pipeline
- Add /proposals/stats endpoint for efficient server-side status counts
- Guard status transitions: only allow Draft → InReview via update endpoint
- Add vendor proposals, generated PDFs, and similar proposals controllers
- Add ISimilarProposalService and SimilarProposalService
- Add [Authorize] to AddSimilarReference endpoint
* Implement Lambda functions for PDF processing, suggestions, and library ingest
- pdf-extract: Parse vendor PDFs with pdfplumber, fallback to Claude multimodal
- pdf-generate: Generate branded proposal PDFs with reportlab Platypus
- library-ingest: Format approved proposals as markdown and sync to Bedrock KB
- suggestions: Query KB for similar proposals, generate line items via Claude
- All Lambdas use internal API key auth and cold-start secret caching
- Fix pdf_path unbound variable in pdf-extract error handling
* Add Bedrock Knowledge Base, OpenSearch Serverless, and SQS message filtering
- Provision OpenSearch Serverless collection for vector search
- Create Bedrock Knowledge Base with Titan embedding model
- Configure S3 data source with fixed-size chunking (512 tokens, 20% overlap)
- Add suggestions Lambda with SQS event source filtering
- Scope bedrock:InvokeModel IAM to specific model ARN patterns
- Add internal API key secret in Secrets Manager
- Add log retention (2 months) to all Lambda functions
- Add docker-compose.yml for local PostgreSQL
* Apply SHOC design system styling across frontend
- Rewrite theme with SHOC palette (#0c4f6f primary, Nunito font, 4px radius)
- Add global CSS with Google Fonts import for Nunito
- Redesign Topbar with avatar initials, role subtitle, gradient header
- Redesign Sidebar with 220px width, section headers, active state border
- Restyle LoginPage with SHOC branded card and dev-mode role selector
- Update AdminDashboard KPI cards to centered SHOC style
- Add devLogin API method for local development auth flow
* 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
* Add appsettings.Development.json to gitignore
Prevent dev-only signing keys and connection strings from being committed.
* Fix CI failures: unused Python imports and CDK synth asset path
CDK synth job needs the .NET API published first so the Lambda asset
path exists. Python lint had 3 unused imports in pdf-generate.
* Apply ruff formatting to all Lambda Python files
2026-05-17 13:06:23 -04:00
|
|
|
<ArrowUpwardIcon sx={{ fontSize: 14 }} />
|
|
|
|
|
</IconButton>
|
|
|
|
|
<Typography variant="caption">{index + 1}</Typography>
|
Phase 3 audit fixes: FIX-01–47, accessibility NITs, code quality NITs [skip deploy]
## Summary
Implements Phase 3 of the AUDIT-2026-05-20 findings:
- 29 FIX-severity items across API, web, infra, and lambdas
- 7 accessibility NITs (aria-labels, document titles)
- 4 code quality NITs (deduplication, constants extraction)
Key changes:
- API: N+1 fix, pagination clamping, idempotent transitions, upload confirm endpoint, revision TotalBidAmount carry-forward
- Web: confirmation dialogs, currency formatting, error states, date range filters, document titles
- Infra: S3 CORS lockdown, API Gateway throttling, AOSS network policy fix, CI concurrency
- Lambdas: skip empty suggestions, remove status side-effect
- Scripts: post-deploy health check
## Test plan
- [x] tsc --noEmit (web + infra)
- [x] dotnet build (api)
- [x] ruff check + format (lambdas)
- [x] Cross-review via orchestrator (no blockers)
[skip deploy]
2026-05-20 19:38:36 -04:00
|
|
|
<IconButton size="small" onClick={() => moveItem(index, 1)} disabled={index === items.length - 1} aria-label={`Move item ${index + 1} down`}>
|
Implement Phases 2-5: Frontend, AI/RAG, PDF Generation (#22)
* Fix NuGet versions and add InitialCreate EF Core migration
- Update AWSSDK.SQS and AWSSDK.SecretsManager to 3.7.500.0 (actual available versions)
- Update AWSSDK.Extensions.NETCore.Setup to 3.7.400
- Generate InitialCreate migration for PostgreSQL (all 8 entities)
- Build verified: 0 errors, 0 warnings
* Implement Dispatcher Frontend (Phase 2)
React 19 + MUI v7 + TypeScript + Vite SPA matching SHOC patterns:
Redux Toolkit (auth/ui slices), TanStack React Query, axios interceptors,
react-toastify, Cognito OAuth PKCE login flow, paginated proposal list,
new proposal form with customer autocomplete and vendor PDF upload,
read-only proposal detail with status stepper timeline.
* Add AuthController for Cognito code exchange and .env.example
Backend endpoint POST /api/auth/callback exchanges the OAuth
authorization code with Cognito's token endpoint, auto-provisions
the user in the DB, and returns the access token to the frontend.
* Implement Admin Frontend Experience (Phase 3)
Three-panel admin workspace: left reference panel (submission details,
vendor data), center editor (refined scope, inline line item table with
reorder/add/remove/pricing), right similar proposals panel (KB results
with pull-to-editor). Admin dashboard with stats cards and proposal
queue table. Approval flow with confirmation dialog, mark-as-sent,
and create-revision actions. Role-based sidebar navigation.
* Implement backend dev mode, internal API auth, and service layer enhancements
- Add dev-login endpoint with local JWT signing for local development
- Add InternalApiKeyMiddleware with timing-safe comparison for Lambda-to-API auth
- Add DevS3Service and NoOpJobPublisher for running without AWS services
- Implement CurrentUserService cascading user resolution (ID → sub → email → create)
- Add async ResolveAsync() to avoid synchronous DB calls in request pipeline
- Add /proposals/stats endpoint for efficient server-side status counts
- Guard status transitions: only allow Draft → InReview via update endpoint
- Add vendor proposals, generated PDFs, and similar proposals controllers
- Add ISimilarProposalService and SimilarProposalService
- Add [Authorize] to AddSimilarReference endpoint
* Implement Lambda functions for PDF processing, suggestions, and library ingest
- pdf-extract: Parse vendor PDFs with pdfplumber, fallback to Claude multimodal
- pdf-generate: Generate branded proposal PDFs with reportlab Platypus
- library-ingest: Format approved proposals as markdown and sync to Bedrock KB
- suggestions: Query KB for similar proposals, generate line items via Claude
- All Lambdas use internal API key auth and cold-start secret caching
- Fix pdf_path unbound variable in pdf-extract error handling
* Add Bedrock Knowledge Base, OpenSearch Serverless, and SQS message filtering
- Provision OpenSearch Serverless collection for vector search
- Create Bedrock Knowledge Base with Titan embedding model
- Configure S3 data source with fixed-size chunking (512 tokens, 20% overlap)
- Add suggestions Lambda with SQS event source filtering
- Scope bedrock:InvokeModel IAM to specific model ARN patterns
- Add internal API key secret in Secrets Manager
- Add log retention (2 months) to all Lambda functions
- Add docker-compose.yml for local PostgreSQL
* Apply SHOC design system styling across frontend
- Rewrite theme with SHOC palette (#0c4f6f primary, Nunito font, 4px radius)
- Add global CSS with Google Fonts import for Nunito
- Redesign Topbar with avatar initials, role subtitle, gradient header
- Redesign Sidebar with 220px width, section headers, active state border
- Restyle LoginPage with SHOC branded card and dev-mode role selector
- Update AdminDashboard KPI cards to centered SHOC style
- Add devLogin API method for local development auth flow
* 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
* Add appsettings.Development.json to gitignore
Prevent dev-only signing keys and connection strings from being committed.
* Fix CI failures: unused Python imports and CDK synth asset path
CDK synth job needs the .NET API published first so the Lambda asset
path exists. Python lint had 3 unused imports in pdf-generate.
* Apply ruff formatting to all Lambda Python files
2026-05-17 13:06:23 -04:00
|
|
|
<ArrowDownwardIcon sx={{ fontSize: 14 }} />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Box>
|
|
|
|
|
)}
|
|
|
|
|
{disabled && <Typography variant="caption">{index + 1}</Typography>}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<TextField
|
|
|
|
|
size="small"
|
|
|
|
|
fullWidth
|
|
|
|
|
multiline
|
|
|
|
|
maxRows={3}
|
|
|
|
|
value={item.description}
|
|
|
|
|
onChange={(e) => updateItem(index, 'description', e.target.value)}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
variant="standard"
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<TextField
|
|
|
|
|
size="small"
|
|
|
|
|
type="number"
|
|
|
|
|
value={item.quantity}
|
|
|
|
|
onChange={(e) => updateItem(index, 'quantity', parseFloat(e.target.value) || 0)}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
variant="standard"
|
|
|
|
|
slotProps={{ htmlInput: { min: 0, step: 0.01 } }}
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<TextField
|
|
|
|
|
size="small"
|
|
|
|
|
select
|
|
|
|
|
value={item.unit}
|
|
|
|
|
onChange={(e) => updateItem(index, 'unit', e.target.value)}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
variant="standard"
|
|
|
|
|
fullWidth
|
|
|
|
|
>
|
|
|
|
|
{UNITS.map((u) => (
|
|
|
|
|
<MenuItem key={u} value={u}>{u}</MenuItem>
|
|
|
|
|
))}
|
|
|
|
|
</TextField>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<TextField
|
|
|
|
|
size="small"
|
|
|
|
|
type="number"
|
|
|
|
|
value={item.unitPrice ?? ''}
|
|
|
|
|
onChange={(e) => updateItem(index, 'unitPrice', parseFloat(e.target.value) || 0)}
|
|
|
|
|
disabled={disabled || item.pricingMode === 'TotalPrice'}
|
|
|
|
|
variant="standard"
|
|
|
|
|
slotProps={{ htmlInput: { min: 0, step: 0.01 } }}
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<TextField
|
|
|
|
|
size="small"
|
|
|
|
|
type="number"
|
|
|
|
|
value={item.totalPrice}
|
|
|
|
|
onChange={(e) => updateItem(index, 'totalPrice', parseFloat(e.target.value) || 0)}
|
|
|
|
|
disabled={disabled || item.pricingMode === 'UnitPrice'}
|
|
|
|
|
variant="standard"
|
|
|
|
|
slotProps={{ htmlInput: { min: 0, step: 0.01 } }}
|
|
|
|
|
sx={{ fontWeight: 600 }}
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Chip
|
|
|
|
|
label={item.source}
|
|
|
|
|
size="small"
|
|
|
|
|
color={SOURCE_COLORS[item.source]}
|
|
|
|
|
variant="outlined"
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
{!disabled && (
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Tooltip title="Remove">
|
|
|
|
|
<IconButton size="small" color="error" onClick={() => removeItem(index)}>
|
|
|
|
|
<DeleteIcon fontSize="small" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</TableCell>
|
|
|
|
|
)}
|
|
|
|
|
</TableRow>
|
|
|
|
|
))}
|
|
|
|
|
{items.length === 0 && (
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell colSpan={disabled ? 7 : 8} align="center" sx={{ py: 3 }}>
|
|
|
|
|
<Typography color="text.secondary">
|
|
|
|
|
No line items yet. {!disabled && 'Click "Add Row" to start.'}
|
|
|
|
|
</Typography>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
)}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</TableContainer>
|
|
|
|
|
|
|
|
|
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1, pr: 2 }}>
|
|
|
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
|
|
|
|
Total: {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(subtotal)}
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|