Implement Backend API Core (Phase 1)
Complete API layer with Clean Architecture:
- Application DTOs (proposals, line items, customers, users, files, audit, dashboard)
- Service interfaces (IProposalService, ILineItemService, ICustomerService, IAuditService, IS3Service, IJobPublisher)
- FluentValidation validators for all create requests
- Infrastructure service implementations (ProposalService, LineItemService, CustomerService, AuditService, S3Service, SqsJobPublisher, ProposalNumberGenerator)
- Secrets Manager connection string resolver for RDS
- API controllers: Proposals (CRUD + approve/send/revise), LineItems (CRUD + bulk), Customers, Users, Files (presigned upload/download), Admin (dashboard)
- Middleware: GlobalExceptionHandler (ProblemDetails), ValidationFilter (FluentValidation pipeline)
- CurrentUserService (Cognito JWT claims -> User entity, auto-provisioning)
- Full DI configuration in Program.cs with Lambda hosting
- Role-based authorization (dispatchers, admins, sysadmins)
- CORS configured for proposals.seahaven.com + localhost
2026-05-16 18:49:48 -04:00
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using ProposalSystem.Application.DTOs;
|
|
|
|
|
using ProposalSystem.Application.Interfaces;
|
|
|
|
|
using ProposalSystem.Domain.Entities;
|
|
|
|
|
using ProposalSystem.Infrastructure.Data;
|
|
|
|
|
|
|
|
|
|
namespace ProposalSystem.Infrastructure.Services;
|
|
|
|
|
|
|
|
|
|
public class ProposalService : IProposalService
|
|
|
|
|
{
|
|
|
|
|
private readonly ProposalDbContext _db;
|
|
|
|
|
private readonly ICurrentUserService _currentUser;
|
|
|
|
|
private readonly IProposalNumberGenerator _numberGenerator;
|
|
|
|
|
private readonly IAuditService _audit;
|
|
|
|
|
private readonly IJobPublisher _jobPublisher;
|
|
|
|
|
|
|
|
|
|
public ProposalService(
|
|
|
|
|
ProposalDbContext db,
|
|
|
|
|
ICurrentUserService currentUser,
|
|
|
|
|
IProposalNumberGenerator numberGenerator,
|
|
|
|
|
IAuditService audit,
|
|
|
|
|
IJobPublisher jobPublisher)
|
|
|
|
|
{
|
|
|
|
|
_db = db;
|
|
|
|
|
_currentUser = currentUser;
|
|
|
|
|
_numberGenerator = numberGenerator;
|
|
|
|
|
_audit = audit;
|
|
|
|
|
_jobPublisher = jobPublisher;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<ProposalResponse> CreateAsync(CreateProposalRequest request, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
var proposalNumber = await _numberGenerator.GenerateAsync(ct);
|
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
|
|
|
|
|
|
var proposal = new Proposal
|
|
|
|
|
{
|
|
|
|
|
Id = Guid.NewGuid(),
|
|
|
|
|
ProposalNumber = proposalNumber,
|
|
|
|
|
WorkOrderNumber = request.WorkOrderNumber,
|
|
|
|
|
CustomerName = request.CustomerName,
|
|
|
|
|
CustomerAddress = request.CustomerAddress,
|
|
|
|
|
ScopeOfWork = request.ScopeOfWork,
|
|
|
|
|
ServiceCategory = request.ServiceCategory,
|
|
|
|
|
Priority = request.Priority,
|
|
|
|
|
Status = ProposalStatus.Draft,
|
|
|
|
|
Notes = request.Notes ?? string.Empty,
|
|
|
|
|
SubmittedById = _currentUser.UserId,
|
|
|
|
|
SubmittedAt = now,
|
|
|
|
|
CreatedAt = now,
|
|
|
|
|
UpdatedAt = now,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_db.Proposals.Add(proposal);
|
|
|
|
|
await _db.SaveChangesAsync(ct);
|
|
|
|
|
|
|
|
|
|
await _audit.LogAsync(AuditAction.Submit, proposal.Id, null, ct);
|
|
|
|
|
|
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
|
|
|
await _jobPublisher.PublishAsync("suggestions", new { proposalId = proposal.Id, trigger = "generate" }, ct);
|
Implement Backend API Core (Phase 1)
Complete API layer with Clean Architecture:
- Application DTOs (proposals, line items, customers, users, files, audit, dashboard)
- Service interfaces (IProposalService, ILineItemService, ICustomerService, IAuditService, IS3Service, IJobPublisher)
- FluentValidation validators for all create requests
- Infrastructure service implementations (ProposalService, LineItemService, CustomerService, AuditService, S3Service, SqsJobPublisher, ProposalNumberGenerator)
- Secrets Manager connection string resolver for RDS
- API controllers: Proposals (CRUD + approve/send/revise), LineItems (CRUD + bulk), Customers, Users, Files (presigned upload/download), Admin (dashboard)
- Middleware: GlobalExceptionHandler (ProblemDetails), ValidationFilter (FluentValidation pipeline)
- CurrentUserService (Cognito JWT claims -> User entity, auto-provisioning)
- Full DI configuration in Program.cs with Lambda hosting
- Role-based authorization (dispatchers, admins, sysadmins)
- CORS configured for proposals.seahaven.com + localhost
2026-05-16 18:49:48 -04:00
|
|
|
|
|
|
|
|
return MapToResponse(proposal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<ProposalResponse?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
var proposal = await _db.Proposals
|
|
|
|
|
.Include(p => p.SubmittedBy)
|
|
|
|
|
.Include(p => p.AssignedAdmin)
|
|
|
|
|
.Include(p => p.ApprovedBy)
|
|
|
|
|
.FirstOrDefaultAsync(p => p.Id == id, ct);
|
|
|
|
|
|
|
|
|
|
return proposal == null ? null : MapToResponse(proposal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<PagedResponse<ProposalListResponse>> GetAllAsync(ProposalFilterRequest filter, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
var query = _db.Proposals
|
|
|
|
|
.Include(p => p.SubmittedBy)
|
|
|
|
|
.Include(p => p.AssignedAdmin)
|
|
|
|
|
.AsQueryable();
|
|
|
|
|
|
|
|
|
|
if (_currentUser.Role == UserRole.Dispatcher)
|
|
|
|
|
{
|
|
|
|
|
query = query.Where(p => p.SubmittedById == _currentUser.UserId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filter.Status.HasValue)
|
|
|
|
|
query = query.Where(p => p.Status == filter.Status.Value);
|
|
|
|
|
|
|
|
|
|
if (filter.ServiceCategory.HasValue)
|
|
|
|
|
query = query.Where(p => p.ServiceCategory == filter.ServiceCategory.Value);
|
|
|
|
|
|
|
|
|
|
if (filter.Priority.HasValue)
|
|
|
|
|
query = query.Where(p => p.Priority == filter.Priority.Value);
|
|
|
|
|
|
|
|
|
|
if (filter.FromDate.HasValue)
|
|
|
|
|
query = query.Where(p => p.SubmittedAt >= filter.FromDate.Value);
|
|
|
|
|
|
|
|
|
|
if (filter.ToDate.HasValue)
|
|
|
|
|
query = query.Where(p => p.SubmittedAt <= filter.ToDate.Value);
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(filter.Search))
|
|
|
|
|
{
|
|
|
|
|
var search = filter.Search.ToLower();
|
|
|
|
|
query = query.Where(p =>
|
|
|
|
|
p.CustomerName.ToLower().Contains(search) ||
|
|
|
|
|
p.ProposalNumber.ToLower().Contains(search) ||
|
|
|
|
|
p.WorkOrderNumber.ToLower().Contains(search));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var totalCount = await query.CountAsync(ct);
|
|
|
|
|
|
|
|
|
|
var items = await query
|
|
|
|
|
.OrderByDescending(p => p.Priority)
|
|
|
|
|
.ThenByDescending(p => p.SubmittedAt)
|
|
|
|
|
.Skip((filter.Page - 1) * filter.PageSize)
|
|
|
|
|
.Take(filter.PageSize)
|
|
|
|
|
.Select(p => new ProposalListResponse(
|
|
|
|
|
p.Id,
|
|
|
|
|
p.ProposalNumber,
|
|
|
|
|
p.CustomerName,
|
|
|
|
|
p.WorkOrderNumber,
|
|
|
|
|
p.ServiceCategory,
|
|
|
|
|
p.Priority,
|
|
|
|
|
p.Status,
|
|
|
|
|
p.TotalBidAmount,
|
|
|
|
|
p.SubmittedAt,
|
|
|
|
|
p.SubmittedBy != null ? p.SubmittedBy.DisplayName : null,
|
|
|
|
|
p.AssignedAdmin != null ? p.AssignedAdmin.DisplayName : null
|
|
|
|
|
))
|
|
|
|
|
.ToListAsync(ct);
|
|
|
|
|
|
|
|
|
|
return new PagedResponse<ProposalListResponse>(items, totalCount, filter.Page, filter.PageSize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<ProposalResponse> UpdateAsync(Guid id, UpdateProposalRequest request, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
var proposal = await _db.Proposals.FindAsync(new object[] { id }, ct)
|
|
|
|
|
?? throw new KeyNotFoundException($"Proposal {id} not found");
|
|
|
|
|
|
|
|
|
|
if (request.RefinedScope != null)
|
|
|
|
|
proposal.RefinedScope = request.RefinedScope;
|
|
|
|
|
|
|
|
|
|
if (request.Notes != null)
|
|
|
|
|
proposal.Notes = request.Notes;
|
|
|
|
|
|
|
|
|
|
if (request.AssignedAdminId.HasValue)
|
|
|
|
|
proposal.AssignedAdminId = request.AssignedAdminId.Value;
|
|
|
|
|
|
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
|
|
|
if (request.Status.HasValue && proposal.Status == ProposalStatus.Draft
|
|
|
|
|
&& request.Status.Value == ProposalStatus.InReview)
|
|
|
|
|
proposal.Status = request.Status.Value;
|
|
|
|
|
|
Implement Backend API Core (Phase 1)
Complete API layer with Clean Architecture:
- Application DTOs (proposals, line items, customers, users, files, audit, dashboard)
- Service interfaces (IProposalService, ILineItemService, ICustomerService, IAuditService, IS3Service, IJobPublisher)
- FluentValidation validators for all create requests
- Infrastructure service implementations (ProposalService, LineItemService, CustomerService, AuditService, S3Service, SqsJobPublisher, ProposalNumberGenerator)
- Secrets Manager connection string resolver for RDS
- API controllers: Proposals (CRUD + approve/send/revise), LineItems (CRUD + bulk), Customers, Users, Files (presigned upload/download), Admin (dashboard)
- Middleware: GlobalExceptionHandler (ProblemDetails), ValidationFilter (FluentValidation pipeline)
- CurrentUserService (Cognito JWT claims -> User entity, auto-provisioning)
- Full DI configuration in Program.cs with Lambda hosting
- Role-based authorization (dispatchers, admins, sysadmins)
- CORS configured for proposals.seahaven.com + localhost
2026-05-16 18:49:48 -04:00
|
|
|
proposal.UpdatedAt = DateTime.UtcNow;
|
|
|
|
|
await _db.SaveChangesAsync(ct);
|
|
|
|
|
|
|
|
|
|
await _audit.LogAsync(AuditAction.Edit, id, null, ct);
|
|
|
|
|
|
|
|
|
|
return MapToResponse(proposal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<ProposalResponse> ApproveAsync(Guid id, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
var proposal = await _db.Proposals
|
|
|
|
|
.Include(p => p.LineItems)
|
|
|
|
|
.FirstOrDefaultAsync(p => p.Id == id, ct)
|
|
|
|
|
?? throw new KeyNotFoundException($"Proposal {id} not found");
|
|
|
|
|
|
|
|
|
|
if (proposal.Status != ProposalStatus.InReview)
|
|
|
|
|
throw new InvalidOperationException("Only proposals in review can be approved");
|
|
|
|
|
|
|
|
|
|
if (!proposal.LineItems.Any() || proposal.LineItems.All(li => li.TotalPrice <= 0))
|
|
|
|
|
throw new InvalidOperationException("Cannot approve proposal without priced line items");
|
|
|
|
|
|
|
|
|
|
proposal.Status = ProposalStatus.Approved;
|
|
|
|
|
proposal.ApprovedById = _currentUser.UserId;
|
|
|
|
|
proposal.ApprovedAt = DateTime.UtcNow;
|
|
|
|
|
proposal.TotalBidAmount = proposal.LineItems.Sum(li => li.TotalPrice);
|
|
|
|
|
proposal.UpdatedAt = DateTime.UtcNow;
|
|
|
|
|
|
|
|
|
|
await _db.SaveChangesAsync(ct);
|
|
|
|
|
await _audit.LogAsync(AuditAction.Approve, id, null, ct);
|
|
|
|
|
|
|
|
|
|
return MapToResponse(proposal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<ProposalResponse> MarkSentAsync(Guid id, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
var proposal = await _db.Proposals.FindAsync(new object[] { id }, ct)
|
|
|
|
|
?? throw new KeyNotFoundException($"Proposal {id} not found");
|
|
|
|
|
|
|
|
|
|
if (proposal.Status != ProposalStatus.Approved)
|
|
|
|
|
throw new InvalidOperationException("Only approved proposals can be marked as sent");
|
|
|
|
|
|
|
|
|
|
proposal.Status = ProposalStatus.Sent;
|
|
|
|
|
proposal.SentAt = DateTime.UtcNow;
|
|
|
|
|
proposal.UpdatedAt = DateTime.UtcNow;
|
|
|
|
|
|
|
|
|
|
await _db.SaveChangesAsync(ct);
|
|
|
|
|
await _audit.LogAsync(AuditAction.MarkSent, id, null, ct);
|
|
|
|
|
|
|
|
|
|
await _jobPublisher.PublishAsync("library-ingest", new { proposalId = id }, ct);
|
|
|
|
|
|
|
|
|
|
return MapToResponse(proposal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<ProposalResponse> ReviseAsync(Guid id, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
var proposal = await _db.Proposals
|
|
|
|
|
.Include(p => p.LineItems)
|
|
|
|
|
.FirstOrDefaultAsync(p => p.Id == id, ct)
|
|
|
|
|
?? throw new KeyNotFoundException($"Proposal {id} not found");
|
|
|
|
|
|
|
|
|
|
if (proposal.Status != ProposalStatus.Sent)
|
|
|
|
|
throw new InvalidOperationException("Only sent proposals can be revised");
|
|
|
|
|
|
|
|
|
|
var revision = new Proposal
|
|
|
|
|
{
|
|
|
|
|
Id = Guid.NewGuid(),
|
|
|
|
|
ProposalNumber = proposal.ProposalNumber,
|
|
|
|
|
WorkOrderNumber = proposal.WorkOrderNumber,
|
|
|
|
|
CustomerName = proposal.CustomerName,
|
|
|
|
|
CustomerAddress = proposal.CustomerAddress,
|
|
|
|
|
ScopeOfWork = proposal.ScopeOfWork,
|
|
|
|
|
RefinedScope = proposal.RefinedScope,
|
|
|
|
|
ServiceCategory = proposal.ServiceCategory,
|
|
|
|
|
Priority = proposal.Priority,
|
|
|
|
|
Status = ProposalStatus.InReview,
|
|
|
|
|
Notes = proposal.Notes,
|
|
|
|
|
SubmittedById = proposal.SubmittedById,
|
|
|
|
|
SubmittedAt = proposal.SubmittedAt,
|
|
|
|
|
AssignedAdminId = _currentUser.UserId,
|
|
|
|
|
CurrentRevision = proposal.CurrentRevision + 1,
|
|
|
|
|
ParentProposalId = proposal.Id,
|
|
|
|
|
CreatedAt = DateTime.UtcNow,
|
|
|
|
|
UpdatedAt = DateTime.UtcNow,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
foreach (var li in proposal.LineItems)
|
|
|
|
|
{
|
|
|
|
|
revision.LineItems.Add(new LineItem
|
|
|
|
|
{
|
|
|
|
|
Id = Guid.NewGuid(),
|
|
|
|
|
ProposalId = revision.Id,
|
|
|
|
|
Description = li.Description,
|
|
|
|
|
Quantity = li.Quantity,
|
|
|
|
|
Unit = li.Unit,
|
|
|
|
|
UnitPrice = li.UnitPrice,
|
|
|
|
|
TotalPrice = li.TotalPrice,
|
|
|
|
|
PricingMode = li.PricingMode,
|
|
|
|
|
SortOrder = li.SortOrder,
|
|
|
|
|
Source = li.Source,
|
|
|
|
|
CreatedAt = DateTime.UtcNow,
|
|
|
|
|
UpdatedAt = DateTime.UtcNow,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
proposal.Status = ProposalStatus.Revised;
|
|
|
|
|
proposal.UpdatedAt = DateTime.UtcNow;
|
|
|
|
|
|
|
|
|
|
_db.Proposals.Add(revision);
|
|
|
|
|
await _db.SaveChangesAsync(ct);
|
|
|
|
|
|
|
|
|
|
await _audit.LogAsync(AuditAction.CreateRevision, revision.Id, $"Revised from {proposal.Id}", ct);
|
|
|
|
|
|
|
|
|
|
return MapToResponse(revision);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<IReadOnlyList<ProposalResponse>> GetRevisionHistoryAsync(Guid id, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
var proposal = await _db.Proposals.FindAsync(new object[] { id }, ct)
|
|
|
|
|
?? throw new KeyNotFoundException($"Proposal {id} not found");
|
|
|
|
|
|
|
|
|
|
var rootId = proposal.ParentProposalId ?? proposal.Id;
|
|
|
|
|
|
|
|
|
|
var revisions = await _db.Proposals
|
|
|
|
|
.Where(p => p.Id == rootId || p.ParentProposalId == rootId)
|
|
|
|
|
.OrderBy(p => p.CurrentRevision)
|
|
|
|
|
.ToListAsync(ct);
|
|
|
|
|
|
|
|
|
|
return revisions.Select(MapToResponse).ToList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<IReadOnlyList<AuditLogResponse>> GetAuditTrailAsync(Guid id, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
return await _db.AuditLogs
|
|
|
|
|
.Include(a => a.User)
|
|
|
|
|
.Where(a => a.ProposalId == id)
|
|
|
|
|
.OrderByDescending(a => a.Timestamp)
|
|
|
|
|
.Select(a => new AuditLogResponse(
|
|
|
|
|
a.Id,
|
|
|
|
|
a.ProposalId,
|
|
|
|
|
a.UserId,
|
|
|
|
|
a.User != null ? a.User.DisplayName : null,
|
|
|
|
|
a.Action,
|
|
|
|
|
a.Details,
|
|
|
|
|
a.Timestamp,
|
|
|
|
|
a.IpAddress
|
|
|
|
|
))
|
|
|
|
|
.ToListAsync(ct);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
public async Task<ProposalStatsResponse> GetStatsAsync(CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
var userId = _currentUser.UserId;
|
|
|
|
|
|
2026-05-17 13:48:14 -04:00
|
|
|
var counts = await _db.Proposals
|
|
|
|
|
.Where(p => p.SubmittedById == userId)
|
|
|
|
|
.GroupBy(_ => 1)
|
|
|
|
|
.Select(g => new
|
|
|
|
|
{
|
|
|
|
|
Total = g.Count(),
|
|
|
|
|
InReview = g.Count(p => p.Status == ProposalStatus.InReview),
|
|
|
|
|
Approved = g.Count(p => p.Status == ProposalStatus.Approved),
|
|
|
|
|
Sent = g.Count(p => p.Status == ProposalStatus.Sent),
|
|
|
|
|
})
|
|
|
|
|
.FirstOrDefaultAsync(ct);
|
|
|
|
|
|
|
|
|
|
return counts == null
|
|
|
|
|
? new ProposalStatsResponse(0, 0, 0, 0)
|
|
|
|
|
: new ProposalStatsResponse(counts.Total, counts.InReview, counts.Approved, counts.Sent);
|
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
|
|
|
}
|
|
|
|
|
|
Implement Backend API Core (Phase 1)
Complete API layer with Clean Architecture:
- Application DTOs (proposals, line items, customers, users, files, audit, dashboard)
- Service interfaces (IProposalService, ILineItemService, ICustomerService, IAuditService, IS3Service, IJobPublisher)
- FluentValidation validators for all create requests
- Infrastructure service implementations (ProposalService, LineItemService, CustomerService, AuditService, S3Service, SqsJobPublisher, ProposalNumberGenerator)
- Secrets Manager connection string resolver for RDS
- API controllers: Proposals (CRUD + approve/send/revise), LineItems (CRUD + bulk), Customers, Users, Files (presigned upload/download), Admin (dashboard)
- Middleware: GlobalExceptionHandler (ProblemDetails), ValidationFilter (FluentValidation pipeline)
- CurrentUserService (Cognito JWT claims -> User entity, auto-provisioning)
- Full DI configuration in Program.cs with Lambda hosting
- Role-based authorization (dispatchers, admins, sysadmins)
- CORS configured for proposals.seahaven.com + localhost
2026-05-16 18:49:48 -04:00
|
|
|
private static ProposalResponse MapToResponse(Proposal p) => new(
|
|
|
|
|
p.Id,
|
|
|
|
|
p.ProposalNumber,
|
|
|
|
|
p.WorkOrderNumber,
|
|
|
|
|
p.CustomerName,
|
|
|
|
|
p.CustomerAddress,
|
|
|
|
|
p.ScopeOfWork,
|
|
|
|
|
p.RefinedScope,
|
|
|
|
|
p.ServiceCategory,
|
|
|
|
|
p.Priority,
|
|
|
|
|
p.Status,
|
|
|
|
|
p.TotalBidAmount,
|
|
|
|
|
p.VendorTotalCost,
|
|
|
|
|
p.Notes,
|
|
|
|
|
p.SubmittedById,
|
|
|
|
|
p.SubmittedBy?.DisplayName,
|
|
|
|
|
p.SubmittedAt,
|
|
|
|
|
p.AssignedAdminId,
|
|
|
|
|
p.ApprovedById,
|
|
|
|
|
p.ApprovedAt,
|
|
|
|
|
p.SentAt,
|
|
|
|
|
p.CurrentRevision,
|
|
|
|
|
p.ParentProposalId,
|
|
|
|
|
p.CreatedAt,
|
|
|
|
|
p.UpdatedAt
|
|
|
|
|
);
|
|
|
|
|
}
|