+ {/* Breadcrumb */}
+
+ Items
+ /
+ {product.name}
+
+
+ {/* Header */}
+
+
+
+ {product.code}
+
+
{product.name}
+ {product.description &&
{product.description}
}
+
+ {minPrice !== null && (
+
+ )}
+
+
+ {/* Stats */}
+
+
+
Vendors
+
{product.vendorPrices.length}
+
+
+
Lowest Price
+
{minPrice !== null ? formatCurrency(minPrice) : "—"}
+
+
+
Highest Price
+
{maxPrice !== null ? formatCurrency(maxPrice) : "—"}
+
+
+
+ {/* Price chart */}
+ {priceChartData.length > 1 &&
}
+
+ {/* Site filter */}
+ {sites.length > 0 && (
+
({ id: s.id, name: s.name }))}
+ currentSiteId={siteId ?? null}
+ baseHref={baseHref}
+ />
+ )}
+
+ {/* Vendors table */}
+
+
+ Available From
+ ({enriched.length} vendor{enriched.length !== 1 ? "s" : ""})
+ {selectedSite && sorted by distance from {selectedSite.name}}
+
+ {enriched.length === 0 ? (
+
No vendor pricing on record. Updated automatically when a PO is marked as paid.
+ ) : (
+
+
+
+ | Vendor |
+ Verified |
+ Price |
+ {selectedSite && Distance | }
+ Updated |
+ |
+
+
+
+ {enriched.map((vp, idx) => {
+ const isCheapest = minPrice !== null && vp.price === minPrice && enriched.length > 1;
+ const isClosest = selectedSite !== null && idx === 0 && vp.distanceKm !== null;
+ return (
+
+ |
+
+ {vp.vendor.name}
+
+ |
+
+
+ {vp.vendor.isVerified ? "Verified" : "Unverified"}
+
+ |
+
+ {formatCurrency(vp.price)}
+ {isCheapest && !selectedSite && lowest}
+ |
+ {selectedSite && (
+
+ {vp.distanceKm !== null
+ ? {formatDistance(vp.distanceKm)}{isClosest ? " ★" : ""}
+ : No location}
+ |
+ )}
+ {formatDate(vp.updatedAt)} |
+
+
+ |
+
+ );
+ })}
+
+
+ )}
+
+
+ {/* Stock by site */}
+ {product.inventory.length > 0 && (
+
+
Stock on Hand
+
+ {product.inventory.map((inv) => (
+
+ {inv.site.name}
+ ({inv.site.code})
+ {Number(inv.quantity)} units
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/App/pelagia-portal/app/(portal)/inventory/items/[id]/site-select.tsx b/App/pelagia-portal/app/(portal)/inventory/items/[id]/site-select.tsx
new file mode 100644
index 0000000..798d0bb
--- /dev/null
+++ b/App/pelagia-portal/app/(portal)/inventory/items/[id]/site-select.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { MapPin } from "lucide-react";
+
+type SiteOption = { id: string; name: string };
+
+export function SiteSelect({
+ sites,
+ currentSiteId,
+ baseHref,
+ paramKey = "siteId",
+}: {
+ sites: SiteOption[];
+ currentSiteId: string | null;
+ baseHref: string;
+ paramKey?: string;
+}) {
+ const router = useRouter();
+ return (
+
- {vendor.vendorName}
+ e.stopPropagation()}
+ className="font-medium text-neutral-800 hover:text-primary-600 hover:underline"
+ >
+ {vendor.vendorName}
+
{vendor.isVerified && (
Verified
)}
diff --git a/App/pelagia-portal/app/(portal)/inventory/vendors/[id]/page.tsx b/App/pelagia-portal/app/(portal)/inventory/vendors/[id]/page.tsx
new file mode 100644
index 0000000..184c08c
--- /dev/null
+++ b/App/pelagia-portal/app/(portal)/inventory/vendors/[id]/page.tsx
@@ -0,0 +1,132 @@
+import { auth } from "@/auth";
+import { db } from "@/lib/db";
+import { notFound, redirect } from "next/navigation";
+import Link from "next/link";
+import { formatCurrency, formatDate } from "@/lib/utils";
+import { VendorItemsTable } from "./vendor-items-table";
+import type { Metadata } from "next";
+
+interface Props { params: Promise<{ id: string }> }
+
+export async function generateMetadata({ params }: Props): Promise {
+ const { id } = await params;
+ const vendor = await db.vendor.findUnique({ where: { id }, select: { name: true } });
+ return { title: vendor?.name ?? "Vendor Detail" };
+}
+
+export default async function InventoryVendorDetailPage({ params }: Props) {
+ const session = await auth();
+ if (!session?.user) redirect("/login");
+
+ const { id } = await params;
+
+ const vendor = await db.vendor.findUnique({
+ where: { id, isActive: true },
+ include: {
+ contacts: { orderBy: [{ isPrimary: "desc" }, { createdAt: "asc" }] },
+ vendorPrices: {
+ include: { product: { select: { id: true, code: true, name: true, description: true, isActive: true } } },
+ orderBy: { updatedAt: "desc" },
+ },
+ },
+ });
+
+ if (!vendor) notFound();
+
+ const items = vendor.vendorPrices.map((vp) => ({
+ id: vp.id,
+ productId: vp.product.id,
+ code: vp.product.code,
+ name: vp.product.name,
+ description: vp.product.description ?? "",
+ isActive: vp.product.isActive,
+ price: Number(vp.price),
+ updatedAt: vp.updatedAt.toISOString(),
+ }));
+
+ return (
+
+ {/* Breadcrumb */}
+
+ Vendors
+ /
+ {vendor.name}
+
+
+ {/* Header */}
+
+
+ {vendor.vendorId && {vendor.vendorId}}
+
+ {vendor.isVerified ? "Verified" : "Unverified"}
+
+
+ {vendor.name}
+
+
+ {/* Details + Contacts */}
+
+
+ Vendor Details
+
+ {vendor.gstin && (
+
+ - GSTIN
+ - {vendor.gstin}
+
+ )}
+ {vendor.address && (
+
+ - Address
+ - {vendor.address}
+
+ )}
+ {vendor.pincode && (
+
+ - Pincode
+ - {vendor.pincode}
+
+ )}
+ {!vendor.gstin && !vendor.address && !vendor.pincode && (
+ No additional details on record.
+ )}
+
+
+
+
+
+ Contacts
+ ({vendor.contacts.length})
+
+ {vendor.contacts.length === 0 ? (
+ No contacts on record.
+ ) : (
+
+ {vendor.contacts.map((c) => (
+
+
+ {c.name.slice(0, 2).toUpperCase()}
+
+
+
+ {c.name}
+ {c.role && {c.role}}
+ {c.isPrimary && Primary}
+
+
+ {c.mobile && {c.mobile}}
+ {c.email && {c.email}}
+
+
+
+ ))}
+
+ )}
+
+
+
+ {/* Items supplied */}
+
+
+ );
+}
diff --git a/App/pelagia-portal/app/(portal)/inventory/vendors/[id]/vendor-items-table.tsx b/App/pelagia-portal/app/(portal)/inventory/vendors/[id]/vendor-items-table.tsx
new file mode 100644
index 0000000..d80a4e9
--- /dev/null
+++ b/App/pelagia-portal/app/(portal)/inventory/vendors/[id]/vendor-items-table.tsx
@@ -0,0 +1,101 @@
+"use client";
+
+import { useState, useMemo } from "react";
+import { Search, X } from "lucide-react";
+import Link from "next/link";
+import { formatCurrency, formatDate } from "@/lib/utils";
+import { AddToCartButton } from "@/components/inventory/add-to-cart-button";
+
+type Item = {
+ id: string;
+ productId: string;
+ code: string;
+ name: string;
+ description: string;
+ isActive: boolean;
+ price: number;
+ updatedAt: string;
+};
+
+export function VendorItemsTable({ items }: { items: Item[] }) {
+ const [query, setQuery] = useState("");
+
+ const filtered = useMemo(() => {
+ const q = query.toLowerCase().trim();
+ if (!q) return items;
+ return items.filter(
+ (item) =>
+ item.name.toLowerCase().includes(q) ||
+ item.code.toLowerCase().includes(q) ||
+ item.description.toLowerCase().includes(q)
+ );
+ }, [items, query]);
+
+ return (
+
+
+
+ Items Supplied
+ ({filtered.length}{query ? ` of ${items.length}` : ""})
+
+
+
+ setQuery(e.target.value)}
+ placeholder="Search items…"
+ className="w-full rounded-lg border border-neutral-200 py-1.5 pl-8 pr-8 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20"
+ />
+ {query && (
+
+ )}
+
+
+
+ {items.length === 0 ? (
+
+ No items on record yet. Updated automatically when a PO with this vendor is marked as paid.
+
+ ) : filtered.length === 0 ? (
+ No items match "{query}"
+ ) : (
+
+
+
+ | Item |
+ Code |
+ Price |
+ Updated |
+ |
+
+
+
+ {filtered.map((item) => (
+
+ |
+
+ {item.name}
+
+ {item.description && (
+ {item.description}
+ )}
+ |
+ {item.code} |
+ {formatCurrency(item.price)} |
+ {formatDate(new Date(item.updatedAt))} |
+
+
+ |
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/App/pelagia-portal/app/(portal)/inventory/vendors/vendors-table.tsx b/App/pelagia-portal/app/(portal)/inventory/vendors/vendors-table.tsx
index fe27608..5eeaeb5 100644
--- a/App/pelagia-portal/app/(portal)/inventory/vendors/vendors-table.tsx
+++ b/App/pelagia-portal/app/(portal)/inventory/vendors/vendors-table.tsx
@@ -2,6 +2,7 @@
import { useState, useMemo } from "react";
import { useRouter } from "next/navigation";
+import Link from "next/link";
import { Search, X, MapPin, Tag } from "lucide-react";
import { formatDistance } from "@/lib/geo";
@@ -147,7 +148,9 @@ export function VendorsTable({
|
- {vendor.name}
+
+ {vendor.name}
+
{vendor.isVerified && (
Verified
)}
| |