2eaac6b84a
- 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
149 lines
8.4 KiB
React
149 lines
8.4 KiB
React
import React, { useState } from 'react';
|
|
import { useMockStore } from '../../data/mockStore';
|
|
import { useAuth } from '../../context/AuthContext';
|
|
import { SpotlightCard } from '../../components/SpotlightCard';
|
|
import { Package, FileText, Filter, Search, ChevronRight, CheckCircle, Clock, Truck } from 'lucide-react';
|
|
import OrderDetailsModal from '../../components/vendor/OrderDetailsModal';
|
|
|
|
const VendorOrders = () => {
|
|
const { user } = useAuth();
|
|
const { projects, orders } = useMockStore();
|
|
const [filter, setFilter] = useState('all');
|
|
const [selectedOrder, setSelectedOrder] = useState(null);
|
|
const [isOrderModalOpen, setIsOrderModalOpen] = useState(false);
|
|
|
|
// Mock ID logic
|
|
const vendorId = user.id === 'ven_001' ? 'v3' : 'v1';
|
|
|
|
// Get vendor orders from MOCK_ORDERS
|
|
const vendorOrders = orders.filter(o => o.vendorId === vendorId);
|
|
const filteredOrders = filter === 'all' ? vendorOrders : vendorOrders.filter(o => o.status === filter);
|
|
|
|
const handleOrderClick = (order) => {
|
|
setSelectedOrder(order);
|
|
setIsOrderModalOpen(true);
|
|
};
|
|
|
|
const getStatusColor = (status) => {
|
|
switch (status) {
|
|
case 'delivered': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400';
|
|
case 'shipped': return 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400';
|
|
default: return 'bg-zinc-100 text-zinc-700 dark:bg-white/10 dark:text-zinc-400';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="p-6 md:p-10 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500">
|
|
<header className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">My Orders</h1>
|
|
<p className="text-zinc-500 dark:text-zinc-400 mt-1">Manage purchase orders and deliveries.</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={16} />
|
|
<input
|
|
type="text"
|
|
placeholder="Search orders..."
|
|
className="pl-9 pr-4 py-2 rounded-lg bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
|
/>
|
|
</div>
|
|
<button className="flex items-center gap-2 px-3 py-2 rounded-lg bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-sm font-medium hover:bg-zinc-200 dark:hover:bg-white/10 transition-colors">
|
|
<Filter size={16} /> Filter
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Status Tabs */}
|
|
<div className="flex gap-2 overflow-x-auto pb-2">
|
|
{['all', 'pending', 'confirmed', 'shipped', 'delivered'].map(tab => (
|
|
<button
|
|
key={tab}
|
|
onClick={() => setFilter(tab)}
|
|
className={`px-4 py-2 rounded-full text-sm font-bold uppercase tracking-wide whitespace-nowrap transition-colors ${filter === tab
|
|
? 'bg-zinc-900 text-white dark:bg-white dark:text-zinc-900'
|
|
: 'bg-zinc-100 text-zinc-500 hover:bg-zinc-200 dark:bg-white/5 dark:text-zinc-400 dark:hover:bg-white/10'
|
|
}`}
|
|
>
|
|
{tab}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<SpotlightCard className="overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr className="border-b border-zinc-200 dark:border-white/10 text-xs font-bold uppercase tracking-wider text-zinc-500 bg-zinc-50/50 dark:bg-white/5">
|
|
<th className="p-4">Order ID</th>
|
|
<th className="p-4">Project / Location</th>
|
|
<th className="p-4">Items / Service</th>
|
|
<th className="p-4">Due Date</th>
|
|
<th className="p-4">Amount</th>
|
|
<th className="p-4">Status</th>
|
|
<th className="p-4">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-zinc-100 dark:divide-white/5">
|
|
{filteredOrders.map(order => (
|
|
<tr
|
|
key={order.id}
|
|
onClick={() => handleOrderClick(order)}
|
|
className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors group cursor-pointer"
|
|
>
|
|
<td className="p-4 font-mono text-sm text-zinc-500">{order.id}</td>
|
|
<td className="p-4 text-sm font-medium text-zinc-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
|
{order.projectAddress || order.project}
|
|
</td>
|
|
<td className="p-4 text-sm text-zinc-600 dark:text-zinc-300">
|
|
<div className="flex items-center gap-2">
|
|
<Package size={16} className="text-zinc-400" />
|
|
{order.items ? `${order.items.length} items` : order.item}
|
|
</div>
|
|
</td>
|
|
<td className="p-4 text-sm text-zinc-500">{order.dueDate}</td>
|
|
<td className="p-4 text-sm font-medium text-zinc-900 dark:text-white">${(order.total || order.amount).toLocaleString()}</td>
|
|
<td className="p-4">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold uppercase tracking-wide ${getStatusColor(order.status)}`}>
|
|
{order.status === 'delivered' && <CheckCircle size={12} className="mr-1.5" />}
|
|
{order.status === 'shipped' && <Truck size={12} className="mr-1.5" />}
|
|
{(order.status === 'pending' || order.status === 'confirmed') && <Clock size={12} className="mr-1.5" />}
|
|
{order.status}
|
|
</span>
|
|
</td>
|
|
<td className="p-4">
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleOrderClick(order);
|
|
}}
|
|
className="p-2 rounded-lg hover:bg-zinc-200 dark:hover:bg-white/10 text-zinc-400 hover:text-zinc-900 dark:hover:text-white transition-colors"
|
|
>
|
|
<ChevronRight size={18} />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{filteredOrders.length === 0 && (
|
|
<div className="p-12 text-center text-zinc-500">
|
|
<Package size={48} className="mx-auto mb-4 opacity-20" />
|
|
<p>No orders found matching this filter.</p>
|
|
</div>
|
|
)}
|
|
</SpotlightCard>
|
|
|
|
{/* Order Details Modal */}
|
|
<OrderDetailsModal
|
|
isOpen={isOrderModalOpen}
|
|
onClose={() => setIsOrderModalOpen(false)}
|
|
order={selectedOrder}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default VendorOrders;
|