# Hybrid Tendering Frontend Design

## Overview

Build frontend UI for the hybrid tendering feature. The backend (Prisma schema, services, routers, types) is already fully implemented. This spec covers the UI layer: component extraction, new pages, enhanced existing views, and shared hooks.

## Requirements Summary

1. **Project-specific items management** — Create, list, edit project-specific items on project detail page
2. **BOQ bundle creation with dual item selection** — Pick from both master items and project-specific items
3. **Ad-hoc tender creation** — Create tenders directly for a project without needing a pre-existing BOQ bundle
4. **Display project-specific items in existing views** — Badge-based source identification in BOQ detail, tender detail, bid views

---

## 1. Architecture

### 1.1 New Components

#### ItemSelector (`src/components/items/item-selector.tsx`)

Unified item picker with tabbed interface. Used by BOQ creation, ad-hoc tender creation, and any future item-picking flow.

**Props:**
- `projectId: string` — required, determines which project's items to show
- `selectedMasterItemIds: Set<string>` — currently selected master item IDs
- `selectedProjectItemIds: Set<string>` — currently selected project-specific item IDs
- `onToggleMasterItem(itemId, defaultQty, defaultRate)` — callback
- `onToggleProjectItem(item, defaultQty, defaultRate)` — callback
- `onQuantityChange(itemId, value)` / `onRateChange(itemId, value)` — for master items
- `onProjectItemQuantityChange(itemId, value)` / `onProjectItemRateChange(itemId, value)` — for project-specific items

**Structure:**
- Tabs: "Master Items" | "Project-Specific Items"
- Each tab has: search input, category filter (master only), item table with checkboxes
- Bottom of "Project-Specific Items" tab: inline creation form (name, unit, description, +Add)

#### ProjectSpecificItemCreator (`src/components/items/project-specific-item-creator.tsx`)

Inline creation form within the ItemSelector's second tab.

**Props:**
- `projectId: string`
- `onCreate(item)` — callback with newly created item
- `isCreating: boolean` — loading state from mutation

**Behavior:**
- Input fields: name (required, max 255), unit (required, max 50), description (optional)
- Submit calls `projectSpecificItem.create` mutation
- On success: toast with generated code, callback to parent to add to selection, clear form

#### ItemSourceBadge (`src/components/items/item-source-badge.tsx`)

Small badge component showing item source type.

**Props:**
- `source: "MASTER_ITEM" | "PROJECT_SPECIFIC_ITEM"`
- `code?: string | null`

**Rendering:**
- MASTER_ITEM: `<Badge variant="outline">Master</Badge>` + code if present
- PROJECT_SPECIFIC_ITEM: `<Badge variant="secondary">Project-Specific</Badge>` + generated code

### 1.2 Extracted Components

#### ProjectItemsTab (`src/components/project/project-items-tab.tsx`)

Extracted from the existing "items" tab content in `projects/[projectId]/page.tsx`. The current page is 1200+ lines; extraction makes it maintainable.

**Props:**
- `project: ProjectDetail`
- All existing state and callbacks for: item management, quick add, import, weight editing, category grouping, sort, print/export

**Note:** This is a 1:1 extraction with no functional changes — pure refactor to enable adding a new tab alongside it.

### 1.3 New Hook

#### useProjectSpecificItems (`src/lib/hooks/use-project-specific-items.ts`)

```typescript
export function useProjectSpecificItems(projectId: string) {
  const { data, isLoading, refetch } = trpc.projectSpecificItem.list.useQuery({ projectId });
  const createMutation = trpc.projectSpecificItem.create.useMutation();

  return {
    items: data ?? [],
    isLoading,
    refetch,
    create: createMutation.mutateAsync,
    isCreating: createMutation.isPending,
  };
}
```

### 1.4 New Page

#### Ad-hoc Tender Creation (`src/app/(dashboard)/tenders/new/page.tsx`)

Unified tender creation supporting both PROJECT and CATEGORY types, with PROJECT supporting ad-hoc (no bundle) flow.

**Flow:**
1. Select tender type: PROJECT | CATEGORY
2. If CATEGORY: show category bundle selector (existing pattern from CategoryTenderList)
3. If PROJECT:
   a. Select project
   b. ItemSelector appears (master + project-specific items)
   c. Fill tender details (title, description, eligibility, deadline, fiscal year)
   d. On submit: creates TenderBundle + TenderNotice in one operation

---

## 2. Modified Existing Components

### 2.1 Project Detail Page (`src/app/(dashboard)/projects/[projectId]/page.tsx`)

**Changes:**
- Extract current items tab content into `ProjectItemsTab` component
- Add new "Project-Specific Items" tab using `ProjectSpecificItemsTab` (new component below)
- Tab list becomes: "Items" | "Project-Specific Items" | "Tenders"

### 2.2 ProjectSpecificItemsTab (`src/components/project/project-specific-items-tab.tsx`)

New component managing the list of project-specific items for a project.

**Features:**
- Table: Code, Name, Unit, Description, Created Date, Actions
- "Create New" button opens inline form (or sheet with ProjectSpecificItemCreator)
- Actions: Delete (only if not referenced in active tender/bid)
- Uses `useProjectSpecificItems` hook

### 2.3 BOQ Creation Page (`src/app/(dashboard)/boq/new/page.tsx`)

**Changes:**
- Replace current item selection UI (CategoryAccordion + flat table) with `ItemSelector` shared component
- Selected items from both sources unified into single items array for `boq.create` mutation
- Item source tracked via `itemId` vs `projectSpecificItemId` fields
- Estimated total calculation includes items from both sources

### 2.4 BOQ Detail Page (`src/app/(dashboard)/boq/[bundleId]/page.tsx`)

**Changes:**
- `tenderBundleItems` already includes `projectSpecificItem` relation
- For each row, check if `itemId` or `projectSpecificItemId` is set
- Render `ItemSourceBadge` in appropriate column
- Name/code display: if `item` is null, use `projectSpecificItem.name` and `projectSpecificItem.code`

### 2.5 Tender Detail Page (`src/app/(dashboard)/tenders/[tenderId]/page.tsx`)

**Changes:**
- Bundle items display uses same pattern as BOQ detail
- For ad-hoc tenders (no bundle, has projectId), show project items directly
- Item source badge on all item listings

### 2.6 Bid Views (`src/components/bid/`)

**Changes:**
- `BidLineItem` table rows check `itemId` vs `projectSpecificItemId`
- Item source badge displayed in bid comparison tables
- When `item` relation is null, fall back to `projectSpecificItem` for name/code/unit

### 2.7 Tenders List Page (`src/app/(dashboard)/tenders/page.tsx`)

**Changes:**
- Add "Create Ad-hoc Tender" button in the Project Tenders tab header
- Links to `/tenders/new`

---

## 3. Data Flow

### 3.1 Project-Specific Item Creation

```
User enters name/unit in ProjectSpecificItemCreator
  → trpc.projectSpecificItem.create mutation
    → server generates code: PROJ-{projectCode}-{seq}
    → creates ProjectSpecificItem record
  → returns { id, code, name, unit }
    → UI shows toast: "Item created: PROJ-P001-0001"
    → item appears in list and becomes selectable in ItemSelector
```

### 3.2 BOQ Bundle Creation with Dual Items

```
User selects project
  → ItemSelector loads Tab 1: project.projectItems (master items)
  → ItemSelector loads Tab 2: projectSpecificItem.list (project-specific items)
User selects items from either/both tabs
  → Form state tracks: { itemId?, projectSpecificItemId?, quantity, estimatedRate, weight }[]
  → Estimated total = sum(quantity × weight × rate) for all selected
Submit:
  → boq.create.mutate({ code, name, projectId, items: unifiedArray })
  → Service validates: no itemId + projectSpecificItemId on same item, at least one set
  → Creates TenderBundle with TenderBundleItems
```

### 3.3 Ad-hoc Tender Creation

```
User clicks "Create Ad-hoc Tender" → /tenders/new
  → Selects PROJECT type
  → Selects project
  → ItemSelector loads (same as BOQ flow)
  → Selects items
  → Fills tender form (title, description, eligibility, deadline, fiscalYear)
Submit:
  → Creates TenderBundle (code auto-generated, name = tender title, items from selection)
  → Creates TenderNotice with projectId, bundleId, tenderType = "PROJECT"
```

### 3.4 Display in Existing Views

```
BOQ/Tender/Bid detail page renders items
  → For each item row:
    → if itemId is set: show item.name, item.code, item.category.name, <ItemSourceBadge source="MASTER_ITEM" />
    → if projectSpecificItemId is set: show projectSpecificItem.name, projectSpecificItem.code, <ItemSourceBadge source="PROJECT_SPECIFIC_ITEM" />
  → Estimated rate: from ItemRate (master) or user-set value (project-specific)
```

---

## 4. Error Handling

### 4.1 Code Generation Race Condition
- Prisma unique constraint on `ProjectSpecificItem.code` prevents duplicates
- If mutation fails with unique constraint violation, UI shows error toast and auto-retries by refetching and re-submitting

### 4.2 Item Deletion Protection
- Project-specific items referenced in TenderBundleItem or BidLineItem cannot be deleted
- The DB relations have no `onDelete: Cascade`, so DB rejects the deletion
- UI: check for references before delete attempt, show message: "Cannot delete — used in X tender(s)"

### 4.3 Category Tender Conflict
- Service already validates: items assigned to category tenders cannot be added to project BOQ bundles
- UI surfaces this as toast error listing conflicting item names

### 4.4 Form Validation
- Ad-hoc tender form: submit disabled until project selected + at least one item selected + title + deadline
- Project-specific item creator: submit disabled until name + unit filled
- BOQ creation: submit disabled until code + name + project + at least one item

### 4.5 Missing Project Code
- If project has no code, code generation algorithm uses fallback: `PROJ-UNKNOWN-{seq}`
- This edge case should not occur in practice (project code is required and unique)

---

## 5. Loading States

- ItemSelector: each tab has independent loading. Master items use skeleton rows (existing pattern). Project-specific items use simple spinner (smaller dataset).
- Project-specific item creator: button shows "Creating..." while mutation is pending
- All list pages use existing LoadingState/EmptyState/ErrorState shared components

---

## 6. Backwards Compatibility

- Existing BOQ creation with only master items must continue to work unchanged
- `ItemSelector` component passes master items through the same `itemId` field as before
- BOQ detail page: rows with only `itemId` set render exactly as before (badge is optional)
- No changes to existing tRPC router inputs — only additions (new optional fields)
- Existing project detail page items tab functionality preserved via extraction

---

## 7. File Changes Summary

### New Files
```
src/components/items/item-selector.tsx
src/components/items/project-specific-item-creator.tsx
src/components/items/item-source-badge.tsx
src/components/project/project-items-tab.tsx
src/components/project/project-specific-items-tab.tsx
src/lib/hooks/use-project-specific-items.ts
src/app/(dashboard)/tenders/new/page.tsx
```

### Modified Files
```
src/app/(dashboard)/projects/[projectId]/page.tsx  — extract items tab, add new tab
src/app/(dashboard)/boq/new/page.tsx               — replace with ItemSelector
src/app/(dashboard)/boq/[bundleId]/page.tsx        — add item source display
src/app/(dashboard)/tenders/page.tsx               — add ad-hoc tender button
src/app/(dashboard)/tenders/[tenderId]/page.tsx    — add item source display
src/components/bid/BidComparisonTable.tsx          — add item source display
```
