# Vendor Bidding Flow & Super Admin Oversight Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Implement self-service vendor bidding with approval workflow and super admin oversight dashboard.

**Architecture:**

- Bid submission uses approval gate logic based on tender type + vendor verification + tender config
- Super admin users with VENDOR role can toggle between admin and vendor views via context switch
- Admin oversight via dedicated `/dashboard/vendor-oversight` page and per-tender bid management

**Tech Stack:** Next.js 14, tRPC, Prisma, PostgreSQL, Tailwind CSS

---

## File Structure

```
Modified Files:
- prisma/schema.prisma                              # Add bid fields, TenderNotice.autoApproveBids
- src/server/routers/bid.router.ts                  # Add admin endpoints, approval logic
- src/server/routers/auth.router.ts                  # Add getSession for dual-role check
- src/components/layouts/header.tsx                  # Add context switch toggle
- src/lib/permissions.ts                            # Add bid permissions
- src/app/vendor-portal/layout.tsx                  # Allow super admin with vendor role

New Files:
- src/app/(dashboard)/vendor-oversight/page.tsx      # Main oversight dashboard
- src/app/(dashboard)/tender/[id]/bids/page.tsx      # Per-tender bid management
- src/components/bids/BidApprovalCard.tsx            # Pending approval card
- src/components/bids/BidStatusBadge.tsx              # Status indicator
- src/components/bids/BidComparisonTable.tsx         # Side-by-side comparison
- src/components/bids/VendorBidFilters.tsx           # Admin filter bar
- src/lib/bid-helpers.ts                            # Approval path determination
```

---

## Task 1: Database Schema Updates

**Files:**

- Modify: `prisma/schema.prisma`
- Run: `npx prisma db push`

- [ ] **Step 1: Add new BidStatus enum values**

```prisma
enum BidStatus {
    DRAFT
    PENDING_APPROVAL
    SUBMITTED
    WITHDRAWN
    APPROVED
    REJECTED
}
```

- [ ] **Step 2: Add approval fields to BidSubmission model**

```prisma
model BidSubmission {
    id              String    @id @default(cuid())
    tenderId        String
    vendorId        String
    referenceNo     String    @unique
    status          BidStatus @default(DRAFT)
    totalAmount     Decimal?  @db.Decimal(15, 2)
    submittedAt     DateTime?
    approvedAt      DateTime?
    approvedById    String?
    rejectedAt      DateTime?
    rejectedById    String?
    rejectionReason String?
    createdAt       DateTime  @default(now())
    updatedAt       DateTime  @updatedAt

    tender        TenderNotice  @relation(fields: [tenderId], references: [id])
    vendor        Vendor        @relation(fields: [vendorId], references: [id])
    approvedBy    User?         @relation("BidApprovals", fields: [approvedById], references: [id])
    rejectedBy    User?         @relation("BidRejections", fields: [rejectedById], references: [id])
    bidLineItems  BidLineItem[]
    versions      BidVersion[]

    @@unique([tenderId, vendorId])
}

model User {
    // ... existing fields ...
    approvedBids  BidSubmission[] @relation("BidApprovals")
    rejectedBids  BidSubmission[] @relation("BidRejections")
}
```

- [ ] **Step 3: Add autoApproveBids to TenderNotice**

```prisma
model TenderNotice {
    // ... existing fields ...
    autoApproveBids Boolean @default(false)
}
```

- [ ] **Step 4: Push schema to database**

Run: `npx prisma db push`
Expected: "The changes have been applied successfully"

- [ ] **Step 5: Generate Prisma client**

Run: `npx prisma generate`
Expected: "Generated Prisma client"

- [ ] **Step 6: Commit**

```bash
git add prisma/schema.prisma
git commit -m "feat: add bid approval fields and autoApproveBids to TenderNotice"
```

---

## Task 2: Bid Approval Helper Logic

**Files:**

- Create: `src/lib/bid-helpers.ts`
- Modify: `src/server/routers/bid.router.ts`

- [ ] **Step 1: Create bid-helpers.ts with approval logic**

```typescript
import type { PrismaClient } from "@prisma/generated/client";
import type {
  VendorVerificationStatus,
  TenderType,
} from "@prisma/generated/client";

export type BidApprovalPath = "DIRECT" | "PENDING_APPROVAL";

export function determineApprovalPath(
  tenderType: TenderType,
  vendorVerification: VendorVerificationStatus,
  tenderAutoApprove: boolean,
): BidApprovalPath {
  if (
    tenderType === "CATEGORY" &&
    vendorVerification === "VERIFIED" &&
    tenderAutoApprove
  ) {
    return "DIRECT";
  }

  if (vendorVerification === "VERIFIED") {
    return "DIRECT";
  }

  return "PENDING_APPROVAL";
}

export async function getVendorIdByEmail(
  db: PrismaClient,
  email: string,
): Promise<string> {
  const vendor = await db.vendor.findFirst({ where: { email } });
  if (!vendor) {
    throw new Error("Vendor not found");
  }
  return vendor.id;
}
```

- [ ] **Step 2: Commit**

```bash
git add src/lib/bid-helpers.ts
git commit -m "feat: add bid approval path determination logic"
```

---

## Task 3: Update Bid Submission Router

**Files:**

- Modify: `src/server/routers/bid.router.ts:1-304`

- [ ] **Step 1: Update imports**

```typescript
import { z } from "zod";
import { TRPCError } from "@trpc/server";
import {
  createTRPCRouter,
  protectedProcedure,
  adminProcedure,
  superAdminProcedure,
} from "@/server/trpc";
import type { PrismaClient } from "@prisma/generated/client";
import { Prisma } from "@prisma/generated/client";
import { AuditService } from "@/server/services/audit.service";
import { determineApprovalPath, getVendorIdByEmail } from "@/lib/bid-helpers";
```

- [ ] **Step 2: Update submit mutation to use approval logic**

Replace the existing `submit` mutation body (lines 38-122) with:

```typescript
.mutation(async ({ ctx, input }) => {
  let vendorId: string;
  let vendor;

  if (input.vendorId) {
    vendorId = input.vendorId;
    vendor = await ctx.db.vendor.findUnique({ where: { id: vendorId } });
  } else {
    if (ctx.session.user.role !== "VENDOR" && ctx.session.user.role !== "SUPER_ADMIN") {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: "vendorId is required for non-vendor users",
      });
    }
    vendorId = await getVendorIdByEmail(ctx.db, ctx.session.user.email);
    vendor = await ctx.db.vendor.findFirst({ where: { email: ctx.session.user.email } });
  }

  const tender = await ctx.db.tenderNotice.findUnique({
    where: { id: input.tenderId },
  });

  if (!tender) {
    throw new TRPCError({ code: "NOT_FOUND", message: "Tender not found" });
  }

  if (new Date() > tender.submissionDeadline) {
    throw new TRPCError({
      code: "FORBIDDEN",
      message: "Submission deadline has passed",
    });
  }

  const approvalPath = determineApprovalPath(
    tender.tenderType,
    vendor!.verificationStatus,
    tender.autoApproveBids ?? false
  );

  const status = approvalPath === "DIRECT" ? "SUBMITTED" : "PENDING_APPROVAL";

  const existing = await ctx.db.bidSubmission.findUnique({
    where: {
      tenderId_vendorId: {
        tenderId: input.tenderId,
        vendorId: vendorId!,
      },
    },
  });

  const count = await ctx.db.bidSubmission.count({
    where: { tenderId: input.tenderId },
  });
  const referenceNo = `BID/${count + 1}`;

  const lineItems = input.lineItems.map((item) => ({
    ...item,
    totalAmount: item.quotedRate * item.quantity,
  }));
  const totalAmount = lineItems.reduce((sum, i) => sum + i.totalAmount, 0);

  let result;
  if (existing) {
    await ctx.db.bidLineItem.deleteMany({
      where: { submissionId: existing.id },
    });
    result = await ctx.db.bidSubmission.update({
      where: { id: existing.id },
      data: {
        status,
        submittedAt: new Date(),
        totalAmount,
        bidLineItems: { create: lineItems },
      },
    });
  } else {
    result = await ctx.db.bidSubmission.create({
      data: {
        tenderId: input.tenderId,
        vendorId: vendorId!,
        referenceNo,
        status,
        submittedAt: new Date(),
        totalAmount,
        bidLineItems: { create: lineItems },
      },
    });
  }

  await AuditService.log({
    userId: ctx.session.user.id,
    action: "SUBMIT",
    entityType: "BID_SUBMISSION",
    entityId: result.id,
    entityName: referenceNo,
    changes: [
      { field: "tenderId", newValue: input.tenderId },
      { field: "vendorId", newValue: vendorId },
      { field: "totalAmount", newValue: totalAmount },
      { field: "status", newValue: status },
    ],
  });

  return result;
}),
```

- [ ] **Step 3: Add admin oversight endpoints at end of router**

```typescript
getAllBids: superAdminProcedure
  .input(
    z.object({
      status: z.enum(["PENDING_APPROVAL", "SUBMITTED", "APPROVED", "REJECTED", "DRAFT", "WITHDRAWN"]).optional(),
      tenderType: z.enum(["PROJECT", "CATEGORY"]).optional(),
      vendorId: z.string().optional(),
      fromDate: z.date().optional(),
      toDate: z.date().optional(),
      limit: z.number().min(1).max(100).default(20),
      offset: z.number().min(0).default(0),
    }),
  )
  .query(async ({ ctx, input }) => {
    const where: any = {};
    if (input.status) where.status = input.status;
    if (input.vendorId) where.vendorId = input.vendorId;
    if (input.fromDate || input.toDate) {
      where.submittedAt = {};
      if (input.fromDate) where.submittedAt.gte = input.fromDate;
      if (input.toDate) where.submittedAt.lte = input.toDate;
    }
    if (input.tenderType) {
      where.tender = { tenderType: input.tenderType };
    }

    return ctx.db.bidSubmission.findMany({
      where,
      include: {
        vendor: true,
        tender: true,
        bidLineItems: { include: { item: true } },
      },
      take: input.limit,
      skip: input.offset,
      orderBy: { submittedAt: "desc" },
    });
  }),

getPendingApprovals: superAdminProcedure.query(async ({ ctx }) => {
  return ctx.db.bidSubmission.findMany({
    where: { status: "PENDING_APPROVAL" },
    include: {
      vendor: true,
      tender: true,
      bidLineItems: { include: { item: true } },
    },
    orderBy: { submittedAt: "asc" },
  });
}),

approveBid: superAdminProcedure
  .input(
    z.object({
      bidId: z.string(),
      notes: z.string().optional(),
    }),
  )
  .mutation(async ({ ctx, input }) => {
    const bid = await ctx.db.bidSubmission.findUnique({
      where: { id: input.bidId },
      include: { tender: true, vendor: true },
    });

    if (!bid) {
      throw new TRPCError({ code: "NOT_FOUND", message: "Bid not found" });
    }

    const result = await ctx.db.bidSubmission.update({
      where: { id: input.bidId },
      data: {
        status: "APPROVED",
        approvedAt: new Date(),
        approvedById: ctx.session.user.id,
      },
    });

    await AuditService.log({
      userId: ctx.session.user.id,
      action: "APPROVE",
      entityType: "BID_SUBMISSION",
      entityId: input.bidId,
      entityName: bid.referenceNo,
      changes: [
        { field: "status", oldValue: bid.status, newValue: "APPROVED" },
        { field: "notes", newValue: input.notes },
      ],
    });

    return result;
  }),

rejectBid: superAdminProcedure
  .input(
    z.object({
      bidId: z.string(),
      reason: z.string(),
    }),
  )
  .mutation(async ({ ctx, input }) => {
    const bid = await ctx.db.bidSubmission.findUnique({
      where: { id: input.bidId },
    });

    if (!bid) {
      throw new TRPCError({ code: "NOT_FOUND", message: "Bid not found" });
    }

    const result = await ctx.db.bidSubmission.update({
      where: { id: input.bidId },
      data: {
        status: "REJECTED",
        rejectedAt: new Date(),
        rejectedById: ctx.session.user.id,
        rejectionReason: input.reason,
      },
    });

    await AuditService.log({
      userId: ctx.session.user.id,
      action: "REJECT",
      entityType: "BID_SUBMISSION",
      entityId: input.bidId,
      entityName: bid.referenceNo,
      changes: [
        { field: "status", oldValue: bid.status, newValue: "REJECTED" },
        { field: "reason", newValue: input.reason },
      ],
    });

    return result;
  }),

adminWithdrawBid: superAdminProcedure
  .input(
    z.object({
      bidId: z.string(),
      reason: z.string(),
    }),
  )
  .mutation(async ({ ctx, input }) => {
    const bid = await ctx.db.bidSubmission.findUnique({
      where: { id: input.bidId },
    });

    if (!bid) {
      throw new TRPCError({ code: "NOT_FOUND", message: "Bid not found" });
    }

    const result = await ctx.db.bidSubmission.update({
      where: { id: input.bidId },
      data: {
        status: "WITHDRAWN",
      },
    });

    await AuditService.log({
      userId: ctx.session.user.id,
      action: "WITHDRAW",
      entityType: "BID_SUBMISSION",
      entityId: input.bidId,
      entityName: bid.referenceNo,
      changes: [
        { field: "status", oldValue: bid.status, newValue: "WITHDRAWN" },
        { field: "reason", newValue: input.reason },
      ],
    });

    return result;
  }),

getBidAnalytics: superAdminProcedure.query(async ({ ctx }) => {
  const [totalBids, pendingBids, approvedBids, rejectedBids, avgAmount] = await Promise.all([
    ctx.db.bidSubmission.count(),
    ctx.db.bidSubmission.count({ where: { status: "PENDING_APPROVAL" } }),
    ctx.db.bidSubmission.count({ where: { status: "APPROVED" } }),
    ctx.db.bidSubmission.count({ where: { status: "REJECTED" } }),
    ctx.db.bidSubmission.aggregate({
      _avg: { totalAmount: true },
      where: { status: { in: ["SUBMITTED", "APPROVED"] } },
    }),
  ]);

  return {
    totalBids,
    pendingBids,
    approvedBids,
    rejectedBids,
    avgBidAmount: avgAmount._avg.totalAmount ?? 0,
  };
}),
```

- [ ] **Step 4: Register new endpoints in \_app router**

Check `src/server/routers/_app.ts` and ensure bidRouter is registered.

- [ ] **Step 5: Commit**

```bash
git add src/server/routers/bid.router.ts
git commit -m "feat: add approval logic and admin oversight endpoints"
```

---

## Task 4: Add Bid Permissions

**Files:**

- Modify: `src/lib/permissions.ts`

- [ ] **Step 1: Add bid permissions**

```typescript
type Permission =
  // ... existing permissions ...
  | "bids:read" // NEW
  | "bids:approve" // NEW
  | "bids:reject" // NEW
  | "bids:withdraw" // NEW
  | "bids:read:own" // NEW
  | "bids:edit:own"; // NEW
```

- [ ] **Step 2: Add to SUPER_ADMIN role**

```typescript
const rolePermissions: Record<Role, Permission[]> = {
  SUPER_ADMIN: [
    // ... existing permissions ...
    "bids:read",
    "bids:approve",
    "bids:reject",
    "bids:withdraw",
  ],
  // ... existing roles ...
  VENDOR: [
    "tenders:read",
    "bids:submit", // NEW
    "bids:read:own", // NEW
    "bids:edit:own", // NEW
  ],
};
```

- [ ] **Step 3: Commit**

```bash
git add src/lib/permissions.ts
git commit -m "feat: add bid permissions for vendors and admins"
```

---

## Task 5: Context Switch Toggle

**Files:**

- Modify: `src/components/layouts/header.tsx`
- Modify: `src/app/vendor-portal/layout.tsx`

- [ ] **Step 1: Create view mode context**

Create `src/contexts/ViewModeContext.tsx`:

```typescript
"use client";

import { createContext, useContext, useState, ReactNode } from "react";

type ViewMode = "admin" | "vendor";

interface ViewModeContextType {
  mode: ViewMode;
  setMode: (mode: ViewMode) => void;
  toggle: () => void;
}

const ViewModeContext = createContext<ViewModeContextType | undefined>(undefined);

export function ViewModeProvider({ children }: { children: ReactNode }) {
  const [mode, setMode] = useState<ViewMode>("admin");

  const toggle = () => {
    setMode((prev) => (prev === "admin" ? "vendor" : "admin"));
  };

  return (
    <ViewModeContext.Provider value={{ mode, setMode, toggle }}>
      {children}
    </ViewModeContext.Provider>
  );
}

export function useViewMode() {
  const context = useContext(ViewModeContext);
  if (!context) throw new Error("useViewMode must be used within ViewModeProvider");
  return context;
}
```

- [ ] **Step 2: Update header with context switch toggle**

```typescript
"use client";
import { signOut } from "next-auth/react";
import { Button } from "@/components/ui/button";
import { trpc } from "@/trpc/client";
import { useViewMode } from "@/contexts/ViewModeContext";
import { useRouter } from "next/navigation";

export function Header() {
  const { data: session } = trpc.auth.getSession.useQuery();
  const { mode, toggle } = useViewMode();
  const router = useRouter();

  const isVendorUser = session?.user?.role === "VENDOR" ||
                       (session?.user?.role === "SUPER_ADMIN" && hasVendorProfile);

  const handleToggle = () => {
    toggle();
    if (mode === "admin") {
      router.push("/vendor-portal/tenders");
    } else {
      router.push("/dashboard");
    }
  };

  return (
    <header className="bg-card flex h-16 items-center justify-between border-b px-6">
      <h1 className="text-lg font-semibold">Tender Management System</h1>
      <div className="flex items-center gap-4">
        {isVendorUser && (
          <Button variant="outline" size="sm" onClick={handleToggle}>
            {mode === "admin" ? "Switch to Vendor View" : "Switch to Admin View"}
          </Button>
        )}
        <span className="text-muted-foreground text-sm">
          {session?.user?.name} ({session?.user?.role})
        </span>
        <Button variant="outline" onClick={() => signOut()}>
          Sign Out
        </Button>
      </div>
    </header>
  );
}
```

- [ ] **Step 3: Update vendor portal layout to allow super admin**

Modify `src/app/vendor-portal/layout.tsx`:

```typescript
export default async function VendorPortalLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await auth();
  // Allow VENDOR role OR SUPER_ADMIN with vendor profile
  const canAccess =
    session?.user?.role === "VENDOR" ||
    (session?.user?.role === "SUPER_ADMIN" &&
      (await hasVendorProfile(session.user.email)));

  if (!canAccess) {
    redirect("/login");
  }
  // ... rest unchanged
}
```

- [ ] **Step 4: Add ViewModeProvider to root layout**

Add to `src/app/layout.tsx`:

```typescript
import { ViewModeProvider } from "@/contexts/ViewModeContext";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <TRPCProvider>
          <ViewModeProvider>
            {children}
          </ViewModeProvider>
        </TRPCProvider>
      </body>
    </html>
  );
}
```

- [ ] **Step 5: Commit**

```bash
git add src/contexts/ViewModeContext.tsx src/components/layouts/header.tsx src/app/vendor-portal/layout.tsx src/app/layout.tsx
git commit -m "feat: add vendor/admin context switch toggle"
```

---

## Task 6: Admin Oversight Pages

**Files:**

- Create: `src/app/(dashboard)/vendor-oversight/page.tsx`
- Create: `src/app/(dashboard)/tender/[id]/bids/page.tsx`
- Create: `src/components/bids/BidStatusBadge.tsx`
- Create: `src/components/bids/BidApprovalCard.tsx`
- Create: `src/components/bids/BidComparisonTable.tsx`
- Create: `src/components/bids/VendorBidFilters.tsx`

- [ ] **Step 1: Create BidStatusBadge component**

```typescript
// src/components/bids/BidStatusBadge.tsx
import type { BidStatus } from "@prisma/generated/client";

interface BidStatusBadgeProps {
  status: BidStatus;
}

const statusConfig: Record<BidStatus, { label: string; className: string }> = {
  DRAFT: { label: "Draft", className: "bg-gray-100 text-gray-800" },
  PENDING_APPROVAL: { label: "Pending Approval", className: "bg-yellow-100 text-yellow-800" },
  SUBMITTED: { label: "Submitted", className: "bg-blue-100 text-blue-800" },
  APPROVED: { label: "Approved", className: "bg-green-100 text-green-800" },
  REJECTED: { label: "Rejected", className: "bg-red-100 text-red-800" },
  WITHDRAWN: { label: "Withdrawn", className: "bg-gray-100 text-gray-600" },
};

export function BidStatusBadge({ status }: BidStatusBadgeProps) {
  const config = statusConfig[status] ?? statusConfig.DRAFT;
  return (
    <span className={`inline-flex rounded-full px-2 py-1 text-xs font-medium ${config.className}`}>
      {config.label}
    </span>
  );
}
```

- [ ] **Step 2: Create BidApprovalCard component**

```typescript
// src/components/bids/BidApprovalCard.tsx
"use client";

import { trpc } from "@/trpc/client";
import { Button } from "@/components/ui/button";
import { BidStatusBadge } from "./BidStatusBadge";
import { useState } from "react";

interface BidApprovalCardProps {
  bid: {
    id: string;
    referenceNo: string;
    totalAmount: any;
    submittedAt: Date | null;
    vendor: { name: string; email: string };
    tender: { title: string; referenceNo: string };
    bidLineItems: { quantity: any; quotedRate: any }[];
  };
}

export function BidApprovalCard({ bid }: BidApprovalCardProps) {
  const utils = trpc.useUtils();
  const [rejectReason, setRejectReason] = useState("");
  const [showReject, setShowReject] = useState(false);

  const approve = trpc.bid.approveBid.useMutation({
    onSuccess: () => utils.bid.getPendingApprovals.invalidate(),
  });

  const reject = trpc.bid.rejectBid.useMutation({
    onSuccess: () => utils.bid.getPendingApprovals.invalidate(),
  });

  const handleApprove = () => {
    approve.mutate({ bidId: bid.id });
  };

  const handleReject = () => {
    if (!rejectReason.trim()) return;
    reject.mutate({ bidId: bid.id, reason: rejectReason });
  };

  return (
    <div className="rounded-lg border bg-card p-4">
      <div className="mb-3 flex items-center justify-between">
        <div>
          <p className="font-medium">{bid.vendor.name}</p>
          <p className="text-muted-foreground text-sm">{bid.vendor.email}</p>
        </div>
        <BidStatusBadge status={bid.status} />
      </div>

      <div className="mb-3">
        <p className="text-sm">
          <span className="text-muted-foreground">Tender:</span> {bid.tender.title}
        </p>
        <p className="text-sm">
          <span className="text-muted-foreground">Bid Ref:</span> {bid.referenceNo}
        </p>
        <p className="text-sm">
          <span className="text-muted-foreground">Amount:</span> Rs. {Number(bid.totalAmount).toLocaleString()}
        </p>
        <p className="text-sm">
          <span className="text-muted-foreground">Submitted:</span>{" "}
          {bid.submittedAt ? new Date(bid.submittedAt).toLocaleString() : "N/A"}
        </p>
      </div>

      {!showReject ? (
        <div className="flex gap-2">
          <Button size="sm" onClick={handleApprove} disabled={approve.isPending}>
            Approve
          </Button>
          <Button size="sm" variant="outline" onClick={() => setShowReject(true)}>
            Reject
          </Button>
        </div>
      ) : (
        <div className="space-y-2">
          <textarea
            className="w-full rounded-md border p-2 text-sm"
            placeholder="Rejection reason..."
            value={rejectReason}
            onChange={(e) => setRejectReason(e.target.value)}
          />
          <div className="flex gap-2">
            <Button size="sm" variant="destructive" onClick={handleReject} disabled={reject.isPending}>
              Confirm Reject
            </Button>
            <Button size="sm" variant="ghost" onClick={() => setShowReject(false)}>
              Cancel
            </Button>
          </div>
        </div>
      )}
    </div>
  );
}
```

- [ ] **Step 3: Create VendorBidFilters component**

```typescript
// src/components/bids/VendorBidFilters.tsx
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";

interface VendorBidFiltersProps {
  onFilter: (filters: {
    status?: string;
    tenderType?: string;
    fromDate?: string;
    toDate?: string;
  }) => void;
}

export function VendorBidFilters({ onFilter }: VendorBidFiltersProps) {
  const [status, setStatus] = useState<string>("");
  const [tenderType, setTenderType] = useState<string>("");

  const handleApply = () => {
    onFilter({
      status: status || undefined,
      tenderType: tenderType || undefined,
    });
  };

  const handleReset = () => {
    setStatus("");
    setTenderType("");
    onFilter({});
  };

  return (
    <div className="flex flex-wrap gap-4 rounded-lg border bg-card p-4">
      <Select value={status} onValueChange={setStatus}>
        <SelectTrigger className="w-[180px]">
          <SelectValue placeholder="Status" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="PENDING_APPROVAL">Pending Approval</SelectItem>
          <SelectItem value="SUBMITTED">Submitted</SelectItem>
          <SelectItem value="APPROVED">Approved</SelectItem>
          <SelectItem value="REJECTED">Rejected</SelectItem>
          <SelectItem value="WITHDRAWN">Withdrawn</SelectItem>
        </SelectContent>
      </Select>

      <Select value={tenderType} onValueChange={setTenderType}>
        <SelectTrigger className="w-[180px]">
          <SelectValue placeholder="Tender Type" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="PROJECT">Project</SelectItem>
          <SelectItem value="CATEGORY">Category</SelectItem>
        </SelectContent>
      </Select>

      <Button onClick={handleApply}>Apply Filters</Button>
      <Button variant="ghost" onClick={handleReset}>Reset</Button>
    </div>
  );
}
```

- [ ] **Step 4: Create BidComparisonTable component**

```typescript
// src/components/bids/BidComparisonTable.tsx
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";

interface BidLineItem {
  item: { name: string; code: string | null };
  quantity: any;
  quotedRate: any;
  totalAmount: any;
}

interface Bid {
  id: string;
  referenceNo: string;
  vendor: { name: string };
  totalAmount: any;
  status: string;
  bidLineItems: BidLineItem[];
}

interface BidComparisonTableProps {
  bids: Bid[];
}

export function BidComparisonTable({ bids }: BidComparisonTableProps) {
  if (bids.length === 0) return <p className="text-muted-foreground">No bids to compare</p>;

  const allItems = Array.from(
    new Set(bids.flatMap((b) => b.bidLineItems.map((i) => i.item.code ?? i.item.name)))
  );

  return (
    <div className="overflow-x-auto">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Item</TableHead>
            {bids.map((bid) => (
              <TableHead key={bid.id} className="text-center">
                {bid.vendor.name}
                <br />
                <span className="text-xs font-normal">{bid.referenceNo}</span>
              </TableHead>
            ))}
          </TableRow>
        </TableHeader>
        <TableBody>
          {allItems.map((itemCode) => (
            <TableRow key={itemCode}>
              <TableCell className="font-medium">{itemCode}</TableCell>
              {bids.map((bid) => {
                const lineItem = bid.bidLineItems.find(
                  (i) => i.item.code === itemCode || i.item.name === itemCode
                );
                return (
                  <TableCell key={bid.id} className="text-center">
                    {lineItem ? (
                      <>
                        <div>Qty: {Number(lineItem.quantity)}</div>
                        <div>Rate: Rs. {Number(lineItem.quotedRate).toLocaleString()}</div>
                        <div className="font-medium">
                          Total: Rs. {Number(lineItem.totalAmount).toLocaleString()}
                        </div>
                      </>
                    ) : (
                      <span className="text-muted-foreground">-</span>
                    )}
                  </TableCell>
                );
              })}
            </TableRow>
          ))}
          <TableRow>
            <TableCell className="font-bold">TOTAL</TableCell>
            {bids.map((bid) => (
              <TableCell key={bid.id} className="text-center font-bold">
                Rs. {Number(bid.totalAmount).toLocaleString()}
              </TableCell>
            ))}
          </TableRow>
        </TableBody>
      </Table>
    </div>
  );
}
```

- [ ] **Step 5: Create vendor oversight page**

```typescript
// src/app/(dashboard)/vendor-oversight/page.tsx
"use client";

import { trpc } from "@/trpc/client";
import { BidApprovalCard } from "@/components/bids/BidApprovalCard";
import { BidStatusBadge } from "@/components/bids/BidStatusBadge";
import { VendorBidFilters } from "@/components/bids/VendorBidFilters";
import { BidComparisonTable } from "@/components/bids/BidComparisonTable";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

export default function VendorOversightPage() {
  const [filters, setFilters] = useState<{
    status?: string;
    tenderType?: string;
    fromDate?: string;
    toDate?: string;
  }>({});
  const [showComparison, setShowComparison] = useState(false);

  const { data: analytics, isLoading: analyticsLoading } = trpc.bid.getBidAnalytics.useQuery();
  const { data: pendingBids, isLoading: pendingLoading } = trpc.bid.getPendingApprovals.useQuery();
  const { data: allBids, isLoading: allBidsLoading } = trpc.bid.getAllBids.useQuery(filters);

  const handleFilter = (newFilters: typeof filters) => {
    setFilters(newFilters);
  };

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-2xl font-bold">Vendor Bid Oversight</h1>
      </div>

      {/* Analytics Cards */}
      <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
        <Card>
          <CardHeader className="pb-2">
            <CardTitle className="text-sm font-medium text-muted-foreground">Total Bids</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{analytics?.totalBids ?? 0}</div>
          </CardContent>
        </Card>
        <Card>
          <CardHeader className="pb-2">
            <CardTitle className="text-sm font-medium text-muted-foreground">Pending Approval</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold text-yellow-600">{analytics?.pendingBids ?? 0}</div>
          </CardContent>
        </Card>
        <Card>
          <CardHeader className="pb-2">
            <CardTitle className="text-sm font-medium text-muted-foreground">Approved</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold text-green-600">{analytics?.approvedBids ?? 0}</div>
          </CardContent>
        </Card>
        <Card>
          <CardHeader className="pb-2">
            <CardTitle className="text-sm font-medium text-muted-foreground">Avg Bid Amount</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">
              Rs. {(Number(analytics?.avgBidAmount) || 0).toLocaleString()}
            </div>
          </CardContent>
        </Card>
      </div>

      {/* Pending Approvals Section */}
      {pendingBids && pendingBids.length > 0 && (
        <section>
          <h2 className="mb-4 text-lg font-semibold">Pending Approvals ({pendingBids.length})</h2>
          <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
            {pendingBids.map((bid) => (
              <BidApprovalCard key={bid.id} bid={bid} />
            ))}
          </div>
        </section>
      )}

      {/* All Bids Section */}
      <section>
        <div className="mb-4 flex items-center justify-between">
          <h2 className="text-lg font-semibold">All Bids</h2>
          <Button
            variant="outline"
            onClick={() => setShowComparison(!showComparison)}
          >
            {showComparison ? "Hide Comparison" : "Show Comparison"}
          </Button>
        </div>

        <VendorBidFilters onFilter={handleFilter} />

        {showComparison && allBids && allBids.length > 0 && (
          <div className="mb-6 mt-4">
            <h3 className="mb-2 text-sm font-medium">Bid Comparison</h3>
            <BidComparisonTable bids={allBids.slice(0, 5)} />
          </div>
        )}

        <div className="mt-4 overflow-x-auto">
          <table className="w-full border-collapse text-sm">
            <thead>
              <tr className="border-b bg-muted/50">
                <th className="px-4 py-2 text-left">Vendor</th>
                <th className="px-4 py-2 text-left">Tender</th>
                <th className="px-4 py-2 text-right">Amount</th>
                <th className="px-4 py-2 text-center">Status</th>
                <th className="px-4 py-2 text-left">Submitted</th>
                <th className="px-4 py-2 text-left">Actions</th>
              </tr>
            </thead>
            <tbody>
              {allBids?.map((bid) => (
                <tr key={bid.id} className="border-b">
                  <td className="px-4 py-2">{bid.vendor.name}</td>
                  <td className="px-4 py-2">{bid.tender.title}</td>
                  <td className="px-4 py-2 text-right">
                    Rs. {Number(bid.totalAmount).toLocaleString()}
                  </td>
                  <td className="px-4 py-2 text-center">
                    <BidStatusBadge status={bid.status} />
                  </td>
                  <td className="px-4 py-2">
                    {bid.submittedAt ? new Date(bid.submittedAt).toLocaleDateString() : "-"}
                  </td>
                  <td className="px-4 py-2">
                    <Button size="sm" variant="ghost" asChild>
                      <a href={`/tender/${bid.tenderId}/bids`}>View</a>
                    </Button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </section>
    </div>
  );
}
```

- [ ] **Step 6: Create per-tender bid management page**

```typescript
// src/app/(dashboard)/tender/[id]/bids/page.tsx
"use client";

import { trpc } from "@/trpc/client";
import { BidApprovalCard } from "@/components/bids/BidApprovalCard";
import { BidStatusBadge } from "@/components/bids/BidStatusBadge";
import { BidComparisonTable } from "@/components/bids/BidComparisonTable";
import { Button } from "@/components/ui/button";
import { useState } from "react";

interface TenderBidsPageProps {
  params: { id: string };
}

export default function TenderBidsPage({ params }: TenderBidsPageProps) {
  const [showComparison, setShowComparison] = useState(true);

  const { data: tender, isLoading: tenderLoading } = trpc.tender.getById.useQuery({
    id: params.id,
  });

  const { data: bids, isLoading: bidsLoading } = trpc.bid.getTenderBids.useQuery({
    tenderId: params.id,
  });

  if (tenderLoading || bidsLoading) return <div>Loading...</div>;

  return (
    <div className="space-y-6">
      <div>
        <h1 className="text-2xl font-bold">Bids for: {tender?.title}</h1>
        <p className="text-muted-foreground">Reference: {tender?.referenceNo}</p>
      </div>

      <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
        <div className="rounded-lg border bg-card p-4">
          <p className="text-sm text-muted-foreground">Total Bids</p>
          <p className="text-2xl font-bold">{bids?.length ?? 0}</p>
        </div>
        <div className="rounded-lg border bg-card p-4">
          <p className="text-sm text-muted-foreground">Total Value</p>
          <p className="text-2xl font-bold">
            Rs. {(bids?.reduce((sum, b) => sum + Number(b.totalAmount ?? 0), 0) ?? 0).toLocaleString()}
          </p>
        </div>
        <div className="rounded-lg border bg-card p-4">
          <p className="text-sm text-muted-foreground">Lowest Bid</p>
          <p className="text-2xl font-bold">
            Rs. {(Math.min(...(bids?.map((b) => Number(b.totalAmount ?? Infinity)) ?? [0])))?.toLocaleString() ?? 0}
          </p>
        </div>
      </div>

      <div className="flex items-center justify-between">
        <h2 className="text-lg font-semibold">All Bids</h2>
        <Button
          variant="outline"
          onClick={() => setShowComparison(!showComparison)}
        >
          {showComparison ? "Hide Comparison" : "Show Comparison"}
        </Button>
      </div>

      {showComparison && bids && bids.length > 0 && (
        <div className="mb-6">
          <h3 className="mb-2 text-sm font-medium">Bid Comparison</h3>
          <BidComparisonTable bids={bids} />
        </div>
      )}

      <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
        {bids?.map((bid) => (
          <BidApprovalCard key={bid.id} bid={bid} />
        ))}
      </div>
    </div>
  );
}
```

- [ ] **Step 7: Add link to sidebar**

Modify `src/components/layouts/sidebar.tsx` to add vendor-oversight link.

- [ ] **Step 8: Commit**

```bash
git add src/app/\(dashboard\)/vendor-oversight/page.tsx src/app/\(dashboard\)/tender/\[id\]/bids/page.tsx
git add src/components/bids/BidStatusBadge.tsx src/components/bids/BidApprovalCard.tsx src/components/bids/BidComparisonTable.tsx src/components/bids/VendorBidFilters.tsx
git commit -m "feat: add vendor oversight dashboard and per-tender bid management pages"
```

---

## Task 7: Update Vendor Portal Layout for Super Admin

**Files:**

- Modify: `src/app/vendor-portal/layout.tsx`

- [ ] **Step 1: Update layout to check for dual-role users**

```typescript
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { TRPCProvider } from "@/trpc/provider";
import { VendorSidebar } from "@/components/layouts/vendor-sidebar";
import { VendorHeader } from "@/components/layouts/vendor-header";
import { PageContainer } from "@/components/layouts/page-container";
import { db } from "@/lib/db";

export default async function VendorPortalLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await auth();

  // Allow VENDOR role OR SUPER_ADMIN with vendor profile
  let canAccess = session?.user?.role === "VENDOR";

  if (session?.user?.role === "SUPER_ADMIN") {
    const vendor = await db.vendor.findFirst({
      where: { email: session.user.email },
    });
    canAccess = !!vendor;
  }

  if (!canAccess) {
    redirect("/login");
  }

  return (
    <TRPCProvider>
      <div className="flex h-screen bg-background">
        <VendorSidebar userName={session.user.name ?? undefined} />
        <div className="flex flex-1 flex-col overflow-hidden">
          <VendorHeader userName={session.user.name ?? undefined} />
          <main className="flex-1 overflow-y-auto p-6">
            <PageContainer>
              {children}
            </PageContainer>
          </main>
        </div>
      </div>
    </TRPCProvider>
  );
}
```

- [ ] **Step 2: Commit**

```bash
git add src/app/vendor-portal/layout.tsx
git commit -m "feat: allow super admin with vendor profile to access vendor portal"
```

---

## Verification Checklist

- [ ] Run `npm run lint` and fix any issues
- [ ] Run `npm run typecheck` and fix any issues
- [ ] Run `npx prisma db push` to verify schema
- [ ] Test vendor login and bid submission flow
- [ ] Test super admin context switch toggle
- [ ] Test bid approval/rejection flow
- [ ] Verify vendor oversight dashboard loads all bids
- [ ] Verify per-tender bid management page works

---

**Plan complete and saved to `docs/superpowers/plans/2026-03-30-vendor-bidding-plan.md`**

**Two execution options:**

**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration

**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints

**Which approach?**
