# Vendor CRUD 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:** Add full CRUD (Create, Read, Update, Delete) operations for vendors in superadmin dashboard

**Architecture:** Add update and delete mutations to vendor router, update frontend page with edit dialog and delete confirmation

**Tech Stack:** Next.js, tRPC, React, shadcn/ui

---

## File Structure

```
src/server/routers/
  - vendor.router.ts (add update + delete mutations)

src/app/(dashboard)/vendors/
  - page.tsx (add edit dialog + delete button)
```

---

### Task 1: Add DEACTIVATED status to Prisma schema

**Files:**
- Modify: `prisma/schema.prisma`

- [ ] **Step 1: Update VendorStatus enum**

```prisma
enum VendorStatus {
  PENDING
  APPROVED
  REJECTED
  DEACTIVATED  // Add this
}
```

- [ ] **Step 2: Commit**

```bash
git add prisma/schema.prisma && git commit -m "feat: add DEACTIVATED vendor status"
```

---

### Task 2: Add update mutation to vendor router

**Files:**
- Modify: `src/server/routers/vendor.router.ts`

- [ ] **Step 1: Add update mutation**

Add after the `create` mutation:

```typescript
update: adminProcedure
  .input(
    z.object({
      id: z.string(),
      name: z.string().min(1),
      email: z.string().email(),
      phone: z.string().optional(),
      vatPan: z.string().min(1),
      address: z.string().optional(),
    }),
  )
  .mutation(async ({ ctx, input }) => {
    const { id, ...data } = input;

    const existing = await ctx.db.vendor.findFirst({
      where: {
        id: { not: id },
        OR: [{ vatPan: data.vatPan }, { email: data.email }],
      },
      select: { vatPan: true, email: true },
    });
    if (existing) {
      throw new TRPCError({
        code: "CONFLICT",
        message:
          existing.vatPan === data.vatPan
            ? "A vendor with this VAT/PAN number already exists."
            : "A vendor with this email already exists.",
      });
    }

    const oldVendor = await ctx.db.vendor.findUnique({ where: { id } });
    const vendor = await ctx.db.vendor.update({
      where: { id },
      data,
    });

    await ctx.db.user.updateMany({
      where: { email: oldVendor?.email },
      data: { email: data.email, name: data.name },
    });

    await AuditService.log({
      userId: ctx.session.user.id,
      action: "UPDATE",
      entityType: "VENDOR",
      entityId: vendor.id,
      entityName: vendor.name,
      changes: AuditService.diff(
        { name: oldVendor?.name, email: oldVendor?.email, vatPan: oldVendor?.vatPan, address: oldVendor?.address },
        { name: data.name, email: data.email, vatPan: data.vatPan, address: data.address ?? null },
        ["name", "email", "vatPan", "address"],
      ),
    });

    return vendor;
  }),
```

- [ ] **Step 2: Commit**

```bash
git add src/server/routers/vendor.router.ts && git commit -m "feat: add vendor update mutation"
```

---

### Task 3: Add delete mutation to vendor router

**Files:**
- Modify: `src/server/routers/vendor.router.ts`

- [ ] **Step 1: Add delete mutation (soft delete)**

Add after the `update` mutation:

```typescript
delete: adminProcedure
  .input(z.object({ id: z.string() }))
  .mutation(async ({ ctx, input }) => {
    const vendor = await ctx.db.vendor.findUnique({
      where: { id: input.id },
    });
    if (!vendor) {
      throw new TRPCError({ code: "NOT_FOUND", message: "Vendor not found" });
    }

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

    await AuditService.log({
      userId: ctx.session.user.id,
      action: "DELETE",
      entityType: "VENDOR",
      entityId: result.id,
      entityName: result.name,
      changes: [{ field: "status", oldValue: vendor.status, newValue: "DEACTIVATED" }],
    });

    publish(CHANNELS.DASHBOARD_STATS);
    return result;
  }),
```

- [ ] **Step 2: Commit**

```bash
git add src/server/routers/vendor.router.ts && git commit -m "feat: add vendor delete mutation"
```

---

### Task 4: Add edit vendor dialog to vendors page

**Files:**
- Modify: `src/app/(dashboard)/vendors/page.tsx`

- [ ] **Step 1: Add vendor type and state**

Add after imports:

```typescript
type VendorForm = {
  id?: string;
  name: string;
  email: string;
  phone: string;
  vatPan: string;
  address: string;
  password?: string;
};

const BLANK: VendorForm = { name: "", email: "", phone: "", vatPan: "", address: "" };
```

- [ ] **Step 2: Add edit state and mutation**

Add to component:

```typescript
const [editVendor, setEditVendor] = useState<VendorForm | null>(null);
const [isEditOpen, setIsEditOpen] = useState(false);

const updateMutation = trpc.vendor.update.useMutation({
  onSuccess: () => {
    toast.success("Vendor updated successfully");
    utils.vendor.list.invalidate();
    setIsEditOpen(false);
    setEditVendor(null);
  },
  onError: (err) => toast.error(err.message),
});
```

- [ ] **Step 3: Add handleUpdate function**

```typescript
const handleUpdate = (e: React.FormEvent) => {
  e.preventDefault();
  if (!editVendor?.id) return;
  updateMutation.mutate({
    id: editVendor.id,
    name: editVendor.name,
    email: editVendor.email,
    phone: editVendor.phone || undefined,
    vatPan: editVendor.vatPan,
    address: editVendor.address || undefined,
  });
};
```

- [ ] **Step 4: Add edit dialog after the create dialog**

```tsx
<Dialog open={isEditOpen} onOpenChange={(open) => {
  setIsEditOpen(open);
  if (!open) setEditVendor(null);
}}>
  <DialogContent className="max-w-lg">
    <DialogHeader>
      <DialogTitle>Edit Vendor</DialogTitle>
    </DialogHeader>
    <form onSubmit={handleUpdate} className="space-y-4">
      <div className="grid gap-4 sm:grid-cols-2">
        <div className="sm:col-span-2">
          <Label>Company / Vendor Name *</Label>
          <Input
            value={editVendor?.name ?? ""}
            onChange={(e) => setEditVendor((v) => v ? { ...v, name: e.target.value } : v)}
            placeholder="ABC Construction Pvt. Ltd."
            required
          />
        </div>
        <div>
          <Label>Email *</Label>
          <Input
            type="email"
            value={editVendor?.email ?? ""}
            onChange={(e) => setEditVendor((v) => v ? { ...v, email: e.target.value } : v)}
            placeholder="vendor@example.com"
            required
          />
        </div>
        <div>
          <Label>Phone</Label>
          <Input
            type="tel"
            value={editVendor?.phone ?? ""}
            onChange={(e) => setEditVendor((v) => v ? { ...v, phone: e.target.value } : v)}
            placeholder="+977-9800000000"
          />
        </div>
        <div>
          <Label>VAT / PAN Number *</Label>
          <Input
            value={editVendor?.vatPan ?? ""}
            onChange={(e) => setEditVendor((v) => v ? { ...v, vatPan: e.target.value } : v)}
            placeholder="e.g. 123456789"
            required
          />
        </div>
        <div>
          <Label>Address</Label>
          <Input
            value={editVendor?.address ?? ""}
            onChange={(e) => setEditVendor((v) => v ? { ...v, address: e.target.value } : v)}
            placeholder="Kathmandu, Nepal"
          />
        </div>
      </div>
      <DialogFooter>
        <Button type="button" variant="outline" onClick={() => { setIsEditOpen(false); setEditVendor(null); }}>
          Cancel
        </Button>
        <Button type="submit" disabled={updateMutation.isPending}>
          {updateMutation.isPending ? "Saving..." : "Save Changes"}
        </Button>
      </DialogFooter>
    </form>
  </DialogContent>
</Dialog>
```

- [ ] **Step 5: Make table row clickable to edit**

Change `<TableRow key={vendor.id}>` to:

```tsx
<TableRow
  key={vendor.id}
  className="cursor-pointer"
  onClick={() => {
    setEditVendor({
      id: vendor.id,
      name: vendor.name,
      email: vendor.email,
      phone: vendor.phone ?? "",
      vatPan: vendor.vatPan,
      address: vendor.address ?? "",
    });
    setIsEditOpen(true);
  }}
>
```

- [ ] **Step 6: Commit**

```bash
git add src/app/\(dashboard\)/vendors/page.tsx && git commit -m "feat: add edit vendor dialog"
```

---

### Task 5: Add delete vendor functionality

**Files:**
- Modify: `src/app/(dashboard)/vendors/page.tsx`

- [ ] **Step 1: Add delete state**

```typescript
const [deleteVendorId, setDeleteVendorId] = useState<string | null>(null);
```

- [ ] **Step 2: Add delete mutation**

```typescript
const deleteMutation = trpc.vendor.delete.useMutation({
  onSuccess: () => {
    toast.success("Vendor deleted successfully");
    utils.vendor.list.invalidate();
    setDeleteVendorId(null);
  },
  onError: (err) => toast.error(err.message),
});
```

- [ ] **Step 3: Add delete confirmation dialog**

Add before the closing `</div>`:

```tsx
<Dialog open={!!deleteVendorId} onOpenChange={(open) => !open && setDeleteVendorId(null)}>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Delete Vendor</DialogTitle>
    </DialogHeader>
    <p className="text-muted-foreground">
      Are you sure you want to delete this vendor? This action cannot be undone.
    </p>
    <DialogFooter>
      <Button variant="outline" onClick={() => setDeleteVendorId(null)}>
        Cancel
      </Button>
      <Button
        variant="destructive"
        onClick={() => deleteVendorId && deleteMutation.mutate({ id: deleteVendorId })}
        disabled={deleteMutation.isPending}
      >
        {deleteMutation.isPending ? "Deleting..." : "Delete"}
      </Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

- [ ] **Step 4: Add delete button in table actions**

Replace the actions cell content:

```tsx
<TableCell>
  <div className="flex items-center gap-2">
    {vendor.status === "PENDING" && (
      <>
        <Button
          size="sm"
          onClick={(e) => { e.stopPropagation(); approveMutation.mutate({ id: vendor.id }); }}
          disabled={approveMutation.isPending}
        >
          Approve
        </Button>
        <Button
          size="sm"
          variant="destructive"
          onClick={(e) => { e.stopPropagation(); rejectMutation.mutate({ id: vendor.id }); }}
          disabled={rejectMutation.isPending}
        >
          Reject
        </Button>
      </>
    )}
    {vendor.status !== "DEACTIVATED" && (
      <Button
        size="sm"
        variant="ghost"
        onClick={(e) => { e.stopPropagation(); setDeleteVendorId(vendor.id); }}
      >
        <Trash2 className="h-4 w-4" />
      </Button>
    )}
  </div>
</TableCell>
```

- [ ] **Step 5: Import Trash2 icon**

Add to imports:

```typescript
import { Trash2 } from "lucide-react";
```

- [ ] **Step 6: Commit**

```bash
git add src/app/\(dashboard\)/vendors/page.tsx && git commit -m "feat: add delete vendor functionality"
```

---

### Task 6: Run typecheck and verify

**Files:**
- Run: `npm run typecheck`

- [ ] **Step 1: Run typecheck**

```bash
npm run typecheck
```

- [ ] **Step 2: Fix any errors**

- [ ] **Step 3: Commit**

```bash
git add . && git commit -m "fix: typecheck fixes"
```

---

## Plan Complete

**Summary:**
1. Added DEACTIVATED status to Prisma schema
2. Added vendor update mutation (backend)
3. Added vendor delete mutation (backend)
4. Added edit vendor dialog (frontend)
5. Added delete vendor with confirmation (frontend)

**Visual layout:** Table rows are now clickable to edit, delete button with trash icon added.
