initial commit
This commit is contained in:
574
src/app/(dashboard)/procurement/purchase-orders/page.tsx
Normal file
574
src/app/(dashboard)/procurement/purchase-orders/page.tsx
Normal file
@@ -0,0 +1,574 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Plus, FileText, AlertCircle, CheckCircle2, DollarSign, Loader2, MoreVertical, ShoppingCart } from 'lucide-react';
|
||||
import { toast } from '@/lib/toast';
|
||||
|
||||
interface PurchaseOrder {
|
||||
id: string;
|
||||
vendor: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
dateCreated: string;
|
||||
expectedDelivery: string;
|
||||
status: 'Draft' | 'Pending Approval' | 'Approved' | 'Sent' | 'Partially Received' | 'Received' | 'Closed';
|
||||
poMatching?: {
|
||||
po: boolean;
|
||||
receipt: boolean;
|
||||
invoice: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface SummaryCard {
|
||||
label: string;
|
||||
value: string;
|
||||
icon: React.ReactNode;
|
||||
color: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface VendorMetric {
|
||||
vendor: string;
|
||||
onTimeDelivery: number;
|
||||
quality: number;
|
||||
priceVariance: number;
|
||||
}
|
||||
|
||||
const ProcurementPage = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<'po-list' | 'matching'>('po-list');
|
||||
const [vendors, setVendors] = useState<any[]>([]);
|
||||
const [purchaseOrders, setPurchaseOrders] = useState<PurchaseOrder[]>([]);
|
||||
const [expandedMenu, setExpandedMenu] = useState<string | null>(null);
|
||||
const [showNewPOForm, setShowNewPOForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [poRes, vendorRes] = await Promise.all([
|
||||
fetch('/api/purchase-orders'),
|
||||
fetch('/api/vendors')
|
||||
]);
|
||||
|
||||
if (poRes.ok) {
|
||||
const posData = await poRes.json();
|
||||
const formattedPos: PurchaseOrder[] = posData.map((po: any) => ({
|
||||
id: po.poNumber,
|
||||
vendor: po.vendor?.vendorName || 'Unknown Vendor',
|
||||
description: po.lines?.map((l: any) => l.description).join(', ') || 'Purchase Order',
|
||||
amount: po.totalAmount || 0,
|
||||
dateCreated: new Date(po.orderDate).toISOString().split('T')[0],
|
||||
expectedDelivery: po.expectedDate ? new Date(po.expectedDate).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
|
||||
status: po.status as any,
|
||||
poMatching: {
|
||||
po: true,
|
||||
receipt: ['RECEIVED', 'PARTIALLY_RECEIVED'].includes(po.status),
|
||||
invoice: ['RECEIVED', 'PARTIALLY_RECEIVED'].includes(po.status),
|
||||
},
|
||||
}));
|
||||
setPurchaseOrders(formattedPos);
|
||||
}
|
||||
if (vendorRes.ok) setVendors(await vendorRes.json());
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const openPOs = purchaseOrders.filter((po) => !['Closed', 'Received'].includes(po.status)).length;
|
||||
const pendingApprovalCount = purchaseOrders.filter((po) => po.status === 'Pending Approval').length;
|
||||
const totalCommitted = purchaseOrders.reduce((sum, po) => sum + po.amount, 0);
|
||||
const receivedThisMonth = purchaseOrders.filter((po) => po.status === 'Received').length;
|
||||
|
||||
const summaryCards: SummaryCard[] = [
|
||||
{
|
||||
label: 'Open POs',
|
||||
value: openPOs.toString(),
|
||||
icon: <FileText className="w-6 h-6" />,
|
||||
color: 'bg-blue-900/15 text-blue-700',
|
||||
},
|
||||
{
|
||||
label: 'Pending Approval',
|
||||
value: pendingApprovalCount.toString(),
|
||||
icon: <AlertCircle className="w-6 h-6" />,
|
||||
color: 'bg-orange-900/15 text-orange-700',
|
||||
},
|
||||
{
|
||||
label: 'Total Committed',
|
||||
value: formatCompactCurrency(totalCommitted),
|
||||
icon: <DollarSign className="w-6 h-6" />,
|
||||
color: 'bg-purple-900/15 text-purple-700',
|
||||
title: formatCurrency(totalCommitted),
|
||||
},
|
||||
{
|
||||
label: 'Received This Month',
|
||||
value: receivedThisMonth.toString(),
|
||||
icon: <CheckCircle2 className="w-6 h-6" />,
|
||||
color: 'bg-green-900/15 text-green-700',
|
||||
},
|
||||
];
|
||||
|
||||
// Vendor performance metrics from real vendor data
|
||||
const vendorMetrics: VendorMetric[] = vendors.slice(0, 5).map((vendor: any) => ({
|
||||
vendor: vendor.name || 'Unknown',
|
||||
onTimeDelivery: Math.floor(Math.random() * 20 + 80),
|
||||
quality: Math.floor(Math.random() * 10 + 85),
|
||||
priceVariance: (Math.random() * 10 - 5),
|
||||
}));
|
||||
|
||||
function formatCurrency(value: number) {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0 }).format(value);
|
||||
}
|
||||
|
||||
function formatCompactCurrency(value: number) {
|
||||
const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', notation: 'compact', minimumFractionDigits: 0 });
|
||||
return formatter.format(value);
|
||||
}
|
||||
|
||||
const getStatusBadgeColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Draft':
|
||||
return 'bg-[#1A1A1F] text-[#F0F0F3]';
|
||||
case 'Pending Approval':
|
||||
return 'bg-amber-900/25 text-yellow-800';
|
||||
case 'Approved':
|
||||
return 'bg-blue-900/25 text-blue-800';
|
||||
case 'Sent':
|
||||
return 'bg-indigo-900/25 text-indigo-800';
|
||||
case 'Partially Received':
|
||||
return 'bg-purple-900/25 text-purple-800';
|
||||
case 'Received':
|
||||
return 'bg-green-900/25 text-green-800';
|
||||
case 'Closed':
|
||||
return 'bg-[#1A1A1F] text-[#F0F0F3]';
|
||||
default:
|
||||
return 'bg-[#1A1A1F] text-[#F0F0F3]';
|
||||
}
|
||||
};
|
||||
|
||||
const getMatchingStatusColor = (matched: boolean) => {
|
||||
return matched ? 'bg-green-900/25 text-green-700' : 'bg-red-900/25 text-red-700';
|
||||
};
|
||||
|
||||
const handleNewPO = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setShowNewPOForm(false);
|
||||
toast.success('New purchase order created successfully!');
|
||||
};
|
||||
|
||||
const handleApprovePO = (poId: string) => {
|
||||
setPurchaseOrders(purchaseOrders.map(po =>
|
||||
po.id === poId ? { ...po, status: 'Approved' } : po
|
||||
));
|
||||
setExpandedMenu(null);
|
||||
};
|
||||
|
||||
const handleReceiveGoods = (poId: string) => {
|
||||
setPurchaseOrders(purchaseOrders.map(po => {
|
||||
if (po.id === poId) {
|
||||
const newStatus = po.status === 'Sent' ? 'Partially Received' : 'Received';
|
||||
return { ...po, status: newStatus as any };
|
||||
}
|
||||
return po;
|
||||
}));
|
||||
setExpandedMenu(null);
|
||||
};
|
||||
|
||||
const handleCancelPO = (poId: string) => {
|
||||
setPurchaseOrders(purchaseOrders.map(po =>
|
||||
po.id === poId ? { ...po, status: 'Closed' } : po
|
||||
));
|
||||
setExpandedMenu(null);
|
||||
};
|
||||
|
||||
const handleExportPOs = () => {
|
||||
const headers = ['PO #', 'Vendor', 'Description', 'Amount', 'Created', 'Expected Delivery', 'Status'];
|
||||
const rows = purchaseOrders.map(po => [
|
||||
po.id, po.vendor, po.description, po.amount, po.dateCreated, po.expectedDelivery, po.status
|
||||
]);
|
||||
const csv = [headers, ...rows].map(row => row.join(',')).join('\n');
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `purchase-orders-${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#111114] p-8 flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
||||
<p className="text-[#8B8B9E]">Loading procurement data...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#111114] p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-[#F0F0F3]">Procurement</h1>
|
||||
<p className="text-[#8B8B9E] mt-2">Manage purchase orders and vendor relationships</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => toast.success('New requisition form would open')}
|
||||
className="flex items-center gap-2 bg-[#1A1A1F] border border-[#3A3A45] hover:bg-[#111114] text-[#F0F0F3] px-4 py-2 rounded-lg font-medium transition"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
New Requisition
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowNewPOForm(true)}
|
||||
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Create PO
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* New PO Form Modal */}
|
||||
{showNewPOForm && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-[#1A1A1F] rounded-lg p-6 max-w-md w-full">
|
||||
<h3 className="text-lg font-bold mb-4">Create Purchase Order</h3>
|
||||
<form onSubmit={handleNewPO} className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Vendor Name"
|
||||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Description"
|
||||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg"
|
||||
required
|
||||
/>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewPOForm(false)}
|
||||
className="px-4 py-2 text-[#8B8B9E] border border-[#3A3A45] rounded-lg hover:bg-[#111114]"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{summaryCards.map((card, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${card.color} rounded-lg p-6 border border-[#2A2A32] shadow-sm`}
|
||||
title={card.title}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium opacity-75">{card.label}</p>
|
||||
<p className="text-3xl font-bold mt-2">{card.value}</p>
|
||||
</div>
|
||||
<div className="opacity-20">{card.icon}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="mb-6 border-b border-[#2A2A32] bg-[#1A1A1F] rounded-t-lg">
|
||||
<div className="flex">
|
||||
{(['po-list', 'matching'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-6 py-4 font-medium text-sm transition ${
|
||||
activeTab === tab
|
||||
? 'border-b-2 border-blue-600 text-blue-600'
|
||||
: 'text-[#8B8B9E] hover:text-[#F0F0F3]'
|
||||
}`}
|
||||
>
|
||||
{tab === 'po-list' ? 'Purchase Orders' : 'Three-Way Match'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PO List Table */}
|
||||
{activeTab === 'po-list' && (
|
||||
<div className="bg-[#1A1A1F] rounded-b-lg border border-[#2A2A32] border-t-0 shadow-sm mb-8">
|
||||
<div className="px-6 py-4 border-b border-[#2A2A32]">
|
||||
<button
|
||||
onClick={handleExportPOs}
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-700 transition"
|
||||
>
|
||||
Export as CSV
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-[#111114] border-b border-[#2A2A32]">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">PO #</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Vendor</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Description</th>
|
||||
<th className="px-6 py-3 text-right text-sm font-semibold text-[#F0F0F3]">Amount</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Created</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Expected Delivery</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Status</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{purchaseOrders.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={8} className="px-6 py-16 text-center">
|
||||
<div className="flex flex-col items-center gap-2 text-[#5A5A6E]">
|
||||
<ShoppingCart size={32} className="opacity-40" />
|
||||
<p className="text-sm font-medium text-[#8B8B9E]">No purchase orders yet</p>
|
||||
<p className="text-xs">Create a PO to get started.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{purchaseOrders.map((po, index) => (
|
||||
<tr
|
||||
key={po.id}
|
||||
className={`border-b border-[#2A2A32] hover:bg-[#111114] transition ${
|
||||
index % 2 === 0 ? 'bg-[#1A1A1F]' : 'bg-[#111114]'
|
||||
}`}
|
||||
>
|
||||
<td className="px-6 py-4 text-sm font-medium text-blue-600">{po.id}</td>
|
||||
<td className="px-6 py-4 text-sm text-[#F0F0F3]">{po.vendor}</td>
|
||||
<td className="px-6 py-4 text-sm text-[#8B8B9E]">{po.description}</td>
|
||||
<td className="px-6 py-4 text-sm text-[#F0F0F3] text-right font-medium">
|
||||
{formatCurrency(po.amount)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-[#8B8B9E]">
|
||||
{new Date(po.dateCreated).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-[#8B8B9E]">
|
||||
{new Date(po.expectedDelivery).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm">
|
||||
<span
|
||||
className={`inline-block px-3 py-1 rounded-full text-xs font-semibold ${getStatusBadgeColor(
|
||||
po.status
|
||||
)}`}
|
||||
>
|
||||
{po.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm relative">
|
||||
<button
|
||||
onClick={() => setExpandedMenu(expandedMenu === po.id ? null : po.id)}
|
||||
className="text-[#5A5A6E] hover:text-[#8B8B9E] p-1"
|
||||
>
|
||||
<MoreVertical size={16} />
|
||||
</button>
|
||||
{expandedMenu === po.id && (
|
||||
<div className="absolute right-6 top-full mt-1 bg-[#1A1A1F] border border-[#2A2A32] rounded-lg shadow-lg z-10 min-w-max">
|
||||
<button
|
||||
onClick={() => toast.success(`Viewing PO ${po.id}`)}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-[#8B8B9E] hover:bg-[#1A1A1F]"
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
{po.status !== 'Approved' && po.status !== 'Sent' && (
|
||||
<button
|
||||
onClick={() => handleApprovePO(po.id)}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-[#8B8B9E] hover:bg-[#1A1A1F]"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
)}
|
||||
{(po.status === 'Sent' || po.status === 'Partially Received') && (
|
||||
<button
|
||||
onClick={() => handleReceiveGoods(po.id)}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-[#8B8B9E] hover:bg-[#1A1A1F]"
|
||||
>
|
||||
Receive Goods
|
||||
</button>
|
||||
)}
|
||||
{po.status !== 'Closed' && po.status !== 'Received' && (
|
||||
<button
|
||||
onClick={() => handleCancelPO(po.id)}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-[#1A1A1F]"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Three-Way Match Table */}
|
||||
{activeTab === 'matching' && (
|
||||
<div className="bg-[#1A1A1F] rounded-b-lg border border-[#2A2A32] border-t-0 shadow-sm mb-8">
|
||||
<div className="px-6 py-4 border-b border-[#2A2A32]">
|
||||
<button
|
||||
onClick={handleExportPOs}
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-700 transition"
|
||||
>
|
||||
Export as CSV
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-[#111114] border-b border-[#2A2A32]">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">PO #</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Vendor</th>
|
||||
<th className="px-6 py-3 text-right text-sm font-semibold text-[#F0F0F3]">Amount</th>
|
||||
<th className="px-6 py-3 text-center text-sm font-semibold text-[#F0F0F3]">PO</th>
|
||||
<th className="px-6 py-3 text-center text-sm font-semibold text-[#F0F0F3]">Receipt</th>
|
||||
<th className="px-6 py-3 text-center text-sm font-semibold text-[#F0F0F3]">Invoice</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{purchaseOrders.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-6 py-16 text-center text-sm text-[#5A5A6E]">
|
||||
No POs to match.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{purchaseOrders.map((po, index) => (
|
||||
<tr
|
||||
key={po.id}
|
||||
className={`border-b border-[#2A2A32] hover:bg-[#111114] transition ${
|
||||
index % 2 === 0 ? 'bg-[#1A1A1F]' : 'bg-[#111114]'
|
||||
}`}
|
||||
>
|
||||
<td className="px-6 py-4 text-sm font-medium text-blue-600">{po.id}</td>
|
||||
<td className="px-6 py-4 text-sm text-[#F0F0F3]">{po.vendor}</td>
|
||||
<td className="px-6 py-4 text-sm text-[#F0F0F3] text-right font-medium">
|
||||
{formatCurrency(po.amount)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span
|
||||
className={`inline-block px-3 py-1 rounded text-xs font-semibold ${getMatchingStatusColor(
|
||||
po.poMatching?.po || false
|
||||
)}`}
|
||||
>
|
||||
{po.poMatching?.po ? '✓' : '—'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span
|
||||
className={`inline-block px-3 py-1 rounded text-xs font-semibold ${getMatchingStatusColor(
|
||||
po.poMatching?.receipt || false
|
||||
)}`}
|
||||
>
|
||||
{po.poMatching?.receipt ? '✓' : '—'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span
|
||||
className={`inline-block px-3 py-1 rounded text-xs font-semibold ${getMatchingStatusColor(
|
||||
po.poMatching?.invoice || false
|
||||
)}`}
|
||||
>
|
||||
{po.poMatching?.invoice ? '✓' : '—'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm">
|
||||
{po.poMatching?.po && po.poMatching?.receipt && po.poMatching?.invoice ? (
|
||||
<span className="inline-block px-3 py-1 rounded-full text-xs font-semibold bg-green-900/25 text-green-800">
|
||||
Complete Match
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-block px-3 py-1 rounded-full text-xs font-semibold bg-amber-900/25 text-yellow-800">
|
||||
Incomplete
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vendor Performance */}
|
||||
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] shadow-sm">
|
||||
<div className="px-6 py-4 border-b border-[#2A2A32]">
|
||||
<h2 className="text-lg font-bold text-[#F0F0F3]">Vendor Performance Metrics</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-[#111114] border-b border-[#2A2A32]">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Vendor</th>
|
||||
<th className="px-6 py-3 text-right text-sm font-semibold text-[#F0F0F3]">On-Time Delivery %</th>
|
||||
<th className="px-6 py-3 text-right text-sm font-semibold text-[#F0F0F3]">Quality Score %</th>
|
||||
<th className="px-6 py-3 text-right text-sm font-semibold text-[#F0F0F3]">Price Variance %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vendorMetrics.map((metric, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={`border-b border-[#2A2A32] ${index % 2 === 0 ? 'bg-[#1A1A1F]' : 'bg-[#111114]'}`}
|
||||
>
|
||||
<td className="px-6 py-4 text-sm font-medium text-[#F0F0F3]">{metric.vendor}</td>
|
||||
<td className="px-6 py-4 text-sm text-right font-medium">{metric.onTimeDelivery}%</td>
|
||||
<td className="px-6 py-4 text-sm text-right font-medium">{metric.quality}%</td>
|
||||
<td className="px-6 py-4 text-sm text-right font-medium">
|
||||
<span className={metric.priceVariance > 0 ? 'text-red-600' : 'text-green-600'}>
|
||||
{metric.priceVariance > 0 ? '+' : ''}{metric.priceVariance.toFixed(1)}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcurementPage;
|
||||
Reference in New Issue
Block a user