feat: Multi-role platform expansion, mobile nav redesign, and UI polish
- Add Owner, Contractor, Vendor, Subcontractor dashboards and role-based routing - Owner now has superuser access to all Admin pages (dashboard, schedule, leaderboard) - Redesign landing page mobile menu as slide-in sidebar (replaces broken Framer Motion overlay) - Add body scroll lock to app sidebar for mobile consistency - Fix Team Schedule to match Admin Dashboard design language (ambient glows, gradient header, SpotlightCard, zinc palette) - Fix login page tab overflow — use abbreviated labels and grid layout for 6 role tabs - Fix Recharts ResponsiveContainer warnings — replace height="100%" with fixed pixel heights - Fix lightbox image viewer — unified nav bar, touch swipe support, no more overlapping text - Add AI Assistant page, People/Vendor/Document management for Owner role - Expand mock data store with contractor, vendor, and subcontractor data
This commit is contained in:
+222
@@ -0,0 +1,222 @@
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X, Shield, FileText, CheckCircle, AlertCircle, Clock, Upload, Calendar } from 'lucide-react';
|
||||
|
||||
const ComplianceDetailsModal = ({ isOpen, onClose, vendorData }) => {
|
||||
// Close on Escape key
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen || !vendorData) return null;
|
||||
|
||||
const compliance = vendorData.compliance || {};
|
||||
|
||||
const getDocumentStatus = (doc) => {
|
||||
if (!doc || !doc.uploaded) return 'missing';
|
||||
if (doc.status === 'expired') return 'expired';
|
||||
if (doc.status === 'approved' || doc.status === 'compliant') return 'compliant';
|
||||
return 'pending';
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'compliant':
|
||||
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/20';
|
||||
case 'pending':
|
||||
return 'bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400 border-amber-200 dark:border-amber-500/20';
|
||||
case 'expired':
|
||||
case 'missing':
|
||||
return 'bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-400 border-red-200 dark:border-red-500/20';
|
||||
default:
|
||||
return 'bg-zinc-100 text-zinc-700 dark:bg-white/10 dark:text-zinc-400 border-zinc-200 dark:border-white/10';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'compliant':
|
||||
return <CheckCircle size={20} className="text-emerald-500" />;
|
||||
case 'pending':
|
||||
return <Clock size={20} className="text-amber-500" />;
|
||||
case 'expired':
|
||||
case 'missing':
|
||||
return <AlertCircle size={20} className="text-red-500" />;
|
||||
default:
|
||||
return <FileText size={20} className="text-zinc-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const documents = [
|
||||
{
|
||||
id: 'w9',
|
||||
name: 'W-9 Form',
|
||||
description: 'Tax identification form',
|
||||
data: compliance.w9,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
id: 'coi',
|
||||
name: 'Certificate of Insurance',
|
||||
description: 'General liability and workers comp',
|
||||
data: compliance.coi,
|
||||
required: true,
|
||||
expirationDate: compliance.coi?.expirationDate
|
||||
},
|
||||
{
|
||||
id: 'agreement',
|
||||
name: 'Subcontractor Agreement',
|
||||
description: 'Signed service agreement',
|
||||
data: compliance.subcontractorAgreement,
|
||||
required: true
|
||||
}
|
||||
];
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed top-0 left-0 w-screen h-[100dvh] z-[9999] flex items-end sm:items-center justify-center sm:p-6" role="dialog" aria-modal="true">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="relative w-full sm:max-w-3xl h-[85dvh] sm:h-auto sm:max-h-[85vh] bg-white dark:bg-[#121214] rounded-t-2xl sm:rounded-2xl shadow-2xl overflow-hidden flex flex-col animate-in slide-in-from-bottom-full sm:slide-in-from-bottom-10 sm:zoom-in-95 duration-300 sm:duration-200 border-t border-x sm:border border-zinc-200 dark:border-white/10">
|
||||
|
||||
{/* Mobile Drag Handle */}
|
||||
<div className="sm:hidden w-full flex justify-center pt-3 pb-1 bg-zinc-50 dark:bg-white/5 border-b-0 cursor-grab active:cursor-grabbing" onClick={onClose}>
|
||||
<div className="w-12 h-1.5 rounded-full bg-zinc-300 dark:bg-white/20" />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-4 sm:px-6 py-4 sm:py-5 border-b border-zinc-200 dark:border-white/10 bg-zinc-50/50 dark:bg-white/5">
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-lg sm:text-2xl font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<Shield className="text-blue-500 shrink-0" size={24} />
|
||||
<span className="truncate">Compliance Status</span>
|
||||
</h2>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 text-xs sm:text-sm mt-1">
|
||||
Document verification and compliance tracking
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:bg-zinc-200 dark:hover:bg-white/20 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar bg-white dark:bg-[#121214]">
|
||||
<div className="p-4 sm:p-6 space-y-6">
|
||||
|
||||
{/* Overall Status */}
|
||||
<div className={`p-4 rounded-xl border ${vendorData.status === 'active'
|
||||
? 'bg-emerald-50 dark:bg-emerald-500/5 border-emerald-200 dark:border-emerald-500/20'
|
||||
: 'bg-red-50 dark:bg-red-500/5 border-red-200 dark:border-red-500/20'
|
||||
}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
{vendorData.status === 'active' ? (
|
||||
<CheckCircle size={24} className="text-emerald-600 dark:text-emerald-400" />
|
||||
) : (
|
||||
<AlertCircle size={24} className="text-red-600 dark:text-red-400" />
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-bold text-zinc-900 dark:text-white">
|
||||
{vendorData.status === 'active' ? 'Compliance Verified' : 'Action Required'}
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300 mt-1">
|
||||
{vendorData.status === 'active'
|
||||
? 'All required documents are up to date'
|
||||
: 'Some documents need attention'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Documents List */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Required Documents</h3>
|
||||
|
||||
{documents.map((doc) => {
|
||||
const status = getDocumentStatus(doc.data);
|
||||
return (
|
||||
<div key={doc.id} className="p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 rounded-lg bg-white dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/10">
|
||||
{getStatusIcon(status)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-4 mb-2">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-zinc-900 dark:text-white">{doc.name}</h4>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">{doc.description}</p>
|
||||
</div>
|
||||
<span className={`px-2.5 py-0.5 rounded-full text-xs font-bold uppercase tracking-wide border whitespace-nowrap ${getStatusColor(status)}`}>
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{doc.expirationDate && (
|
||||
<div className="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-300 mt-2">
|
||||
<Calendar size={14} />
|
||||
<span>Expires: {doc.expirationDate}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'missing' && (
|
||||
<button className="mt-3 flex items-center gap-2 px-3 py-1.5 rounded-lg bg-blue-600 text-white text-xs font-medium hover:bg-blue-500 transition-colors">
|
||||
<Upload size={14} />
|
||||
Upload Document
|
||||
</button>
|
||||
)}
|
||||
|
||||
{status === 'expired' && (
|
||||
<button className="mt-3 flex items-center gap-2 px-3 py-1.5 rounded-lg bg-amber-600 text-white text-xs font-medium hover:bg-amber-500 transition-colors">
|
||||
<Upload size={14} />
|
||||
Upload Renewal
|
||||
</button>
|
||||
)}
|
||||
|
||||
{status === 'compliant' && doc.data?.uploaded && (
|
||||
<div className="mt-3 flex items-center gap-2 text-xs text-emerald-600 dark:text-emerald-400">
|
||||
<CheckCircle size={14} />
|
||||
<span>Verified and approved</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Upload Center */}
|
||||
<div className="p-6 rounded-xl border-2 border-dashed border-zinc-200 dark:border-white/10 bg-zinc-50/50 dark:bg-white/5 text-center">
|
||||
<Upload size={32} className="mx-auto text-zinc-400 mb-3" />
|
||||
<h4 className="font-semibold text-zinc-900 dark:text-white mb-1">Upload Documents</h4>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mb-4">
|
||||
Drag and drop files here or click to browse
|
||||
</p>
|
||||
<button className="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-500 transition-colors">
|
||||
Choose Files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default ComplianceDetailsModal;
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X, Package, MapPin, Calendar, DollarSign, FileText, MessageSquare, Truck, CheckCircle, Clock, AlertCircle, Phone, User } from 'lucide-react';
|
||||
|
||||
const OrderDetailsModal = ({ isOpen, onClose, order }) => {
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
|
||||
// Close on Escape key
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen || !order) return null;
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'delivered':
|
||||
case 'completed':
|
||||
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/20';
|
||||
case 'shipped':
|
||||
return 'bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400 border-blue-200 dark:border-blue-500/20';
|
||||
case 'confirmed':
|
||||
return 'bg-purple-100 text-purple-700 dark:bg-purple-500/10 dark:text-purple-400 border-purple-200 dark:border-purple-500/20';
|
||||
case 'pending':
|
||||
return 'bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400 border-amber-200 dark:border-amber-500/20';
|
||||
default:
|
||||
return 'bg-zinc-100 text-zinc-700 dark:bg-white/10 dark:text-zinc-400 border-zinc-200 dark:border-white/10';
|
||||
}
|
||||
};
|
||||
|
||||
const getTimelineIcon = (status) => {
|
||||
if (status === 'completed') return <CheckCircle size={20} className="text-emerald-500" />;
|
||||
if (status === 'pending') return <Clock size={20} className="text-zinc-400" />;
|
||||
return <AlertCircle size={20} className="text-blue-500" />;
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: 'Overview', icon: Package },
|
||||
{ id: 'timeline', label: 'Timeline', icon: Truck },
|
||||
{ id: 'documents', label: 'Documents', icon: FileText },
|
||||
{ id: 'communication', label: 'Communication', icon: MessageSquare }
|
||||
];
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed top-0 left-0 w-screen h-[100dvh] z-[9999] flex items-end sm:items-center justify-center sm:p-6" role="dialog" aria-modal="true" aria-labelledby="order-modal-title">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="relative w-full sm:max-w-5xl h-[85dvh] sm:h-auto sm:max-h-[85vh] bg-white dark:bg-[#121214] rounded-t-2xl sm:rounded-2xl shadow-2xl overflow-hidden flex flex-col animate-in slide-in-from-bottom-full sm:slide-in-from-bottom-10 sm:zoom-in-95 duration-300 sm:duration-200 border-t border-x sm:border border-zinc-200 dark:border-white/10">
|
||||
|
||||
{/* Mobile Drag Handle */}
|
||||
<div className="sm:hidden w-full flex justify-center pt-3 pb-1 bg-zinc-50 dark:bg-white/5 border-b-0 cursor-grab active:cursor-grabbing" onClick={onClose}>
|
||||
<div className="w-12 h-1.5 rounded-full bg-zinc-300 dark:bg-white/20" />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-4 sm:px-6 py-4 sm:py-5 border-b border-zinc-200 dark:border-white/10 bg-zinc-50/50 dark:bg-white/5">
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 id="order-modal-title" className="text-lg sm:text-2xl font-bold text-zinc-900 dark:text-white flex items-center gap-2 mb-2">
|
||||
<Package className="text-blue-500 shrink-0" size={24} />
|
||||
<span className="truncate">Order {order.id}</span>
|
||||
</h2>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs sm:text-sm text-zinc-500 dark:text-zinc-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<MapPin size={14} />
|
||||
<span className="truncate">{order.projectAddress || order.project}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar size={14} />
|
||||
<span>Due: {order.dueDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<span className={`px-2.5 sm:px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide border ${getStatusColor(order.status)}`}>
|
||||
{order.status}
|
||||
</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:bg-zinc-200 dark:hover:bg-white/20 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-zinc-200 dark:border-white/10 bg-white dark:bg-[#121214]">
|
||||
<div className="flex overflow-x-auto hide-scrollbar px-4 sm:px-6">
|
||||
{tabs.map(tab => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex items-center gap-2 px-4 py-3 text-sm font-bold whitespace-nowrap border-b-2 transition-colors ${activeTab === tab.id
|
||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-zinc-500 hover:text-zinc-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Icon size={16} />
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar bg-white dark:bg-[#121214]">
|
||||
<div className="p-4 sm:p-6 space-y-6">
|
||||
|
||||
{/* Overview Tab */}
|
||||
{activeTab === 'overview' && (
|
||||
<>
|
||||
{/* Order Summary */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="p-4 rounded-xl bg-blue-50 dark:bg-blue-500/5 border border-blue-100 dark:border-blue-500/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<DollarSign size={18} className="text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-xs font-bold uppercase text-blue-600 dark:text-blue-400">Total Amount</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-zinc-900 dark:text-white">{formatCurrency(order.total || order.amount)}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-purple-50 dark:bg-purple-500/5 border border-purple-100 dark:border-purple-500/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Calendar size={18} className="text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-xs font-bold uppercase text-purple-600 dark:text-purple-400">Order Date</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-zinc-900 dark:text-white">{order.orderDate || 'N/A'}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-100 dark:border-emerald-500/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Truck size={18} className="text-emerald-600 dark:text-emerald-400" />
|
||||
<span className="text-xs font-bold uppercase text-emerald-600 dark:text-emerald-400">Delivery Date</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-zinc-900 dark:text-white">{order.deliveryDate || 'Pending'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Items List */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Order Items</h3>
|
||||
<div className="space-y-2">
|
||||
{order.items ? order.items.map((item, idx) => (
|
||||
<div key={idx} className="p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5">
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-zinc-900 dark:text-white">{item.description}</h4>
|
||||
{item.specifications && (
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1">{item.specifications}</p>
|
||||
)}
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300 mt-2">
|
||||
Quantity: <span className="font-bold">{item.quantity} {item.unit}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-zinc-500">Unit Price</p>
|
||||
<p className="font-mono font-medium text-zinc-900 dark:text-white">{formatCurrency(item.unitPrice)}</p>
|
||||
<p className="text-xs text-zinc-500 mt-2">Total</p>
|
||||
<p className="text-lg font-bold text-zinc-900 dark:text-white">{formatCurrency(item.total)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5">
|
||||
<p className="font-semibold text-zinc-900 dark:text-white">{order.item}</p>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300 mt-1">
|
||||
Amount: <span className="font-bold">{formatCurrency(order.amount)}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delivery Information */}
|
||||
{order.deliveryAddress && (
|
||||
<div className="p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5">
|
||||
<h3 className="text-sm font-bold text-zinc-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<MapPin size={16} className="text-blue-500" />
|
||||
Delivery Address
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300">
|
||||
{order.deliveryAddress.street}<br />
|
||||
{order.deliveryAddress.city}, {order.deliveryAddress.state} {order.deliveryAddress.zip}
|
||||
</p>
|
||||
{order.deliveryAddress.instructions && (
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-2 italic">
|
||||
Note: {order.deliveryAddress.instructions}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contact Information */}
|
||||
{(order.contactPerson || order.contactPhone) && (
|
||||
<div className="p-4 rounded-xl bg-blue-50 dark:bg-blue-500/5 border border-blue-100 dark:border-blue-500/20">
|
||||
<h3 className="text-sm font-bold text-zinc-900 dark:text-white mb-3">Project Contact</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
{order.contactPerson && (
|
||||
<div className="flex items-center gap-2 text-zinc-600 dark:text-zinc-300">
|
||||
<User size={14} />
|
||||
{order.contactPerson}
|
||||
</div>
|
||||
)}
|
||||
{order.contactPhone && (
|
||||
<div className="flex items-center gap-2 text-zinc-600 dark:text-zinc-300">
|
||||
<Phone size={14} />
|
||||
{order.contactPhone}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Timeline Tab */}
|
||||
{activeTab === 'timeline' && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Order Timeline</h3>
|
||||
{order.timeline ? (
|
||||
<div className="relative space-y-6 pl-8">
|
||||
{/* Timeline line */}
|
||||
<div className="absolute left-2.5 top-2 bottom-2 w-0.5 bg-zinc-200 dark:bg-white/10" />
|
||||
|
||||
{order.timeline.map((event, idx) => (
|
||||
<div key={idx} className="relative">
|
||||
<div className="absolute -left-8 top-0">
|
||||
{getTimelineIcon(event.status)}
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5">
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div>
|
||||
<h4 className="font-bold text-zinc-900 dark:text-white">{event.event}</h4>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">{event.date}</p>
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-bold uppercase ${event.status === 'completed' ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400' :
|
||||
'bg-zinc-100 text-zinc-600 dark:bg-white/10 dark:text-zinc-400'
|
||||
}`}>
|
||||
{event.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-10 text-zinc-500">
|
||||
<Clock size={48} className="mx-auto mb-4 opacity-20" />
|
||||
<p>No timeline information available</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Documents Tab */}
|
||||
{activeTab === 'documents' && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Order Documents</h3>
|
||||
{order.documents && order.documents.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{order.documents.map((doc, idx) => (
|
||||
<div key={idx} className="p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5 hover:border-blue-500/30 transition-colors cursor-pointer group">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400">
|
||||
<FileText size={20} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-zinc-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors truncate">
|
||||
{doc.name}
|
||||
</h4>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1">{doc.type}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-10 text-zinc-500">
|
||||
<FileText size={48} className="mx-auto mb-4 opacity-20" />
|
||||
<p>No documents available</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Communication Tab */}
|
||||
{activeTab === 'communication' && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Communication Log</h3>
|
||||
<div className="text-center py-10 text-zinc-500">
|
||||
<MessageSquare size={48} className="mx-auto mb-4 opacity-20" />
|
||||
<p>No messages yet</p>
|
||||
<button className="mt-4 px-4 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-500 transition-colors">
|
||||
Send Message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{order.notes && (
|
||||
<div className="px-4 sm:px-6 py-3 sm:py-4 border-t border-zinc-200 dark:border-white/10 bg-zinc-50 dark:bg-white/5">
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<span className="font-bold">Note:</span> {order.notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderDetailsModal;
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X, Award, TrendingUp, CheckCircle, Clock, Star, BarChart3 } from 'lucide-react';
|
||||
|
||||
const PerformanceMetricsModal = ({ isOpen, onClose, vendorData }) => {
|
||||
// Close on Escape key
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen || !vendorData) return null;
|
||||
|
||||
const performance = vendorData.performance || {};
|
||||
const rating = performance.rating || 0;
|
||||
const onTimeRate = (performance.onTimeRate || 0) * 100;
|
||||
const jobsCompleted = performance.jobsCompleted || 0;
|
||||
const activeJobs = performance.activeJobs || 0;
|
||||
|
||||
const getRatingColor = (rating) => {
|
||||
if (rating >= 4.5) return 'text-emerald-500';
|
||||
if (rating >= 3.5) return 'text-blue-500';
|
||||
if (rating >= 2.5) return 'text-amber-500';
|
||||
return 'text-red-500';
|
||||
};
|
||||
|
||||
const metrics = [
|
||||
{
|
||||
label: 'Overall Rating',
|
||||
value: `${rating.toFixed(1)}/5.0`,
|
||||
icon: Star,
|
||||
color: 'emerald',
|
||||
description: 'Average customer satisfaction score'
|
||||
},
|
||||
{
|
||||
label: 'On-Time Delivery',
|
||||
value: `${onTimeRate.toFixed(0)}%`,
|
||||
icon: Clock,
|
||||
color: 'blue',
|
||||
description: 'Percentage of orders delivered on schedule'
|
||||
},
|
||||
{
|
||||
label: 'Jobs Completed',
|
||||
value: jobsCompleted,
|
||||
icon: CheckCircle,
|
||||
color: 'purple',
|
||||
description: 'Total number of completed deliveries'
|
||||
},
|
||||
{
|
||||
label: 'Active Jobs',
|
||||
value: activeJobs,
|
||||
icon: TrendingUp,
|
||||
color: 'amber',
|
||||
description: 'Current ongoing projects'
|
||||
}
|
||||
];
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed top-0 left-0 w-screen h-[100dvh] z-[9999] flex items-end sm:items-center justify-center sm:p-6" role="dialog" aria-modal="true">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="relative w-full sm:max-w-4xl h-[85dvh] sm:h-auto sm:max-h-[85vh] bg-white dark:bg-[#121214] rounded-t-2xl sm:rounded-2xl shadow-2xl overflow-hidden flex flex-col animate-in slide-in-from-bottom-full sm:slide-in-from-bottom-10 sm:zoom-in-95 duration-300 sm:duration-200 border-t border-x sm:border border-zinc-200 dark:border-white/10">
|
||||
|
||||
{/* Mobile Drag Handle */}
|
||||
<div className="sm:hidden w-full flex justify-center pt-3 pb-1 bg-zinc-50 dark:bg-white/5 border-b-0 cursor-grab active:cursor-grabbing" onClick={onClose}>
|
||||
<div className="w-12 h-1.5 rounded-full bg-zinc-300 dark:bg-white/20" />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-4 sm:px-6 py-4 sm:py-5 border-b border-zinc-200 dark:border-white/10 bg-zinc-50/50 dark:bg-white/5">
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-lg sm:text-2xl font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<Award className="text-purple-500 shrink-0" size={24} />
|
||||
<span className="truncate">Performance Metrics</span>
|
||||
</h2>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 text-xs sm:text-sm mt-1">
|
||||
{vendorData.vendorName} - Performance Analytics
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:bg-zinc-200 dark:hover:bg-white/20 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar bg-white dark:bg-[#121214]">
|
||||
<div className="p-4 sm:p-6 space-y-6">
|
||||
|
||||
{/* Overall Score */}
|
||||
<div className="p-6 rounded-xl bg-gradient-to-br from-purple-50 to-blue-50 dark:from-purple-500/10 dark:to-blue-500/10 border border-purple-100 dark:border-purple-500/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold uppercase tracking-wide text-purple-600 dark:text-purple-400 mb-2">
|
||||
Overall Performance Score
|
||||
</h3>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className={`text-5xl font-bold ${getRatingColor(rating)}`}>
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<span className="text-2xl text-zinc-400">/5.0</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-2">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Star
|
||||
key={star}
|
||||
size={20}
|
||||
className={star <= rating ? 'fill-amber-400 text-amber-400' : 'text-zinc-300 dark:text-zinc-600'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<BarChart3 size={64} className="text-purple-200 dark:text-purple-500/20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Key Metrics Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{metrics.map((metric) => {
|
||||
const Icon = metric.icon;
|
||||
const colorClasses = {
|
||||
emerald: 'bg-emerald-50 dark:bg-emerald-500/5 border-emerald-100 dark:border-emerald-500/20 text-emerald-600 dark:text-emerald-400',
|
||||
blue: 'bg-blue-50 dark:bg-blue-500/5 border-blue-100 dark:border-blue-500/20 text-blue-600 dark:text-blue-400',
|
||||
purple: 'bg-purple-50 dark:bg-purple-500/5 border-purple-100 dark:border-purple-500/20 text-purple-600 dark:text-purple-400',
|
||||
amber: 'bg-amber-50 dark:bg-amber-500/5 border-amber-100 dark:border-amber-500/20 text-amber-600 dark:text-amber-400'
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={metric.label} className={`p-4 rounded-xl border ${colorClasses[metric.color]}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`p-2 rounded-lg bg-white dark:bg-zinc-900/50`}>
|
||||
<Icon size={20} className={colorClasses[metric.color].split(' ').slice(-2).join(' ')} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-bold uppercase tracking-wide opacity-80">
|
||||
{metric.label}
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-zinc-900 dark:text-white mt-1">
|
||||
{metric.value}
|
||||
</p>
|
||||
<p className="text-xs text-zinc-600 dark:text-zinc-400 mt-1">
|
||||
{metric.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Performance Breakdown */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Performance Breakdown</h3>
|
||||
|
||||
{/* Quality */}
|
||||
<div className="p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-semibold text-zinc-900 dark:text-white">Quality of Work</span>
|
||||
<span className="text-sm font-bold text-emerald-600 dark:text-emerald-400">Excellent</span>
|
||||
</div>
|
||||
<div className="h-2 bg-zinc-200 dark:bg-zinc-700/50 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-gradient-to-r from-emerald-500 to-emerald-400 rounded-full" style={{ width: '95%' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Communication */}
|
||||
<div className="p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-semibold text-zinc-900 dark:text-white">Communication</span>
|
||||
<span className="text-sm font-bold text-blue-600 dark:text-blue-400">Very Good</span>
|
||||
</div>
|
||||
<div className="h-2 bg-zinc-200 dark:bg-zinc-700/50 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-gradient-to-r from-blue-500 to-blue-400 rounded-full" style={{ width: '88%' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reliability */}
|
||||
<div className="p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-semibold text-zinc-900 dark:text-white">Reliability</span>
|
||||
<span className="text-sm font-bold text-emerald-600 dark:text-emerald-400">Excellent</span>
|
||||
</div>
|
||||
<div className="h-2 bg-zinc-200 dark:bg-zinc-700/50 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-gradient-to-r from-emerald-500 to-emerald-400 rounded-full" style={{ width: '92%' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Feedback */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Recent Feedback</h3>
|
||||
<div className="p-4 rounded-xl bg-blue-50 dark:bg-blue-500/5 border border-blue-100 dark:border-blue-500/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<Star key={star} size={14} className="fill-amber-400 text-amber-400" />
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-zinc-700 dark:text-zinc-300 italic">
|
||||
"Excellent service and timely delivery. Materials were exactly as specified."
|
||||
</p>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-2">
|
||||
- Project Manager, 2604 Dunwick Dr
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default PerformanceMetricsModal;
|
||||
@@ -0,0 +1,220 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X, DollarSign, TrendingUp, Clock, CheckCircle, Download, Search, Filter } from 'lucide-react';
|
||||
|
||||
const VendorFinancialSummaryModal = ({ isOpen, onClose, data }) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortConfig, setSortConfig] = useState({ key: 'date', direction: 'desc' });
|
||||
|
||||
// Close on Escape key
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen || !data) return null;
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
|
||||
};
|
||||
|
||||
const handleSort = (key) => {
|
||||
let direction = 'asc';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc';
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
// Filter and sort data
|
||||
const filteredData = (data.invoices || []).filter(item =>
|
||||
item.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.project?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.id?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const sortedData = [...filteredData].sort((a, b) => {
|
||||
if (a[sortConfig.key] < b[sortConfig.key]) {
|
||||
return sortConfig.direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (a[sortConfig.key] > b[sortConfig.key]) {
|
||||
return sortConfig.direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed top-0 left-0 w-screen h-[100dvh] z-[9999] flex items-end sm:items-center justify-center sm:p-6" role="dialog" aria-modal="true">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="relative w-full sm:max-w-5xl h-[85dvh] sm:h-auto sm:max-h-[85vh] bg-white dark:bg-[#121214] rounded-t-2xl sm:rounded-2xl shadow-2xl overflow-hidden flex flex-col animate-in slide-in-from-bottom-full sm:slide-in-from-bottom-10 sm:zoom-in-95 duration-300 sm:duration-200 border-t border-x sm:border border-zinc-200 dark:border-white/10">
|
||||
|
||||
{/* Mobile Drag Handle */}
|
||||
<div className="sm:hidden w-full flex justify-center pt-3 pb-1 bg-zinc-50 dark:bg-white/5 border-b-0 cursor-grab active:cursor-grabbing" onClick={onClose}>
|
||||
<div className="w-12 h-1.5 rounded-full bg-zinc-300 dark:bg-white/20" />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-4 sm:px-6 py-4 sm:py-5 border-b border-zinc-200 dark:border-white/10 flex justify-between items-start bg-zinc-50/50 dark:bg-white/5">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-lg sm:text-2xl font-bold text-zinc-900 dark:text-white flex items-center gap-2">
|
||||
<DollarSign className="text-emerald-500 shrink-0" size={20} />
|
||||
<span className="truncate">Earnings Summary</span>
|
||||
</h2>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 text-xs sm:text-sm mt-1">
|
||||
Total Earned (YTD):
|
||||
<span className="font-mono font-bold text-zinc-900 dark:text-white ml-1">{formatCurrency(data.totalEarnings || 0)}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:gap-3 ml-2">
|
||||
<button className="hidden sm:block p-2 text-zinc-400 hover:text-zinc-900 dark:hover:text-white transition-colors" title="Export CSV">
|
||||
<Download size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:bg-zinc-200 dark:hover:bg-white/20 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="px-4 sm:px-6 py-3 sm:py-4 border-b border-zinc-200 dark:border-white/10 bg-white dark:bg-[#121214]">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div className="p-3 sm:p-4 rounded-xl bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-100 dark:border-emerald-500/20">
|
||||
<div className="flex items-center gap-2 mb-1 sm:mb-2">
|
||||
<CheckCircle size={16} className="text-emerald-600 dark:text-emerald-400" />
|
||||
<span className="text-[10px] sm:text-xs font-bold uppercase text-emerald-600 dark:text-emerald-400">
|
||||
Paid Invoices
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xl sm:text-2xl font-bold text-zinc-900 dark:text-white">{formatCurrency(data.paidInvoices || 0)}</p>
|
||||
</div>
|
||||
<div className="p-3 sm:p-4 rounded-xl bg-amber-50 dark:bg-amber-500/5 border border-amber-100 dark:border-amber-500/20">
|
||||
<div className="flex items-center gap-2 mb-1 sm:mb-2">
|
||||
<Clock size={16} className="text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-[10px] sm:text-xs font-bold uppercase text-amber-600 dark:text-amber-400">
|
||||
Pending Payments
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xl sm:text-2xl font-bold text-zinc-900 dark:text-white">{formatCurrency(data.pendingPayments || 0)}</p>
|
||||
</div>
|
||||
<div className="p-3 sm:p-4 rounded-xl bg-blue-50 dark:bg-blue-500/5 border border-blue-100 dark:border-blue-500/20">
|
||||
<div className="flex items-center gap-2 mb-1 sm:mb-2">
|
||||
<TrendingUp size={16} className="text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-[10px] sm:text-xs font-bold uppercase text-blue-600 dark:text-blue-400">
|
||||
Total Earned (YTD)
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xl sm:text-2xl font-bold text-zinc-900 dark:text-white">{formatCurrency(data.totalEarnings || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="px-4 sm:px-6 py-3 sm:py-4 border-b border-zinc-200 dark:border-white/5 flex flex-col sm:flex-row gap-3 sm:gap-4 justify-between bg-white dark:bg-[#121214]">
|
||||
<div className="relative w-full sm:w-96">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={16} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search invoices..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-9 pr-4 py-2 rounded-xl bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-sm text-zinc-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
/>
|
||||
</div>
|
||||
<button className="hidden sm:flex items-center gap-2 px-4 py-2 rounded-xl border border-zinc-200 dark:border-white/10 text-sm font-medium hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors">
|
||||
<Filter size={16} /> Filter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="flex-1 overflow-y-auto overflow-x-auto custom-scrollbar bg-white dark:bg-[#121214]">
|
||||
<table className="w-full text-left border-collapse min-w-[600px]">
|
||||
<thead className="sticky top-0 z-10 bg-zinc-50 dark:bg-[#18181b] border-b border-zinc-200 dark:border-white/10 shadow-sm">
|
||||
<tr>
|
||||
{[
|
||||
{ key: 'id', label: 'Invoice #' },
|
||||
{ key: 'project', label: 'Project' },
|
||||
{ key: 'invoiceDate', label: 'Date' },
|
||||
{ key: 'dueDate', label: 'Due Date' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'amount', label: 'Amount', align: 'right' }
|
||||
].map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
onClick={() => handleSort(col.key)}
|
||||
className={`px-3 sm:px-6 py-3 sm:py-4 text-[10px] sm:text-xs font-bold uppercase tracking-wider text-zinc-500 cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors ${col.align === 'right' ? 'text-right' : ''}`}
|
||||
>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100 dark:divide-white/5">
|
||||
{sortedData.length > 0 ? sortedData.map((invoice, idx) => (
|
||||
<tr key={idx} className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors">
|
||||
<td className="px-3 sm:px-6 py-3 sm:py-4 text-xs sm:text-sm text-zinc-600 dark:text-zinc-400 font-mono whitespace-nowrap">
|
||||
{invoice.id}
|
||||
</td>
|
||||
<td className="px-3 sm:px-6 py-3 sm:py-4">
|
||||
<div className="font-semibold text-sm sm:text-base text-zinc-900 dark:text-white">{invoice.project}</div>
|
||||
{invoice.description && <div className="text-xs text-zinc-500">{invoice.description}</div>}
|
||||
</td>
|
||||
<td className="px-3 sm:px-6 py-3 sm:py-4 text-xs sm:text-sm text-zinc-600 dark:text-zinc-400 font-mono whitespace-nowrap">
|
||||
{invoice.invoiceDate}
|
||||
</td>
|
||||
<td className="px-3 sm:px-6 py-3 sm:py-4 text-xs sm:text-sm text-zinc-600 dark:text-zinc-400 font-mono whitespace-nowrap">
|
||||
{invoice.dueDate}
|
||||
</td>
|
||||
<td className="px-3 sm:px-6 py-3 sm:py-4">
|
||||
<span className={`px-2 sm:px-2.5 py-0.5 rounded-full text-[10px] sm:text-xs font-bold uppercase tracking-wide whitespace-nowrap ${invoice.status === 'paid'
|
||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400'
|
||||
: invoice.status === 'pending'
|
||||
? 'bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400'
|
||||
: 'bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-400'
|
||||
}`}>
|
||||
{invoice.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 sm:px-6 py-3 sm:py-4 text-right font-mono font-medium text-sm sm:text-base text-zinc-900 dark:text-white whitespace-nowrap">
|
||||
{formatCurrency(invoice.amount)}
|
||||
</td>
|
||||
</tr>
|
||||
)) : (
|
||||
<tr>
|
||||
<td colSpan="6" className="py-20 text-center text-zinc-500">
|
||||
No invoices found.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-4 sm:px-6 py-3 sm:py-4 border-t border-zinc-200 dark:border-white/10 bg-zinc-50 dark:bg-white/5 flex justify-between items-center text-xs sm:text-sm">
|
||||
<span className="text-zinc-500 dark:text-zinc-400">Showing {sortedData.length} invoices</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-zinc-500 dark:text-zinc-400">Total:</span>
|
||||
<span className="text-base sm:text-lg font-bold text-zinc-900 dark:text-white">{formatCurrency(data.totalEarnings || 0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default VendorFinancialSummaryModal;
|
||||
Reference in New Issue
Block a user