From 6b8680570be40eb43a4bffc10c792a5cf31c1f34 Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:09:32 +0530 Subject: [PATCH] enh --- .../src/trpc/admin-apis/vendor-snippets.ts | 2 + apps/backend/src/trpc/user-apis/product.ts | 19 +- .../src/routes/vendor-order-list.tsx | 334 ++++++++++-------- .../app/(drawer)/(tabs)/home/index.tsx | 105 +++--- apps/user-ui/components/cart-page.tsx | 2 - 5 files changed, 265 insertions(+), 197 deletions(-) diff --git a/apps/backend/src/trpc/admin-apis/vendor-snippets.ts b/apps/backend/src/trpc/admin-apis/vendor-snippets.ts index 51793f9..79adfa4 100644 --- a/apps/backend/src/trpc/admin-apis/vendor-snippets.ts +++ b/apps/backend/src/trpc/admin-apis/vendor-snippets.ts @@ -263,6 +263,7 @@ export const vendorSnippetsRouter = router({ productId: item.productId, productName: item.product.name, quantity: parseFloat(item.quantity), + productSize: item.product.productQuantity, price: parseFloat(item.price.toString()), unit: item.product.unit?.shortNotation || 'unit', subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity), @@ -419,6 +420,7 @@ export const vendorSnippetsRouter = router({ price: parseFloat(item.price.toString()), unit: item.product.unit?.shortNotation || 'unit', subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity), + productSize: item.product.productQuantity, is_packaged: item.is_packaged, is_package_verified: item.is_package_verified, })); diff --git a/apps/backend/src/trpc/user-apis/product.ts b/apps/backend/src/trpc/user-apis/product.ts index 70478a1..34432df 100644 --- a/apps/backend/src/trpc/user-apis/product.ts +++ b/apps/backend/src/trpc/user-apis/product.ts @@ -6,6 +6,7 @@ import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url, generateUploa import { ApiError } from '../../lib/api-error'; import { eq, and, gt, sql, inArray, desc } from 'drizzle-orm'; import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '../../stores/product-store'; +import dayjs from 'dayjs'; // Uniform Product Type interface Product { @@ -42,11 +43,20 @@ export const productRouter = router({ console.log('from the api to get product details') - // First, try to get the product from Redis cache +// First, try to get the product from Redis cache const cachedProduct = await getProductByIdFromCache(productId); - + if (cachedProduct) { - return cachedProduct; + // Filter delivery slots to only include those with future freeze times + const currentTime = new Date(); + const filteredSlots = cachedProduct.deliverySlots.filter(slot => + dayjs(slot.freezeTime).isAfter(currentTime) + ); + + return { + ...cachedProduct, + deliverySlots: filteredSlots + }; } // If not in cache, fetch from database (fallback) @@ -97,7 +107,8 @@ export const productRouter = router({ and( eq(productSlots.productId, productId), eq(deliverySlotInfo.isActive, true), - gt(deliverySlotInfo.deliveryTime, sql`NOW()`) + gt(deliverySlotInfo.deliveryTime, sql`NOW()`), + gt(deliverySlotInfo.freezeTime, sql`NOW()`) ) ) .orderBy(deliverySlotInfo.deliveryTime); diff --git a/apps/fallback-ui/src/routes/vendor-order-list.tsx b/apps/fallback-ui/src/routes/vendor-order-list.tsx index 8efb268..fa18a1b 100644 --- a/apps/fallback-ui/src/routes/vendor-order-list.tsx +++ b/apps/fallback-ui/src/routes/vendor-order-list.tsx @@ -1,114 +1,133 @@ -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 { 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 + 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 - }> + 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 + id: number; + deliveryTime: string; + freezeTime: string; + deliverySequence: any; } export function VendorOrderListRoute() { - const { id } = useSearch({ from: '/vendor-order-list' }) + 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 + : { data: null, isLoading: false }; + const snippet = snippetInfo?.snippet; + const isPermanent = snippet?.isPermanent; - const { data: upcomingSlots } = trpc.admin.vendorSnippets.getUpcomingSlots.useQuery(undefined, { enabled: !!id }) + const { data: upcomingSlots } = + trpc.admin.vendorSnippets.getUpcomingSlots.useQuery(undefined, { + enabled: !!id, + }); // State for selected slot - const [selectedSlotId, setSelectedSlotId] = useState(null) + const [selectedSlotId, setSelectedSlotId] = useState(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) + if ( + isPermanent && + upcomingSlots?.data && + upcomingSlots.data.length > 0 && + !selectedSlotId + ) { + setSelectedSlotId(upcomingSlots.data[0].id); } - }, [isPermanent, upcomingSlots, selectedSlotId]) + }, [isPermanent, upcomingSlots, selectedSlotId]); // Fetch orders based on mode - const { data: slotOrdersData, error, isLoading: isLoadingOrders, isFetching, refetch } = trpc.admin.vendorSnippets.getOrdersBySnippetAndSlot.useQuery( + const { + data: slotOrdersData, + error, + isLoading: isLoadingOrders, + isFetching, + refetch, + } = trpc.admin.vendorSnippets.getOrdersBySnippetAndSlot.useQuery( { snippetCode: id!, slotId: selectedSlotId! }, - { enabled: !!id && !!selectedSlotId && isPermanent } - ) + { enabled: !!id && !!selectedSlotId && isPermanent }, + ); -const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.useQuery( - { snippetCode: id! }, - { enabled: !!id && !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 orders = slotOrdersData?.data || regularOrders?.data || []; + const isLoadingCurrent = isPermanent ? isLoadingOrders : isLoadingSnippet; - const updatePackagingMutation = trpc.admin.vendorSnippets.updateOrderItemPackaging.useMutation() + const updatePackagingMutation = + trpc.admin.vendorSnippets.updateOrderItemPackaging.useMutation(); - const [updatingItems, setUpdatingItems] = useState>(new Set()) + const [updatingItems, setUpdatingItems] = useState>(new Set()); - const handlePackagingToggle = async (orderItemId: number, currentValue: boolean) => { - setUpdatingItems(prev => new Set(prev).add(orderItemId)) + const handlePackagingToggle = async ( + orderItemId: number, + currentValue: boolean, + ) => { + setUpdatingItems((prev) => new Set(prev).add(orderItemId)); try { await updatePackagingMutation.mutateAsync({ orderItemId, - is_packaged: !currentValue - }) + is_packaged: !currentValue, + }); // Refetch data to update the UI - refetch() + refetch(); } catch (error) { - console.error('Failed to update packaging status:', error) + console.error("Failed to update packaging status:", error); } finally { - setUpdatingItems(prev => { - const newSet = new Set(prev) - newSet.delete(orderItemId) - return newSet - }) + setUpdatingItems((prev) => { + const newSet = new Set(prev); + newSet.delete(orderItemId); + return newSet; + }); } - } - + }; const productSummary = useMemo(() => { const summary: Record = {}; - orders.forEach(order => { - order.products.forEach(product => { + 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; + summary[key].quantity += product.quantity * product.productSize; }); }); return Object.entries(summary).map(([name, data]) => ({ name, quantity: data.quantity, - unit: data.unit + unit: data.unit, })); - }, [orders]) + }, [orders]); return (
@@ -118,7 +137,9 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use {productSummary.map((item, index) => (
{item.name}: - {item.quantity} {item.unit} + + {item.quantity} {item.unit} +
))} @@ -127,7 +148,9 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use
-

Vendor Orders

+

+ Vendor Orders +

Track incoming orders and fulfilment progress for vendor partners. {id && Snippet: {id}} @@ -135,18 +158,21 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use

{isPermanent && upcomingSlots?.data && (
-
-
- {isLoadingCurrent ? ( -
- Loading orders… -
- ) : error ? ( -
- {error.message ?? 'Unable to load vendor orders right now'} -
- ) : !id ? ( -
- No snippet code provided -
- ) : orders.length === 0 ? ( +
+ {isLoadingCurrent ? ( +
+ Loading orders… +
+ ) : error ? ( +
+ {error.message ?? "Unable to load vendor orders right now"} +
+ ) : !id ? ( +
+ No snippet code provided +
+ ) : orders.length === 0 ? (
No vendor orders found.
) : (
-{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' + {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 (
{order.orderId} - - Pending - + Pending -
-
-
- Products (Packaging Status) -
-
- {order.products.map((product) => ( -
- - {product.productName}: {product.quantity} {product.unit} - - - handlePackagingToggle(product.orderItemId, product.is_packaged)} - className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" - /> -
- ))} -
-
-
-
- Date -
-
{parsedDate}
-
-
-
- Total Amount -
-
- ₹{order.totalAmount} -
-
-
+
+
+
+ Products (Packaging Status) +
+
+ {order.products.map((product) => ( +
+ + {product.productName}: {product.productSize * product.quantity}{" "} + {product.unit} + + + + handlePackagingToggle( + product.orderItemId, + product.is_packaged, + ) + } + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + /> +
+ ))} +
+
+
+
+ Date +
+
+ {parsedDate} +
+
+
+
+ Total Amount +
+
+ ₹{order.totalAmount} +
+
+
- ) + ); })}
)} @@ -263,13 +305,5 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use ) : null}
- ) -} - -const statusBadgeStyles: Record = { - pending: 'border-amber-200 bg-amber-100 text-amber-700', - completed: 'border-emerald-200 bg-emerald-100 text-emerald-700', - cancelled: 'border-rose-200 bg-rose-100 text-rose-700', - inprogress: 'border-sky-200 bg-sky-100 text-sky-700', - dispatched: 'border-indigo-200 bg-indigo-100 text-indigo-700' -} + ); +} \ No newline at end of file diff --git a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx index c6b8fb6..0deab9e 100755 --- a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx @@ -21,6 +21,7 @@ import MaterialIcons from "@expo/vector-icons/MaterialIcons"; import FontAwesome5 from "@expo/vector-icons/FontAwesome5"; import { Ionicons } from "@expo/vector-icons"; import ProductCard from "@/components/ProductCard"; +import MyFlatList from "common-ui/src/components/flat-list"; import { trpc } from "@/src/trpc-client"; import { useGetCart } from "@/hooks/cart-query-hooks"; @@ -38,6 +39,7 @@ dayjs.extend(relativeTime); const { width: screenWidth } = Dimensions.get("window"); const itemWidth = screenWidth * 0.45; // 45% of screen width +const gridItemWidth = (screenWidth * 0.9) / 2; // Half of screen width minus padding const RenderStore = ({ item, @@ -115,6 +117,7 @@ export default function Dashboard() { const [hasMore, setHasMore] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); const { backgroundColor } = useStatusBarStore(); + const { getQuickestSlot } = useProductSlotIdentifier(); const { data: productsData, @@ -136,43 +139,47 @@ export default function Dashboard() { const products = productsData?.products || []; - // Function to load more products +// Function to load more products const loadMoreProducts = () => { if (!hasMore || isLoadingMore) return; setIsLoadingMore(true); - // Simulate loading more products by taking the next batch - // In a real app, you would make an API call with pagination params - // setTimeout(() => { - const batchSize = 10; - const startIndex = page * batchSize; - const endIndex = startIndex + batchSize; + const batchSize = 10; + const startIndex = page * batchSize; + const endIndex = startIndex + batchSize; - // Get the next batch of products - const nextBatch = products.slice(startIndex, endIndex); + // Get the next batch of products + const nextBatchRaw = products.slice(startIndex, endIndex); + + // Filter products to only include those with available slots + const nextBatch = nextBatchRaw.filter(product => { + const slot = getQuickestSlot(product.id); + return slot !== null && slot !== undefined; + }); - const oldBatchIds = displayedProducts.map(p => p.id) - const nextBatchIds = nextBatch.map(p => p.id) - const productIds = products.map(p => p.id); - + if (nextBatch.length > 0) { + setDisplayedProducts(prev => [...prev, ...nextBatch]); + setPage(prev => page + 1); + setHasMore(endIndex < products.length); + } else { + setHasMore(false); + } - if (nextBatch.length > 0) { - setDisplayedProducts(prev => [...prev, ...nextBatch]); - setPage(prev => prev + 1); - setHasMore(endIndex < products.length); - } else { - setHasMore(false); - } - - setIsLoadingMore(false); - // }, 500); // Simulate network delay + setIsLoadingMore(false); }; - // Initialize with the first batch of products + // Initialize with the first batch of products (only those with available slots) React.useEffect(() => { if (products.length > 0 && displayedProducts.length === 0) { - const initialBatch = products.slice(0, 10); // First 10 products + const initialBatchRaw = products.slice(0, 10); + + // Filter to include only products with available slots + const initialBatch = initialBatchRaw.filter(product => { + const slot = getQuickestSlot(product.id); + return slot !== null && slot !== undefined; + }); + setDisplayedProducts(initialBatch); setHasMore(products.length > 10); setPage(1); @@ -359,7 +366,6 @@ export default function Dashboard() { - {/* White Section */} - {/* Product Grid */} - - {displayedProducts.map((item, index: number) => ( - + {/* Product List */} + item.id.toString()} + numColumns={2} + contentContainerStyle={tw`pb-8`} + columnWrapperStyle={tw`py-2`} + renderItem={({ item, index }) => ( + router.push( `/(drawer)/(tabs)/home/product-detail/${item.id}` @@ -601,18 +612,30 @@ export default function Dashboard() { } showDeliveryInfo={true} miniView={false} - nullIfNotAvailable={true} - containerComp={({children}) => {children}} + // nullIfNotAvailable={true} key={item.id} /> - ))} - - - {isLoadingMore && ( - - Loading more... - - )} + + )} + onEndReached={() => { + if (!isLoadingMore && hasMore) { + loadMoreProducts(); + } + }} + onEndReachedThreshold={0.5} + ListFooterComponent={ + isLoadingMore ? ( + + Loading more... + + ) : null + } + ListEmptyComponent={ + + No products available + + } + /> diff --git a/apps/user-ui/components/cart-page.tsx b/apps/user-ui/components/cart-page.tsx index d04b41a..591042c 100644 --- a/apps/user-ui/components/cart-page.tsx +++ b/apps/user-ui/components/cart-page.tsx @@ -825,8 +825,6 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { )} - - {/* Bottom Checkout Bar - Now Static */} {hasAvailableItems && (