enh
This commit is contained in:
parent
b953774fbc
commit
6b8680570b
5 changed files with 265 additions and 197 deletions
|
|
@ -263,6 +263,7 @@ export const vendorSnippetsRouter = router({
|
||||||
productId: item.productId,
|
productId: item.productId,
|
||||||
productName: item.product.name,
|
productName: item.product.name,
|
||||||
quantity: parseFloat(item.quantity),
|
quantity: parseFloat(item.quantity),
|
||||||
|
productSize: item.product.productQuantity,
|
||||||
price: parseFloat(item.price.toString()),
|
price: parseFloat(item.price.toString()),
|
||||||
unit: item.product.unit?.shortNotation || 'unit',
|
unit: item.product.unit?.shortNotation || 'unit',
|
||||||
subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity),
|
subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity),
|
||||||
|
|
@ -419,6 +420,7 @@ export const vendorSnippetsRouter = router({
|
||||||
price: parseFloat(item.price.toString()),
|
price: parseFloat(item.price.toString()),
|
||||||
unit: item.product.unit?.shortNotation || 'unit',
|
unit: item.product.unit?.shortNotation || 'unit',
|
||||||
subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity),
|
subtotal: parseFloat(item.price.toString()) * parseFloat(item.quantity),
|
||||||
|
productSize: item.product.productQuantity,
|
||||||
is_packaged: item.is_packaged,
|
is_packaged: item.is_packaged,
|
||||||
is_package_verified: item.is_package_verified,
|
is_package_verified: item.is_package_verified,
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url, generateUploa
|
||||||
import { ApiError } from '../../lib/api-error';
|
import { ApiError } from '../../lib/api-error';
|
||||||
import { eq, and, gt, sql, inArray, desc } from 'drizzle-orm';
|
import { eq, and, gt, sql, inArray, desc } from 'drizzle-orm';
|
||||||
import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '../../stores/product-store';
|
import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '../../stores/product-store';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
// Uniform Product Type
|
// Uniform Product Type
|
||||||
interface Product {
|
interface Product {
|
||||||
|
|
@ -46,7 +47,16 @@ export const productRouter = router({
|
||||||
const cachedProduct = await getProductByIdFromCache(productId);
|
const cachedProduct = await getProductByIdFromCache(productId);
|
||||||
|
|
||||||
if (cachedProduct) {
|
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)
|
// If not in cache, fetch from database (fallback)
|
||||||
|
|
@ -97,7 +107,8 @@ export const productRouter = router({
|
||||||
and(
|
and(
|
||||||
eq(productSlots.productId, productId),
|
eq(productSlots.productId, productId),
|
||||||
eq(deliverySlotInfo.isActive, true),
|
eq(deliverySlotInfo.isActive, true),
|
||||||
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
|
gt(deliverySlotInfo.deliveryTime, sql`NOW()`),
|
||||||
|
gt(deliverySlotInfo.freezeTime, sql`NOW()`)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.orderBy(deliverySlotInfo.deliveryTime);
|
.orderBy(deliverySlotInfo.deliveryTime);
|
||||||
|
|
|
||||||
|
|
@ -1,114 +1,133 @@
|
||||||
import { useMemo, useState, useEffect } from 'react'
|
import { useMemo, useState, useEffect } from "react";
|
||||||
import { useSearch } from '@tanstack/react-router'
|
import { useSearch } from "@tanstack/react-router";
|
||||||
import { trpc } from '../trpc/client'
|
import { trpc } from "../trpc/client";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from "@/components/ui/button";
|
||||||
import dayjs from 'dayjs'
|
import dayjs from "dayjs";
|
||||||
|
import items from "razorpay/dist/types/items";
|
||||||
|
|
||||||
interface VendorOrder {
|
interface VendorOrder {
|
||||||
orderId: string
|
orderId: string;
|
||||||
customerName: string
|
customerName: string;
|
||||||
orderDate: string
|
orderDate: string;
|
||||||
totalAmount: string
|
totalAmount: string;
|
||||||
products: Array<{
|
products: Array<{
|
||||||
orderItemId: number
|
orderItemId: number;
|
||||||
productId: number
|
productId: number;
|
||||||
productName: string
|
productName: string;
|
||||||
quantity: number
|
quantity: number;
|
||||||
unit: string
|
unit: string;
|
||||||
is_packaged: boolean
|
is_packaged: boolean;
|
||||||
is_package_verified: boolean
|
is_package_verified: boolean;
|
||||||
}>
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeliverySlot {
|
interface DeliverySlot {
|
||||||
id: number
|
id: number;
|
||||||
deliveryTime: string
|
deliveryTime: string;
|
||||||
freezeTime: string
|
freezeTime: string;
|
||||||
deliverySequence: any
|
deliverySequence: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VendorOrderListRoute() {
|
export function VendorOrderListRoute() {
|
||||||
const { id } = useSearch({ from: '/vendor-order-list' })
|
const { id } = useSearch({ from: "/vendor-order-list" });
|
||||||
|
|
||||||
// Fetch snippet info
|
// Fetch snippet info
|
||||||
const { data: snippetInfo, isLoading: isLoadingSnippet } = id
|
const { data: snippetInfo, isLoading: isLoadingSnippet } = id
|
||||||
? trpc.admin.vendorSnippets.getOrdersBySnippet.useQuery({ snippetCode: id })
|
? trpc.admin.vendorSnippets.getOrdersBySnippet.useQuery({ snippetCode: id })
|
||||||
: { data: null, isLoading: false }
|
: { data: null, isLoading: false };
|
||||||
const snippet = snippetInfo?.snippet
|
const snippet = snippetInfo?.snippet;
|
||||||
const isPermanent = snippet?.isPermanent
|
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
|
// State for selected slot
|
||||||
const [selectedSlotId, setSelectedSlotId] = useState<number | null>(null)
|
const [selectedSlotId, setSelectedSlotId] = useState<number | null>(null);
|
||||||
|
|
||||||
// Auto-select first slot when snippets are loaded and isPermanent is true
|
// Auto-select first slot when snippets are loaded and isPermanent is true
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPermanent && upcomingSlots?.data && upcomingSlots.data.length > 0 && !selectedSlotId) {
|
if (
|
||||||
setSelectedSlotId(upcomingSlots.data[0].id)
|
isPermanent &&
|
||||||
|
upcomingSlots?.data &&
|
||||||
|
upcomingSlots.data.length > 0 &&
|
||||||
|
!selectedSlotId
|
||||||
|
) {
|
||||||
|
setSelectedSlotId(upcomingSlots.data[0].id);
|
||||||
}
|
}
|
||||||
}, [isPermanent, upcomingSlots, selectedSlotId])
|
}, [isPermanent, upcomingSlots, selectedSlotId]);
|
||||||
|
|
||||||
// Fetch orders based on mode
|
// 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! },
|
{ snippetCode: id!, slotId: selectedSlotId! },
|
||||||
{ enabled: !!id && !!selectedSlotId && isPermanent }
|
{ enabled: !!id && !!selectedSlotId && isPermanent },
|
||||||
)
|
);
|
||||||
|
|
||||||
const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.useQuery(
|
const { data: regularOrders } =
|
||||||
|
trpc.admin.vendorSnippets.getOrdersBySnippet.useQuery(
|
||||||
{ snippetCode: id! },
|
{ snippetCode: id! },
|
||||||
{ enabled: !!id && !isPermanent }
|
{ enabled: !!id && !isPermanent },
|
||||||
)
|
);
|
||||||
|
|
||||||
const orders = slotOrdersData?.data || regularOrders?.data || []
|
const orders = slotOrdersData?.data || regularOrders?.data || [];
|
||||||
const isLoadingCurrent = isPermanent ? isLoadingOrders : isLoadingSnippet
|
const isLoadingCurrent = isPermanent ? isLoadingOrders : isLoadingSnippet;
|
||||||
|
|
||||||
const updatePackagingMutation = trpc.admin.vendorSnippets.updateOrderItemPackaging.useMutation()
|
const updatePackagingMutation =
|
||||||
|
trpc.admin.vendorSnippets.updateOrderItemPackaging.useMutation();
|
||||||
|
|
||||||
const [updatingItems, setUpdatingItems] = useState<Set<number>>(new Set())
|
const [updatingItems, setUpdatingItems] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
const handlePackagingToggle = async (orderItemId: number, currentValue: boolean) => {
|
const handlePackagingToggle = async (
|
||||||
setUpdatingItems(prev => new Set(prev).add(orderItemId))
|
orderItemId: number,
|
||||||
|
currentValue: boolean,
|
||||||
|
) => {
|
||||||
|
setUpdatingItems((prev) => new Set(prev).add(orderItemId));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updatePackagingMutation.mutateAsync({
|
await updatePackagingMutation.mutateAsync({
|
||||||
orderItemId,
|
orderItemId,
|
||||||
is_packaged: !currentValue
|
is_packaged: !currentValue,
|
||||||
})
|
});
|
||||||
// Refetch data to update the UI
|
// Refetch data to update the UI
|
||||||
refetch()
|
refetch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update packaging status:', error)
|
console.error("Failed to update packaging status:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setUpdatingItems(prev => {
|
setUpdatingItems((prev) => {
|
||||||
const newSet = new Set(prev)
|
const newSet = new Set(prev);
|
||||||
newSet.delete(orderItemId)
|
newSet.delete(orderItemId);
|
||||||
return newSet
|
return newSet;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
const productSummary = useMemo(() => {
|
const productSummary = useMemo(() => {
|
||||||
const summary: Record<string, { quantity: number; unit: string }> = {};
|
const summary: Record<string, { quantity: number; unit: string }> = {};
|
||||||
|
|
||||||
orders.forEach(order => {
|
orders.forEach((order) => {
|
||||||
order.products.forEach(product => {
|
order.products.forEach((product) => {
|
||||||
const key = product.productName;
|
const key = product.productName;
|
||||||
if (!summary[key]) {
|
if (!summary[key]) {
|
||||||
summary[key] = { quantity: 0, unit: product.unit };
|
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]) => ({
|
return Object.entries(summary).map(([name, data]) => ({
|
||||||
name,
|
name,
|
||||||
quantity: data.quantity,
|
quantity: data.quantity,
|
||||||
unit: data.unit
|
unit: data.unit,
|
||||||
}));
|
}));
|
||||||
}, [orders])
|
}, [orders]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
|
|
@ -118,7 +137,9 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use
|
||||||
{productSummary.map((item, index) => (
|
{productSummary.map((item, index) => (
|
||||||
<div key={index} className="flex justify-between text-sm">
|
<div key={index} className="flex justify-between text-sm">
|
||||||
<span className="text-slate-600">{item.name}:</span>
|
<span className="text-slate-600">{item.name}:</span>
|
||||||
<span className="font-medium text-slate-900">{item.quantity} {item.unit}</span>
|
<span className="font-medium text-slate-900">
|
||||||
|
{item.quantity} {item.unit}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -127,7 +148,9 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use
|
||||||
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
<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 className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold text-slate-900">Vendor Orders</h2>
|
<h2 className="text-xl font-semibold text-slate-900">
|
||||||
|
Vendor Orders
|
||||||
|
</h2>
|
||||||
<p className="text-sm text-slate-500">
|
<p className="text-sm text-slate-500">
|
||||||
Track incoming orders and fulfilment progress for vendor partners.
|
Track incoming orders and fulfilment progress for vendor partners.
|
||||||
{id && <span className="block mt-1 text-xs">Snippet: {id}</span>}
|
{id && <span className="block mt-1 text-xs">Snippet: {id}</span>}
|
||||||
|
|
@ -135,18 +158,21 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use
|
||||||
</div>
|
</div>
|
||||||
{isPermanent && upcomingSlots?.data && (
|
{isPermanent && upcomingSlots?.data && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label htmlFor="slot-select" className="text-sm font-medium text-slate-700">
|
<label
|
||||||
|
htmlFor="slot-select"
|
||||||
|
className="text-sm font-medium text-slate-700"
|
||||||
|
>
|
||||||
Select Slot:
|
Select Slot:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="slot-select"
|
id="slot-select"
|
||||||
value={selectedSlotId || ''}
|
value={selectedSlotId || ""}
|
||||||
onChange={(e) => setSelectedSlotId(Number(e.target.value))}
|
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"
|
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) => (
|
{upcomingSlots.data.map((slot) => (
|
||||||
<option key={slot.id} value={slot.id}>
|
<option key={slot.id} value={slot.id}>
|
||||||
{dayjs(slot.deliveryTime).format('ddd, MMM DD hh:mm A')}
|
{dayjs(slot.deliveryTime).format("ddd, MMM DD hh:mm A")}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -155,11 +181,11 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void refetch()
|
void refetch();
|
||||||
}}
|
}}
|
||||||
disabled={isFetching}
|
disabled={isFetching}
|
||||||
>
|
>
|
||||||
{isFetching ? 'Refreshing…' : 'Refresh'}
|
{isFetching ? "Refreshing…" : "Refresh"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -170,7 +196,7 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="rounded-xl border border-red-200 bg-red-50 p-6 text-sm text-red-600">
|
<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'}
|
{error.message ?? "Unable to load vendor orders right now"}
|
||||||
</div>
|
</div>
|
||||||
) : !id ? (
|
) : !id ? (
|
||||||
<div className="rounded-xl border border-red-200 bg-red-50 p-6 text-sm text-red-600">
|
<div className="rounded-xl border border-red-200 bg-red-50 p-6 text-sm text-red-600">
|
||||||
|
|
@ -184,9 +210,10 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{orders.map((order) => {
|
{orders.map((order) => {
|
||||||
const parsedDate = order.orderDate
|
const parsedDate = order.orderDate
|
||||||
? dayjs(order.orderDate).format('ddd, MMM DD hh:mm A')
|
? dayjs(order.orderDate).format("ddd, MMM DD hh:mm A")
|
||||||
: 'N/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'
|
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 (
|
return (
|
||||||
<article
|
<article
|
||||||
|
|
@ -197,9 +224,7 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use
|
||||||
<h3 className="text-base font-semibold text-slate-900">
|
<h3 className="text-base font-semibold text-slate-900">
|
||||||
{order.orderId}
|
{order.orderId}
|
||||||
</h3>
|
</h3>
|
||||||
<span className={badgeClass}>
|
<span className={badgeClass}>Pending</span>
|
||||||
Pending
|
|
||||||
</span>
|
|
||||||
</header>
|
</header>
|
||||||
<dl className="grid gap-3 text-sm text-slate-600">
|
<dl className="grid gap-3 text-sm text-slate-600">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -208,28 +233,43 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="space-y-2">
|
<dd className="space-y-2">
|
||||||
{order.products.map((product) => (
|
{order.products.map((product) => (
|
||||||
<div key={product.orderItemId} className="flex items-center gap-3">
|
<div
|
||||||
|
key={product.orderItemId}
|
||||||
|
className="flex items-center gap-3"
|
||||||
|
>
|
||||||
<span className="text-sm font-medium text-slate-900 flex-1">
|
<span className="text-sm font-medium text-slate-900 flex-1">
|
||||||
{product.productName}: {product.quantity} {product.unit}
|
{product.productName}: {product.productSize * product.quantity}{" "}
|
||||||
|
{product.unit}
|
||||||
</span>
|
</span>
|
||||||
<label
|
<label
|
||||||
htmlFor={`package-${product.orderItemId}`}
|
htmlFor={`package-${product.orderItemId}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm font-medium",
|
"text-sm font-medium",
|
||||||
product.is_packaged ? "text-green-700" : "text-slate-600"
|
product.is_packaged
|
||||||
|
? "text-green-700"
|
||||||
|
: "text-slate-600",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Packaged
|
Packaged
|
||||||
{updatingItems.has(product.orderItemId) && (
|
{updatingItems.has(product.orderItemId) && (
|
||||||
<span className="ml-2 text-xs text-blue-500">(updating...)</span>
|
<span className="ml-2 text-xs text-blue-500">
|
||||||
|
(updating...)
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={`package-${product.orderItemId}`}
|
id={`package-${product.orderItemId}`}
|
||||||
checked={product.is_packaged}
|
checked={product.is_packaged}
|
||||||
disabled={updatingItems.has(product.orderItemId)}
|
disabled={updatingItems.has(
|
||||||
onChange={() => handlePackagingToggle(product.orderItemId, product.is_packaged)}
|
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"
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -240,7 +280,9 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use
|
||||||
<dt className="text-xs uppercase tracking-wide text-slate-400">
|
<dt className="text-xs uppercase tracking-wide text-slate-400">
|
||||||
Date
|
Date
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="font-medium text-slate-900">{parsedDate}</dd>
|
<dd className="font-medium text-slate-900">
|
||||||
|
{parsedDate}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<dt className="text-xs uppercase tracking-wide text-slate-400">
|
<dt className="text-xs uppercase tracking-wide text-slate-400">
|
||||||
|
|
@ -252,7 +294,7 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</article>
|
</article>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -263,13 +305,5 @@ const { data: regularOrders } = trpc.admin.vendorSnippets.getOrdersBySnippet.use
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const statusBadgeStyles: Record<string, string> = {
|
|
||||||
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'
|
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
||||||
import FontAwesome5 from "@expo/vector-icons/FontAwesome5";
|
import FontAwesome5 from "@expo/vector-icons/FontAwesome5";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import ProductCard from "@/components/ProductCard";
|
import ProductCard from "@/components/ProductCard";
|
||||||
|
import MyFlatList from "common-ui/src/components/flat-list";
|
||||||
|
|
||||||
import { trpc } from "@/src/trpc-client";
|
import { trpc } from "@/src/trpc-client";
|
||||||
import { useGetCart } from "@/hooks/cart-query-hooks";
|
import { useGetCart } from "@/hooks/cart-query-hooks";
|
||||||
|
|
@ -38,6 +39,7 @@ dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const { width: screenWidth } = Dimensions.get("window");
|
const { width: screenWidth } = Dimensions.get("window");
|
||||||
const itemWidth = screenWidth * 0.45; // 45% of screen width
|
const itemWidth = screenWidth * 0.45; // 45% of screen width
|
||||||
|
const gridItemWidth = (screenWidth * 0.9) / 2; // Half of screen width minus padding
|
||||||
|
|
||||||
const RenderStore = ({
|
const RenderStore = ({
|
||||||
item,
|
item,
|
||||||
|
|
@ -115,6 +117,7 @@ export default function Dashboard() {
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
const { backgroundColor } = useStatusBarStore();
|
const { backgroundColor } = useStatusBarStore();
|
||||||
|
const { getQuickestSlot } = useProductSlotIdentifier();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: productsData,
|
data: productsData,
|
||||||
|
|
@ -142,37 +145,41 @@ export default function Dashboard() {
|
||||||
|
|
||||||
setIsLoadingMore(true);
|
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 batchSize = 10;
|
||||||
const startIndex = page * batchSize;
|
const startIndex = page * batchSize;
|
||||||
const endIndex = startIndex + batchSize;
|
const endIndex = startIndex + batchSize;
|
||||||
|
|
||||||
// Get the next batch of products
|
// Get the next batch of products
|
||||||
const nextBatch = products.slice(startIndex, endIndex);
|
const nextBatchRaw = products.slice(startIndex, endIndex);
|
||||||
|
|
||||||
const oldBatchIds = displayedProducts.map(p => p.id)
|
|
||||||
const nextBatchIds = nextBatch.map(p => p.id)
|
|
||||||
const productIds = products.map(p => p.id);
|
|
||||||
|
|
||||||
|
// Filter products to only include those with available slots
|
||||||
|
const nextBatch = nextBatchRaw.filter(product => {
|
||||||
|
const slot = getQuickestSlot(product.id);
|
||||||
|
return slot !== null && slot !== undefined;
|
||||||
|
});
|
||||||
|
|
||||||
if (nextBatch.length > 0) {
|
if (nextBatch.length > 0) {
|
||||||
setDisplayedProducts(prev => [...prev, ...nextBatch]);
|
setDisplayedProducts(prev => [...prev, ...nextBatch]);
|
||||||
setPage(prev => prev + 1);
|
setPage(prev => page + 1);
|
||||||
setHasMore(endIndex < products.length);
|
setHasMore(endIndex < products.length);
|
||||||
} else {
|
} else {
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoadingMore(false);
|
setIsLoadingMore(false);
|
||||||
// }, 500); // Simulate network delay
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize with the first batch of products
|
// Initialize with the first batch of products (only those with available slots)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (products.length > 0 && displayedProducts.length === 0) {
|
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);
|
setDisplayedProducts(initialBatch);
|
||||||
setHasMore(products.length > 10);
|
setHasMore(products.length > 10);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
|
|
@ -359,7 +366,6 @@ export default function Dashboard() {
|
||||||
<BannerCarousel />
|
<BannerCarousel />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TestingPhaseNote />
|
|
||||||
|
|
||||||
{/* White Section */}
|
{/* White Section */}
|
||||||
<View
|
<View
|
||||||
|
|
@ -587,13 +593,18 @@ export default function Dashboard() {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Product Grid */}
|
{/* Product List */}
|
||||||
<View style={tw`flex-row flex-wrap`}>
|
<MyFlatList
|
||||||
{displayedProducts.map((item, index: number) => (
|
data={displayedProducts}
|
||||||
|
keyExtractor={(item) => item.id.toString()}
|
||||||
|
numColumns={2}
|
||||||
|
contentContainerStyle={tw`pb-8`}
|
||||||
|
columnWrapperStyle={tw`py-2`}
|
||||||
|
renderItem={({ item, index }) => (
|
||||||
|
<View style={tw`ml-1`}>
|
||||||
<ProductCard
|
<ProductCard
|
||||||
item={item}
|
item={item}
|
||||||
itemWidth={(screenWidth * 0.9) / 2} // Half of screen width minus padding
|
itemWidth={gridItemWidth}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
router.push(
|
router.push(
|
||||||
`/(drawer)/(tabs)/home/product-detail/${item.id}`
|
`/(drawer)/(tabs)/home/product-detail/${item.id}`
|
||||||
|
|
@ -601,18 +612,30 @@ export default function Dashboard() {
|
||||||
}
|
}
|
||||||
showDeliveryInfo={true}
|
showDeliveryInfo={true}
|
||||||
miniView={false}
|
miniView={false}
|
||||||
nullIfNotAvailable={true}
|
// nullIfNotAvailable={true}
|
||||||
containerComp={({children}) => <View key={item.id} style={tw`w-1/2 pr-2 pb-4`}>{children}</View>}
|
|
||||||
key={item.id}
|
key={item.id}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
{isLoadingMore && (
|
onEndReached={() => {
|
||||||
|
if (!isLoadingMore && hasMore) {
|
||||||
|
loadMoreProducts();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
ListFooterComponent={
|
||||||
|
isLoadingMore ? (
|
||||||
<View style={tw`items-center py-4`}>
|
<View style={tw`items-center py-4`}>
|
||||||
<MyText>Loading more...</MyText>
|
<MyText>Loading more...</MyText>
|
||||||
</View>
|
</View>
|
||||||
)}
|
) : null
|
||||||
|
}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<View style={tw`items-center py-8`}>
|
||||||
|
<MyText style={tw`text-gray-500`}>No products available</MyText>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -825,8 +825,6 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TestingPhaseNote />
|
|
||||||
|
|
||||||
{/* Bottom Checkout Bar - Now Static */}
|
{/* Bottom Checkout Bar - Now Static */}
|
||||||
{hasAvailableItems && (
|
{hasAvailableItems && (
|
||||||
<View style={tw`bg-white mt-4 rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6`}>
|
<View style={tw`bg-white mt-4 rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6`}>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue