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):
- Angular 19 (latest stable), standalone components
- Tailwind CSS (primary styling)
- Angular Material (form fields, dialogs, tables)
- ApexCharts via
ngx-apexcharts(charts) - Reactive Forms only, OnPush change detection, ViewEncapsulation.None
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
- Dropdown to select buyer (admin view) or pre-filled (buyer portal)
- Container type selector: 20ft / 40ft / 40ft HC (radio buttons or segmented control)
- Surplus item picker: searchable multi-select from available surplus
- "Get Fill Suggestions" button (calls API)
B. Container Gap Visualization
- Horizontal progress bar showing utilization percentage
- Labels:
{current_weight} MT / {max_payload} MT ({utilization_pct}%) - Color: green if >85%, yellow 50-85%, red <50%
- Gap callout:
{gap_mt} MT available - Badge: "Full" (green) or "Needs Fill" (amber)
C. Fill Suggestions Table
- Sortable table of suggestions, default sorted by score (desc)
- Columns:
| 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 |
- Empty state: "No compatible surplus found. Try adjusting buyer specs or including same-mill items."
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:
Extra product cost: ${extra_product_cost}— cost of fill itemsFreight savings: ${freight_current.lcl_total - freight_filled.fcl_total}Net savings: ${net_savings}— large, bold, green if positive, red if negative- Recommendation text:
- Positive: "Filling this container saves ${net_savings} compared to shipping partial."
- Negative: "Filling this container costs ${abs(net_savings)} more than shipping partial. Partial LCL may be more economical."
- Null: "Freight rates unavailable for this route."
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)
cd frontend && ng build— builds without errors- Copy build output to
dist/, configure nginx, reload - Visit
https://b2bpaper.xdvu.com/mvp/app/— Angular app loads - Navigate to
/containers/fill— fill optimizer page renders - Select a buyer, pick surplus items, choose container type
- Click "Get Fill Suggestions" — table populates with scored suggestions
- Freight comparison card shows LCL vs FCL with correct numbers
- Net savings banner shows green (positive) or red (negative) correctly