309 lines
No EOL
11 KiB
TypeScript
309 lines
No EOL
11 KiB
TypeScript
import { useMemo, useState, useEffect } from "react";
|
|
import { useSearch } from "@tanstack/react-router";
|
|
import { trpc } from "../trpc/client";
|
|
import { cn } from "@/lib/utils";
|
|
import { Button } from "@/components/ui/button";
|
|
import dayjs from "dayjs";
|
|
import items from "razorpay/dist/types/items";
|
|
|
|
interface VendorOrder {
|
|
orderId: string;
|
|
customerName: string;
|
|
orderDate: string;
|
|
totalAmount: string;
|
|
products: Array<{
|
|
orderItemId: number;
|
|
productId: number;
|
|
productName: string;
|
|
quantity: number;
|
|
unit: string;
|
|
is_packaged: boolean;
|
|
is_package_verified: boolean;
|
|
}>;
|
|
}
|
|
|
|
interface DeliverySlot {
|
|
id: number;
|
|
deliveryTime: string;
|
|
freezeTime: string;
|
|
deliverySequence: any;
|
|
}
|
|
|
|
export function VendorOrderListRoute() {
|
|
const { id } = useSearch({ from: "/vendor-order-list" });
|
|
|
|
// Fetch snippet info
|
|
const { data: snippetInfo, isLoading: isLoadingSnippet } = id
|
|
? trpc.admin.vendorSnippets.getOrdersBySnippet.useQuery({ snippetCode: id })
|
|
: { data: null, isLoading: false };
|
|
const snippet = snippetInfo?.snippet;
|
|
const isPermanent = snippet?.isPermanent;
|
|
|
|
const { data: upcomingSlots } =
|
|
trpc.admin.vendorSnippets.getUpcomingSlots.useQuery(undefined, {
|
|
enabled: !!id,
|
|
});
|
|
|
|
// State for selected slot
|
|
const [selectedSlotId, setSelectedSlotId] = useState<number | null>(null);
|
|
|
|
// Auto-select first slot when snippets are loaded and isPermanent is true
|
|
useEffect(() => {
|
|
if (
|
|
isPermanent &&
|
|
upcomingSlots?.data &&
|
|
upcomingSlots.data.length > 0 &&
|
|
!selectedSlotId
|
|
) {
|
|
setSelectedSlotId(upcomingSlots.data[0].id);
|
|
}
|
|
}, [isPermanent, upcomingSlots, selectedSlotId]);
|
|
|
|
// Fetch orders based on mode
|
|
const {
|
|
data: slotOrdersData,
|
|
error,
|
|
isLoading: isLoadingOrders,
|
|
isFetching,
|
|
refetch,
|
|
} = trpc.admin.vendorSnippets.getOrdersBySnippetAndSlot.useQuery(
|
|
{ snippetCode: id!, slotId: selectedSlotId! },
|
|
{ enabled: !!id && !!selectedSlotId && isPermanent },
|
|
);
|
|
|
|
const { data: regularOrders } =
|
|
trpc.admin.vendorSnippets.getOrdersBySnippet.useQuery(
|
|
{ snippetCode: id! },
|
|
{ enabled: !!id && !isPermanent },
|
|
);
|
|
|
|
const orders = slotOrdersData?.data || regularOrders?.data || [];
|
|
const isLoadingCurrent = isPermanent ? isLoadingOrders : isLoadingSnippet;
|
|
|
|
const updatePackagingMutation =
|
|
trpc.admin.vendorSnippets.updateOrderItemPackaging.useMutation();
|
|
|
|
const [updatingItems, setUpdatingItems] = useState<Set<number>>(new Set());
|
|
|
|
const handlePackagingToggle = async (
|
|
orderItemId: number,
|
|
currentValue: boolean,
|
|
) => {
|
|
setUpdatingItems((prev) => new Set(prev).add(orderItemId));
|
|
|
|
try {
|
|
await updatePackagingMutation.mutateAsync({
|
|
orderItemId,
|
|
is_packaged: !currentValue,
|
|
});
|
|
// Refetch data to update the UI
|
|
refetch();
|
|
} catch (error) {
|
|
console.error("Failed to update packaging status:", error);
|
|
} finally {
|
|
setUpdatingItems((prev) => {
|
|
const newSet = new Set(prev);
|
|
newSet.delete(orderItemId);
|
|
return newSet;
|
|
});
|
|
}
|
|
};
|
|
|
|
const productSummary = useMemo(() => {
|
|
const summary: Record<string, { quantity: number; unit: string }> = {};
|
|
|
|
orders.forEach((order) => {
|
|
order.products.forEach((product) => {
|
|
const key = product.productName;
|
|
if (!summary[key]) {
|
|
summary[key] = { quantity: 0, unit: product.unit };
|
|
}
|
|
summary[key].quantity += product.quantity * product.productSize;
|
|
});
|
|
});
|
|
|
|
return Object.entries(summary).map(([name, data]) => ({
|
|
name,
|
|
quantity: data.quantity,
|
|
unit: data.unit,
|
|
}));
|
|
}, [orders]);
|
|
|
|
return (
|
|
<section className="space-y-6">
|
|
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
|
<h2 className="text-xl font-semibold text-slate-900 mb-4">Summary</h2>
|
|
<div className="grid gap-2">
|
|
{productSummary.map((item, index) => (
|
|
<div key={index} className="flex justify-between text-sm">
|
|
<span className="text-slate-600">{item.name}:</span>
|
|
<span className="font-medium text-slate-900">
|
|
{item.quantity} {item.unit}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
<div>
|
|
<h2 className="text-xl font-semibold text-slate-900">
|
|
Vendor Orders
|
|
</h2>
|
|
<p className="text-sm text-slate-500">
|
|
Track incoming orders and fulfilment progress for vendor partners.
|
|
{id && <span className="block mt-1 text-xs">Snippet: {id}</span>}
|
|
</p>
|
|
</div>
|
|
{isPermanent && upcomingSlots?.data && (
|
|
<div className="flex items-center gap-2">
|
|
<label
|
|
htmlFor="slot-select"
|
|
className="text-sm font-medium text-slate-700"
|
|
>
|
|
Select Slot:
|
|
</label>
|
|
<select
|
|
id="slot-select"
|
|
value={selectedSlotId || ""}
|
|
onChange={(e) => setSelectedSlotId(Number(e.target.value))}
|
|
className="base-select px-3 py-2 text-sm border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
{upcomingSlots.data.map((slot) => (
|
|
<option key={slot.id} value={slot.id}>
|
|
{dayjs(slot.deliveryTime).format("ddd, MMM DD hh:mm A")}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
void refetch();
|
|
}}
|
|
disabled={isFetching}
|
|
>
|
|
{isFetching ? "Refreshing…" : "Refresh"}
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="mt-6">
|
|
{isLoadingCurrent ? (
|
|
<div className="rounded-xl border border-slate-100 bg-slate-50 p-6 text-sm text-slate-600">
|
|
Loading orders…
|
|
</div>
|
|
) : error ? (
|
|
<div className="rounded-xl border border-red-200 bg-red-50 p-6 text-sm text-red-600">
|
|
{error.message ?? "Unable to load vendor orders right now"}
|
|
</div>
|
|
) : !id ? (
|
|
<div className="rounded-xl border border-red-200 bg-red-50 p-6 text-sm text-red-600">
|
|
No snippet code provided
|
|
</div>
|
|
) : orders.length === 0 ? (
|
|
<div className="rounded-xl border border-slate-100 bg-slate-50 p-6 text-sm text-slate-600">
|
|
No vendor orders found.
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{orders.map((order) => {
|
|
const parsedDate = order.orderDate
|
|
? dayjs(order.orderDate).format("ddd, MMM DD hh:mm A")
|
|
: "N/A";
|
|
const badgeClass =
|
|
"border-slate-200 bg-slate-100 text-slate-600 inline-flex items-center rounded-full border px-3 py-0.5 text-xs font-semibold uppercase";
|
|
|
|
return (
|
|
<article
|
|
key={order.orderId}
|
|
className="flex flex-col gap-4 rounded-xl border border-slate-200 bg-white p-4 shadow-sm"
|
|
>
|
|
<header className="flex items-start justify-between gap-3">
|
|
<h3 className="text-base font-semibold text-slate-900">
|
|
{order.orderId}
|
|
</h3>
|
|
<span className={badgeClass}>Pending</span>
|
|
</header>
|
|
<dl className="grid gap-3 text-sm text-slate-600">
|
|
<div className="space-y-2">
|
|
<dt className="text-xs uppercase tracking-wide text-slate-400">
|
|
Products (Packaging Status)
|
|
</dt>
|
|
<dd className="space-y-2">
|
|
{order.products.map((product) => (
|
|
<div
|
|
key={product.orderItemId}
|
|
className="flex items-center gap-3"
|
|
>
|
|
<span className="text-sm font-medium text-slate-900 flex-1">
|
|
{product.productName}: {product.productSize * product.quantity}{" "}
|
|
{product.unit}
|
|
</span>
|
|
<label
|
|
htmlFor={`package-${product.orderItemId}`}
|
|
className={cn(
|
|
"text-sm font-medium",
|
|
product.is_packaged
|
|
? "text-green-700"
|
|
: "text-slate-600",
|
|
)}
|
|
>
|
|
Packaged
|
|
{updatingItems.has(product.orderItemId) && (
|
|
<span className="ml-2 text-xs text-blue-500">
|
|
(updating...)
|
|
</span>
|
|
)}
|
|
</label>
|
|
<input
|
|
type="checkbox"
|
|
id={`package-${product.orderItemId}`}
|
|
checked={product.is_packaged}
|
|
disabled={updatingItems.has(
|
|
product.orderItemId,
|
|
)}
|
|
onChange={() =>
|
|
handlePackagingToggle(
|
|
product.orderItemId,
|
|
product.is_packaged,
|
|
)
|
|
}
|
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
/>
|
|
</div>
|
|
))}
|
|
</dd>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<dt className="text-xs uppercase tracking-wide text-slate-400">
|
|
Date
|
|
</dt>
|
|
<dd className="font-medium text-slate-900">
|
|
{parsedDate}
|
|
</dd>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<dt className="text-xs uppercase tracking-wide text-slate-400">
|
|
Total Amount
|
|
</dt>
|
|
<dd className="font-medium text-slate-900">
|
|
₹{order.totalAmount}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{isFetching && !isLoadingCurrent ? (
|
|
<p className="mt-4 text-xs text-slate-500">Refreshing…</p>
|
|
) : null}
|
|
</div>
|
|
</section>
|
|
);
|
|
} |