feat: Implement Order List View for Vendors
This commit is contained in:
@@ -70,7 +70,7 @@ export default function ClientOrderList() {
|
||||
const lower = searchTerm.toLowerCase();
|
||||
filtered = filtered.filter(o =>
|
||||
o.eventName?.toLowerCase().includes(lower) ||
|
||||
o.teamHub?.hubName?.toLowerCase().includes(lower)
|
||||
o.business.businessName.toLowerCase().includes(lower)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function ClientOrderList() {
|
||||
}
|
||||
|
||||
if (locationFilter !== "all") {
|
||||
filtered = filtered.filter(o => o.teamHub?.hubName === locationFilter);
|
||||
filtered = filtered.filter(o => o.business.businessName === locationFilter);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
@@ -88,7 +88,8 @@ export default function ClientOrderList() {
|
||||
const uniqueLocations = useMemo(() => {
|
||||
const locations = new Set<string>();
|
||||
orders.forEach(o => {
|
||||
if (o.teamHub?.hubName) locations.add(o.teamHub.hubName);
|
||||
const businessName = o.business.businessName;
|
||||
if (businessName) locations.add(businessName);
|
||||
});
|
||||
return Array.from(locations).sort();
|
||||
}, [orders]);
|
||||
@@ -274,7 +275,7 @@ export default function ClientOrderList() {
|
||||
{order.eventName}
|
||||
<div className="flex items-center gap-1 text-[10px] text-muted-foreground mt-0.5">
|
||||
<MapPin className="w-3 h-3" />
|
||||
{order.teamHub?.hubName || "No location"}
|
||||
{order.business.businessName || "No location"}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
248
apps/web/src/features/operations/orders/VendorOrderList.tsx
Normal file
248
apps/web/src/features/operations/orders/VendorOrderList.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { format, parseISO, isValid } from "date-fns";
|
||||
import { Search, MapPin } from "lucide-react";
|
||||
|
||||
import { Card, CardContent } from "@/common/components/ui/card";
|
||||
import { Input } from "@/common/components/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/common/components/ui/table";
|
||||
import DashboardLayout from "@/features/layouts/DashboardLayout";
|
||||
import { useSelector } from "react-redux";
|
||||
import type { RootState } from "@/store/store";
|
||||
import { useListOrders, useGetVendorByUserId } from "@/dataconnect-generated/react";
|
||||
import { dataConnect } from "@/features/auth/firebase";
|
||||
|
||||
const safeParseDate = (dateString: any): Date | null => {
|
||||
if (!dateString) return null;
|
||||
try {
|
||||
const date = typeof dateString === "string" ? parseISO(dateString) : new Date(dateString);
|
||||
return isValid(date) ? date : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const hasVendorAssignments = (order: any, vendorId?: string | null) => {
|
||||
if (!vendorId) return false;
|
||||
const assignedStaff = order?.assignedStaff;
|
||||
if (!Array.isArray(assignedStaff)) return false;
|
||||
|
||||
return assignedStaff.some((assignment: any) => {
|
||||
const assignmentVendorId =
|
||||
assignment?.vendorId ||
|
||||
assignment?.vendor_id ||
|
||||
assignment?.vendor?.id ||
|
||||
null;
|
||||
return assignmentVendorId === vendorId;
|
||||
});
|
||||
};
|
||||
|
||||
const getVendorPositionsSummary = (order: any, vendorId?: string | null) => {
|
||||
if (!vendorId) return "—";
|
||||
const assignedStaff = order?.assignedStaff;
|
||||
if (!Array.isArray(assignedStaff)) return "—";
|
||||
|
||||
const vendorAssignments = assignedStaff.filter((assignment: any) => {
|
||||
const assignmentVendorId =
|
||||
assignment?.vendorId ||
|
||||
assignment?.vendor_id ||
|
||||
assignment?.vendor?.id ||
|
||||
null;
|
||||
return assignmentVendorId === vendorId;
|
||||
});
|
||||
|
||||
if (vendorAssignments.length === 0) return "—";
|
||||
|
||||
const positions = Array.from(
|
||||
new Set(
|
||||
vendorAssignments
|
||||
.map((assignment: any) => assignment.positionName || assignment.roleName || assignment.position || null)
|
||||
.filter(Boolean),
|
||||
),
|
||||
) as string[];
|
||||
|
||||
if (positions.length === 0) return `${vendorAssignments.length} staff`;
|
||||
|
||||
return positions.join(", ");
|
||||
};
|
||||
|
||||
const getEstimatedRevenue = (order: any, vendorId?: string | null) => {
|
||||
if (!vendorId) return "—";
|
||||
const assignedStaff = order?.assignedStaff;
|
||||
if (!Array.isArray(assignedStaff)) return "—";
|
||||
|
||||
const vendorAssignments = assignedStaff.filter((assignment: any) => {
|
||||
const assignmentVendorId =
|
||||
assignment?.vendorId ||
|
||||
assignment?.vendor_id ||
|
||||
assignment?.vendor?.id ||
|
||||
null;
|
||||
return assignmentVendorId === vendorId;
|
||||
});
|
||||
|
||||
if (vendorAssignments.length === 0) return "—";
|
||||
|
||||
const total = vendorAssignments.reduce((sum: number, assignment: any) => {
|
||||
const hours = Number(
|
||||
assignment?.estimatedHours ?? assignment?.hours ?? assignment?.shiftHours ?? 0,
|
||||
);
|
||||
const rate = Number(
|
||||
assignment?.vendorBillRate ?? assignment?.billRate ?? assignment?.rate ?? 0,
|
||||
);
|
||||
|
||||
if (!Number.isFinite(hours) || !Number.isFinite(rate)) return sum;
|
||||
return sum + hours * rate;
|
||||
}, 0);
|
||||
|
||||
if (!Number.isFinite(total) || total <= 0) return "—";
|
||||
|
||||
return `$${Math.round(total).toLocaleString()}`;
|
||||
};
|
||||
|
||||
export default function VendorOrderList() {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
// 1. Resolve the logged-in vendor from the current user
|
||||
const { data: vendorData } = useGetVendorByUserId(
|
||||
{ userId: user?.uid || "" },
|
||||
{ enabled: !!user?.uid },
|
||||
);
|
||||
|
||||
const vendor = vendorData?.vendors?.[0];
|
||||
const vendorId: string | null = vendor?.id ?? null;
|
||||
|
||||
// 2. Load all orders from Data Connect
|
||||
const { data: orderData, isLoading } = useListOrders(dataConnect);
|
||||
const orders = orderData?.orders || [];
|
||||
|
||||
// 3. Filter to only orders where this vendor has assigned staff
|
||||
const vendorOrders = useMemo(() => {
|
||||
if (!vendorId) return [];
|
||||
return orders.filter((order: any) => hasVendorAssignments(order, vendorId));
|
||||
}, [orders, vendorId]);
|
||||
|
||||
// 4. Apply search filter (Order #, Client, Event)
|
||||
const filteredOrders = useMemo(() => {
|
||||
const lowerSearch = searchTerm.trim().toLowerCase();
|
||||
if (!lowerSearch) return vendorOrders;
|
||||
|
||||
return vendorOrders.filter((order: any) => {
|
||||
const orderId = order.id?.toString().toLowerCase() ?? "";
|
||||
const eventName = order.eventName?.toLowerCase?.() ?? "";
|
||||
const clientName = order.business?.businessName?.toLowerCase?.() ?? "";
|
||||
|
||||
return (
|
||||
orderId.includes(lowerSearch) ||
|
||||
eventName.includes(lowerSearch) ||
|
||||
clientName.includes(lowerSearch)
|
||||
);
|
||||
});
|
||||
}, [vendorOrders, searchTerm]);
|
||||
|
||||
return (
|
||||
<DashboardLayout
|
||||
title="Vendor Orders"
|
||||
subtitle="Orders where your team is assigned"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Search */}
|
||||
<Card className="bg-card border-border/50 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search by order #, client, or event name..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Orders Table */}
|
||||
<div className="bg-card rounded-xl shadow-sm border border-border/50 overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead className="w-[120px]">Order #</TableHead>
|
||||
<TableHead>Client</TableHead>
|
||||
<TableHead>Event Date</TableHead>
|
||||
<TableHead>Your Positions</TableHead>
|
||||
<TableHead className="text-right">Estimated Revenue</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-10 text-muted-foreground">
|
||||
Loading vendor orders...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredOrders.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-10 text-muted-foreground">
|
||||
{vendorId
|
||||
? "No orders found where your team is assigned."
|
||||
: "No vendor profile found for this user."}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredOrders.map((order: any) => {
|
||||
const eventDate = safeParseDate(order.date);
|
||||
const positionsSummary = getVendorPositionsSummary(order, vendorId);
|
||||
const estimatedRevenue = getEstimatedRevenue(order, vendorId);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={order.id}
|
||||
className="cursor-pointer hover:bg-muted/30"
|
||||
onClick={() => navigate(`/orders/${order.id}`)}
|
||||
>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground uppercase">
|
||||
{order.id?.toString().substring(0, 8)}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{order.business?.businessName || "Unknown client"}
|
||||
<div className="flex items-center gap-1 text-[10px] text-muted-foreground mt-0.5">
|
||||
<MapPin className="w-3 h-3" />
|
||||
{order.business?.businessName || "No location"}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm">
|
||||
{eventDate ? format(eventDate, "MMM dd, yyyy") : "No date"}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground uppercase">
|
||||
{eventDate ? format(eventDate, "EEEE") : ""}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{positionsSummary}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-semibold">
|
||||
{estimatedRevenue}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import ServiceRates from './features/business/rates/ServiceRates';
|
||||
import OrderList from './features/operations/orders/OrderList';
|
||||
import OrderDetail from './features/operations/orders/OrderDetail';
|
||||
import ClientOrderList from './features/operations/orders/ClientOrderList';
|
||||
import VendorOrderList from './features/operations/orders/VendorOrderList';
|
||||
|
||||
/**
|
||||
* AppRoutes Component
|
||||
@@ -97,7 +98,8 @@ const AppRoutes: React.FC = () => {
|
||||
<Route path="/orders" element={<OrderList />} />
|
||||
<Route path="/orders/client" element={<ClientOrderList />} />
|
||||
<Route path="/orders/:id" element={<OrderDetail />} />
|
||||
|
||||
<Route path="/orders/vendor" element={<VendorOrderList />} />
|
||||
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||
</Routes>
|
||||
|
||||
Reference in New Issue
Block a user