feat: Implement Order List View for Vendors
This commit is contained in:
@@ -70,7 +70,7 @@ export default function ClientOrderList() {
|
|||||||
const lower = searchTerm.toLowerCase();
|
const lower = searchTerm.toLowerCase();
|
||||||
filtered = filtered.filter(o =>
|
filtered = filtered.filter(o =>
|
||||||
o.eventName?.toLowerCase().includes(lower) ||
|
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") {
|
if (locationFilter !== "all") {
|
||||||
filtered = filtered.filter(o => o.teamHub?.hubName === locationFilter);
|
filtered = filtered.filter(o => o.business.businessName === locationFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
@@ -88,7 +88,8 @@ export default function ClientOrderList() {
|
|||||||
const uniqueLocations = useMemo(() => {
|
const uniqueLocations = useMemo(() => {
|
||||||
const locations = new Set<string>();
|
const locations = new Set<string>();
|
||||||
orders.forEach(o => {
|
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();
|
return Array.from(locations).sort();
|
||||||
}, [orders]);
|
}, [orders]);
|
||||||
@@ -274,7 +275,7 @@ export default function ClientOrderList() {
|
|||||||
{order.eventName}
|
{order.eventName}
|
||||||
<div className="flex items-center gap-1 text-[10px] text-muted-foreground mt-0.5">
|
<div className="flex items-center gap-1 text-[10px] text-muted-foreground mt-0.5">
|
||||||
<MapPin className="w-3 h-3" />
|
<MapPin className="w-3 h-3" />
|
||||||
{order.teamHub?.hubName || "No location"}
|
{order.business.businessName || "No location"}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<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 OrderList from './features/operations/orders/OrderList';
|
||||||
import OrderDetail from './features/operations/orders/OrderDetail';
|
import OrderDetail from './features/operations/orders/OrderDetail';
|
||||||
import ClientOrderList from './features/operations/orders/ClientOrderList';
|
import ClientOrderList from './features/operations/orders/ClientOrderList';
|
||||||
|
import VendorOrderList from './features/operations/orders/VendorOrderList';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AppRoutes Component
|
* AppRoutes Component
|
||||||
@@ -97,6 +98,7 @@ const AppRoutes: React.FC = () => {
|
|||||||
<Route path="/orders" element={<OrderList />} />
|
<Route path="/orders" element={<OrderList />} />
|
||||||
<Route path="/orders/client" element={<ClientOrderList />} />
|
<Route path="/orders/client" element={<ClientOrderList />} />
|
||||||
<Route path="/orders/:id" element={<OrderDetail />} />
|
<Route path="/orders/:id" element={<OrderDetail />} />
|
||||||
|
<Route path="/orders/vendor" element={<VendorOrderList />} />
|
||||||
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/login" replace />} />
|
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||||
|
|||||||
Reference in New Issue
Block a user