feat: Implement Order List View for Vendors

This commit is contained in:
dhinesh-m24
2026-02-05 16:50:16 +05:30
parent 9dab3fef05
commit 475cc8ae80
3 changed files with 256 additions and 5 deletions

View File

@@ -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>

View 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>
);
}

View File

@@ -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>