Frontend Specification — Paper Surplus Marketplace

Status: Blueprint only — no code scaffold exists yet. Created: 2026-02-25 Backend dependency: Container Fill Optimization API is live at /mvp/api/ (252 tests passing).


1. Angular Project Setup

Location: /home/claude/customers/marketplace/frontend/

Stack (from techContext.md + frontend.md agent):

Project structure:

frontend/
├── src/app/
│   ├── core/
│   │   ├── services/
│   │   │   ├── api.service.ts          # HttpClient wrapper, base URL config
│   │   │   └── auth.service.ts         # JWT token management
│   │   ├── interceptors/
│   │   │   └── auth.interceptor.ts     # Attach Bearer token to requests
│   │   └── guards/
│   │       └── auth.guard.ts
│   ├── shared/
│   │   ├── components/
│   │   │   ├── loading-spinner/
│   │   │   ├── empty-state/
│   │   │   └── currency-display/       # Format decimal + currency code
│   │   ├── pipes/
│   │   │   ├── weight.pipe.ts          # "12.50 MT" formatting
│   │   │   └── percentage.pipe.ts      # "92.31%" formatting
│   │   └── types/
│   │       └── common.types.ts         # Shared enums (PaperType, ContainerType, etc.)
│   ├── features/
│   │   ├── containers/                 # Container Fill Optimization
│   │   │   ├── components/
│   │   │   │   ├── fill-optimizer/     # Main page component
│   │   │   │   ├── container-gap-viz/  # Visual gap indicator
│   │   │   │   ├── suggestion-table/   # Ranked fill suggestions
│   │   │   │   ├── freight-comparison/ # LCL vs FCL side-by-side
│   │   │   │   └── savings-summary/    # Net savings banner
│   │   │   ├── services/
│   │   │   │   └── container.service.ts
│   │   │   ├── types/
│   │   │   │   └── container.types.ts
│   │   │   └── containers.routes.ts
│   │   ├── surplus/                    # Browse/select surplus items
│   │   └── dashboard/                  # Landing page
│   ├── app.component.ts
│   ├── app.routes.ts
│   └── app.config.ts
├── tailwind.config.js
├── angular.json
└── package.json

Environment config:

// environment.ts
export const environment = {
  production: false,
  apiBaseUrl: '/mvp/api'   // Relative — nginx proxies to backend
};

// environment.prod.ts
export const environment = {
  production: true,
  apiBaseUrl: '/mvp/api'
};

2. Container Fill Optimizer — Page Specification

Route: /containers/fill Purpose: Buyer selects surplus items → sees container gap → gets fill suggestions → compares LCL vs FCL freight → sees net savings

Page Layout (top to bottom)

A. Item Selection Panel

B. Container Gap Visualization

C. Fill Suggestions Table

Column Field Format Notes
Score score Badge: green ≥80, yellow ≥60, red <60 Compatibility ranking
Mill mill_name Text Source mill
Paper paper_type_display Text + GSM badge e.g. "Kraftliner 180gsm"
Width width_mm {n} mm Roll width
Grade grade A/B/C badge (colored) Quality grade
Available available_qty {n} MT Total at mill
Can Fill max_qty {n} MT Capped to gap
Price price_per_mt + currency €650.00/MT Unit price
Origin origin_country Flag emoji + code e.g. SE
Action Checkbox Select for fill

D. Freight Comparison Card (side-by-side)

Two-column layout:

Current (Partial) If Filled (FCL)
Weight {current_weight} MT {max_payload} MT
Shipping mode LCL FCL
Freight cost ${lcl_total} ${fcl_total}
Rate per MT ${lcl_rate_per_mt}/MT ${fcl_rate_per_mt}/MT
Route {route} {route}
Rate source Badge: "DB" or "Estimate" Same

E. Savings Summary Banner

Prominent card at the bottom:


3. TypeScript Interfaces

// container.types.ts

export interface FillSuggestionsRequest {
  surplus_item_ids: string[];
  buyer_id: string;
  container_type: '20ft' | '40ft' | '40ft_hc';
  include_same_mill?: boolean;
}

export interface FillSuggestionsResponse {
  container_gap: ContainerGap;
  suggestions: FillSuggestionItem[];
  freight_comparison_current: FreightComparison;
  freight_comparison_if_filled: FreightComparison;
  extra_product_cost: string;     // Decimal as string
  net_savings: string | null;
}

export interface ContainerGap {
  current_weight: string;
  max_payload: string;
  gap_mt: string;
  utilization_pct: string;
  is_full: boolean;
  needs_fill: boolean;
}

export interface FillSuggestionItem {
  surplus_item_id: string;
  mill_name: string;
  paper_type: string;
  paper_type_display: string;
  gsm: number;
  width_mm: number;
  available_qty: string;
  max_qty: string;
  price_per_mt: string;
  currency: string;
  score: string;
  grade: string;
  origin_country: string;
}

export interface FreightComparison {
  lcl_total: string | null;
  fcl_total: string | null;
  lcl_rate_per_mt: string | null;
  fcl_rate_per_mt: string | null;
  savings_amount: string | null;
  savings_pct: string | null;
  currency: string;
  route: string;
  rate_source: 'database' | 'fallback' | 'unavailable';
}

4. Service

// container.service.ts
@Injectable({ providedIn: 'root' })
export class ContainerService {
  private _http = inject(HttpClient);

  getFillSuggestions(req: FillSuggestionsRequest): Observable<FillSuggestionsResponse> {
    return this._http.post<FillSuggestionsResponse>(
      `${environment.apiBaseUrl}/container-proposals/fill-suggestions/`, req
    );
  }

  getFreightRates(filters?: Record<string, string>): Observable<PaginatedResponse<FreightRate>> {
    return this._http.get<PaginatedResponse<FreightRate>>(
      `${environment.apiBaseUrl}/freight-rates/`, { params: filters }
    );
  }
}

5. Deployment Configuration

Nginx addition (in /etc/nginx/sites-available/b2bpaper.xdvu.com):

# Marketplace frontend (Angular build output)
location /mvp/app/ {
    alias /home/claude/customers/marketplace/frontend/dist/marketplace/browser/;
    try_files $uri $uri/ /mvp/app/index.html;
}

Angular base href: <base href="/mvp/app/">

Build command: cd frontend && ng build --base-href /mvp/app/

PM2 not needed — Angular is built to static files, served by nginx.

CORS: Backend already has corsheaders middleware. Add https://b2bpaper.xdvu.com to CORS_ALLOWED_ORIGINS in Django settings.


6. Backend API Reference (Quick)

Method Endpoint Purpose
POST /mvp/api/container-proposals/fill-suggestions/ Get fill suggestions + freight comparison
GET /mvp/api/freight-rates/ List freight rates (paginated)
GET /mvp/api/freight-rates/{id}/ Freight rate detail
GET /mvp/api/surplus/ List surplus items (for picker)
GET /mvp/api/buyers/ List buyers (for admin dropdown)
GET /mvp/api/container-proposals/ List proposals
POST /mvp/api/auth/login/ JWT login (email + password)
POST /mvp/api/auth/refresh/ Refresh JWT token

7. Verification (when implemented)

  1. cd frontend && ng build — builds without errors
  2. Copy build output to dist/, configure nginx, reload
  3. Visit https://b2bpaper.xdvu.com/mvp/app/ — Angular app loads
  4. Navigate to /containers/fill — fill optimizer page renders
  5. Select a buyer, pick surplus items, choose container type
  6. Click "Get Fill Suggestions" — table populates with scored suggestions
  7. Freight comparison card shows LCL vs FCL with correct numbers
  8. Net savings banner shows green (positive) or red (negative) correctly