Compare commits

..

No commits in common. "2929e7725a444f2dafae2380a3ddfb23882de4fe" and "cd5905d40fd05065f4d9c0169f47bd5697b50863" have entirely different histories.

15 changed files with 85 additions and 232 deletions

View file

@ -19,7 +19,7 @@ import { seed } from 'src/db/seed';
import './src/jobs/jobs-index';
import { startAutomatedJobs } from './src/lib/automatedJobs';
// seed()
seed()
initFunc()
startAutomatedJobs()

View file

@ -24,12 +24,6 @@ interface SlotWithProducts {
}>;
}
interface SlotInfo {
id: number;
deliveryTime: Date;
freezeTime: Date;
}
export async function initializeSlotStore(): Promise<void> {
try {
console.log('Initializing slot store in Redis...');
@ -88,28 +82,6 @@ export async function initializeSlotStore(): Promise<void> {
await redisClient.set(`slot:${slot.id}`, JSON.stringify(slot));
}
// Build and store product-slots map
// Group slots by productId
const productSlotsMap: Record<number, SlotInfo[]> = {};
for (const slot of slotsWithProducts) {
for (const product of slot.products) {
if (!productSlotsMap[product.id]) {
productSlotsMap[product.id] = [];
}
productSlotsMap[product.id].push({
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
});
}
}
// Store each product's slots in Redis with key pattern "product:{id}:slots"
for (const [productId, slotInfos] of Object.entries(productSlotsMap)) {
await redisClient.set(`product:${productId}:slots`, JSON.stringify(slotInfos));
}
console.log('Slot store initialized successfully');
} catch (error) {
console.error('Error initializing slot store:', error);
@ -151,70 +123,3 @@ export async function getAllSlots(): Promise<SlotWithProducts[]> {
return [];
}
}
export async function getProductSlots(productId: number): Promise<SlotInfo[]> {
try {
const key = `product:${productId}:slots`;
const data = await redisClient.get(key);
if (!data) return [];
return JSON.parse(data) as SlotInfo[];
} catch (error) {
console.error(`Error getting slots for product ${productId}:`, error);
return [];
}
}
export async function getAllProductsSlots(): Promise<Record<number, SlotInfo[]>> {
try {
// Get all keys matching the pattern "product:*:slots"
const keys = await redisClient.KEYS('product:*:slots');
if (keys.length === 0) return {};
// Get all product slots using MGET for better performance
const productsData = await redisClient.MGET(keys);
const result: Record<number, SlotInfo[]> = {};
for (const key of keys) {
// Extract productId from key "product:{id}:slots"
const match = key.match(/product:(\d+):slots/);
if (match) {
const productId = parseInt(match[1], 10);
const dataIndex = keys.indexOf(key);
if (productsData[dataIndex]) {
result[productId] = JSON.parse(productsData[dataIndex]) as SlotInfo[];
}
}
}
return result;
} catch (error) {
console.error('Error getting all products slots:', error);
return {};
}
}
export async function getMultipleProductsSlots(productIds: number[]): Promise<Record<number, SlotInfo[]>> {
try {
if (productIds.length === 0) return {};
// Build keys for all productIds
const keys = productIds.map(id => `product:${id}:slots`);
// Use MGET for batch retrieval
const productsData = await redisClient.MGET(keys);
const result: Record<number, SlotInfo[]> = {};
for (let i = 0; i < productIds.length; i++) {
const data = productsData[i];
if (data) {
result[productIds[i]] = JSON.parse(data) as SlotInfo[];
}
}
return result;
} catch (error) {
console.error('Error getting products slots:', error);
return {};
}
}

View file

@ -1,8 +1,8 @@
import { router, publicProcedure, protectedProcedure } from '../trpc-index';
import { z } from 'zod';
import { db } from '../../db/db_index';
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, users, orderStatus } from '../../db/schema';
import { eq, and, inArray, isNotNull, gt, sql, asc, ne } from 'drizzle-orm';
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, users } from '../../db/schema';
import { eq, and, inArray, isNotNull, gt, sql, asc } from 'drizzle-orm';
import { appUrl } from '../../lib/env-exporter';
const createSnippetSchema = z.object({
@ -225,7 +225,10 @@ export const vendorSnippetsRouter = router({
// Query orders that match the snippet criteria
const matchingOrders = await db.query.orders.findMany({
where: eq(orders.slotId, snippet.slotId!),
where: and(
eq(orders.slotId, snippet.slotId!),
// We'll filter by products in the application logic
),
with: {
orderItems: {
with: {
@ -236,7 +239,6 @@ export const vendorSnippetsRouter = router({
},
},
},
orderStatus: true,
user: true,
slot: true,
},
@ -245,8 +247,6 @@ export const vendorSnippetsRouter = router({
// Filter orders that contain at least one of the snippet's products
const filteredOrders = matchingOrders.filter(order => {
const status = order.orderStatus;
if (status[0].isCancelled) return false;
const orderProductIds = order.orderItems.map(item => item.productId);
return snippet.productIds.some(productId => orderProductIds.includes(productId));
});
@ -271,13 +271,11 @@ export const vendorSnippetsRouter = router({
is_package_verified: item.is_package_verified,
}));
const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0);
return {
orderId: `ORD${order.readableId.toString().padStart(3, '0')}`,
orderDate: order.createdAt.toISOString(),
customerName: order.user.name,
totalAmount: orderTotal,
totalAmount: order.totalAmount,
slotInfo: order.slot ? {
time: order.slot.deliveryTime.toISOString(),
sequence: order.slot.deliverySequence,
@ -395,7 +393,6 @@ export const vendorSnippetsRouter = router({
},
},
},
orderStatus: true,
user: true,
slot: true,
},
@ -404,8 +401,6 @@ export const vendorSnippetsRouter = router({
// Filter orders that contain at least one of the snippet's products
const filteredOrders = matchingOrders.filter(order => {
const status = order.orderStatus;
if (status[0]?.isCancelled) return false;
const orderProductIds = order.orderItems.map(item => item.productId);
return snippet.productIds.some(productId => orderProductIds.includes(productId));
});
@ -430,13 +425,11 @@ export const vendorSnippetsRouter = router({
is_package_verified: item.is_package_verified,
}));
const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0);
return {
orderId: `ORD${order.readableId.toString().padStart(3, '0')}`,
orderDate: order.createdAt.toISOString(),
customerName: order.user.name,
totalAmount: orderTotal,
totalAmount: order.totalAmount,
slotInfo: order.slot ? {
time: order.slot.deliveryTime.toISOString(),
sequence: order.slot.deliverySequence,

View file

@ -5,7 +5,6 @@ import { cartItems, productInfo, units, productSlots, deliverySlotInfo } from '.
import { eq, and, sql, inArray, gt } from 'drizzle-orm';
import { ApiError } from '../../lib/api-error';
import { generateSignedUrlsFromS3Urls } from '../../lib/s3-client';
import { getProductSlots, getMultipleProductsSlots } from '../../stores/slot-store';
interface CartResponse {
items: any[];
@ -182,52 +181,6 @@ export const cartRouter = router({
};
}),
// Original DB-based getCartSlots (commented out)
// getCartSlots: publicProcedure
// .input(z.object({
// productIds: z.array(z.number().int().positive())
// }))
// .query(async ({ input }) => {
// const { productIds } = input;
//
// if (productIds.length === 0) {
// return {};
// }
//
// // Get slots for these products where freeze time is after current time
// const slotsData = await db
// .select({
// productId: productSlots.productId,
// slotId: deliverySlotInfo.id,
// deliveryTime: deliverySlotInfo.deliveryTime,
// freezeTime: deliverySlotInfo.freezeTime,
// isActive: deliverySlotInfo.isActive,
// })
// .from(productSlots)
// .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
// .where(and(
// inArray(productSlots.productId, productIds),
// gt(deliverySlotInfo.freezeTime, sql`NOW()`),
// eq(deliverySlotInfo.isActive, true)
// ));
//
// // Group by productId
// const result: Record<number, any[]> = {};
// slotsData.forEach(slot => {
// if (!result[slot.productId]) {
// result[slot.productId] = [];
// }
// result[slot.productId].push({
// id: slot.slotId,
// deliveryTime: slot.deliveryTime,
// freezeTime: slot.freezeTime,
// });
// });
//
// return result;
// }),
// Cache-based getCartSlots
getCartSlots: publicProcedure
.input(z.object({
productIds: z.array(z.number().int().positive())
@ -239,6 +192,36 @@ export const cartRouter = router({
return {};
}
return await getMultipleProductsSlots(productIds);
// Get slots for these products where freeze time is after current time
const slotsData = await db
.select({
productId: productSlots.productId,
slotId: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime,
isActive: deliverySlotInfo.isActive,
})
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(and(
inArray(productSlots.productId, productIds),
gt(deliverySlotInfo.freezeTime, sql`NOW()`),
eq(deliverySlotInfo.isActive, true)
));
// Group by productId
const result: Record<number, any[]> = {};
slotsData.forEach(slot => {
if (!result[slot.productId]) {
result[slot.productId] = [];
}
result[slot.productId].push({
id: slot.slotId,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
});
});
return result;
}),
});

View file

@ -712,7 +712,7 @@ export const orderRouter = router({
.input(
z.object({
// id: z.string().regex(/^ORD\d+$/, "Invalid order ID format"),
id: z.number(),
id: z.string(),
reason: z.string().min(1, "Cancellation reason is required"),
})
)
@ -723,9 +723,6 @@ export const orderRouter = router({
const readableId = Number(id);
console.log({id, reason})
// Check if order exists and belongs to user
const order = await db.query.orders.findFirst({
where: eq(orders.id, Number(id)),

View file

@ -34,7 +34,6 @@ import { useUserDetails } from "@/src/contexts/AuthContext";
import TabLayoutWrapper from "@/components/TabLayoutWrapper";
import { useNavigationStore } from "@/src/store/navigationStore";
import { useGetEssentialConsts } from "@/src/api-hooks/essential-consts.api";
import NextOrderGlimpse from "@/components/NextOrderGlimpse";
dayjs.extend(relativeTime);
// import { StatusBar } from "expo-status-bar";
@ -363,10 +362,6 @@ export default function Dashboard() {
setWhiteSectionLayout({ y });
}}
>
<View style={tw`py-2`}>
<NextOrderGlimpse />
</View>
{/* Section Title */}
<View style={tw`mb-4 pt-2 px-1`}>
<MyText style={tw`text-2xl font-extrabold text-gray-900 tracking-tight`}>
@ -496,7 +491,7 @@ export default function Dashboard() {
<MyText
style={tw`text-[11px] font-bold text-slate-500`}
>
{dayjs(slot.deliveryTime).format("ddd, MMM DD")}
Delivery Slot
</MyText>
</View>

View file

@ -1,28 +1,16 @@
import React from 'react';
import { View, ScrollView } from 'react-native';
import { useRouter } from 'expo-router';
import { useFocusEffect } from '@react-navigation/native';
import { AppContainer, MyText, tw, MyTouchableOpacity, ProfileImage, colors } from 'common-ui';
import TabLayoutWrapper from '@/components/TabLayoutWrapper';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import NextOrderGlimpse from '@/components/NextOrderGlimpse';
import { useAuth } from '@/src/contexts/AuthContext';
import { useNavigationTarget } from 'common-ui/hooks/useNavigationTarget';
export default function Me() {
const router = useRouter();
const { logout } = useAuth();
const { getNavigationTarget } = useNavigationTarget();
useFocusEffect(() => {
const target = getNavigationTarget();
console.log({target})
if (target) {
router.replace(target as any);
}
});
const menuSections = [
{

View file

@ -50,8 +50,6 @@ export default function OrderDetails() {
Alert.alert('Success', 'Order cancelled successfully');
},
onError: (error: any) => {
console.log({error})
Alert.alert('Error', error.message || 'Failed to cancel order');
},
});
@ -61,7 +59,7 @@ export default function OrderDetails() {
Alert.alert('Error', 'Please enter a reason for cancellation');
return;
}
cancelOrderMutation.mutate({ id: order.id, reason: cancelReason });
cancelOrderMutation.mutate({ id: order.orderId, reason: cancelReason });
};
useEffect(() => {
@ -128,12 +126,6 @@ export default function OrderDetails() {
{/* Simple Header */}
<View style={tw`bg-white px-6 pt-4 pb-4 border-b border-slate-100 flex-row items-center justify-between`}>
<View style={tw`flex-row items-center`}>
<MyTouchableOpacity
onPress={() => router.back()}
style={tw`mr-3 p-2 -ml-2`}
>
<Ionicons name="chevron-back" size={24} color="#1E293B" />
</MyTouchableOpacity>
<View>
<MyText style={tw`text-slate-900 font-bold text-lg`}>Order #{order.orderId}</MyText>
<MyText style={tw`text-slate-400 text-xs`}>{dayjs(order.orderDate).format("DD MMM, h:mm A")}</MyText>
@ -350,10 +342,10 @@ export default function OrderDetails() {
{/* Footer Actions */}
<View style={tw`flex-row gap-3`}>
<MyTouchableOpacity
onPress={() => router.replace('/(drawer)/(tabs)/me')}
onPress={() => router.back()}
style={tw`flex-1 bg-slate-100 py-3.5 rounded-xl items-center`}
>
<MyText style={tw`text-slate-600 font-bold`}>Back</MyText>
<MyText style={tw`text-slate-600 font-bold`}>Dismiss</MyText>
</MyTouchableOpacity>
<MyTouchableOpacity
onPress={() => setComplaintDialogOpen(true)}

View file

@ -2,7 +2,6 @@ import React from 'react';
import { View, TouchableOpacity, ActivityIndicator } from 'react-native';
import { useRouter } from 'expo-router';
import { tw, MyText } from 'common-ui';
import { useNavigationTarget } from 'common-ui/hooks/useNavigationTarget';
import { MaterialIcons, Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { trpc } from '@/src/trpc-client';
@ -39,7 +38,6 @@ interface Order {
export default function NextOrderGlimpse() {
const router = useRouter();
const { setNavigationTarget } = useNavigationTarget();
const { data: ordersData, isLoading: ordersLoading } = trpc.user.order.getOrders.useQuery({
page: 1,
@ -99,12 +97,7 @@ export default function NextOrderGlimpse() {
return (
<TouchableOpacity
style={tw`px-6 mb-4`}
onPress={() => {
console.log('from next press')
setNavigationTarget(`/(drawer)/(tabs)/me/my-orders/${nextOrder.id}`);
router.replace('/(drawer)/(tabs)/me');
}}
onPress={() => router.push(`/(drawer)/(tabs)/me/my-orders/${nextOrder.id}`)}
>
<View style={tw`bg-gradient-to-r from-amber-50 to-orange-50 rounded-2xl p-4 border-2 border-amber-300`}>
<View style={tw`flex-row items-center justify-between mb-3`}>

View file

@ -161,6 +161,37 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
const router = useRouter();
// Process slots: flatten and unique
const availableSlots = React.useMemo(() => {
if (!slotsData) return [];
const allSlots = Object.values(slotsData).flat();
const uniqueSlots = allSlots.filter(
(slot, index, self) => index === self.findIndex((s) => s.id === slot.id)
);
// Smart time window formatting function
const formatTimeRange = (deliveryTime: string) => {
const time = dayjs(deliveryTime);
const endTime = time.add(1, 'hour');
const startPeriod = time.format('A');
const endPeriod = endTime.format('A');
let timeRange;
if (startPeriod === endPeriod) {
timeRange = `${time.format('h')}-${endTime.format('h')} ${startPeriod}`;
} else {
timeRange = `${time.format('h:mm')} ${startPeriod} - ${endTime.format('h:mm')} ${endPeriod}`;
}
return `${time.format('ddd, DD MMM ')}${timeRange}`;
};
return uniqueSlots.map((slot) => ({
label: `Delivery: ${formatTimeRange(slot.deliveryTime)} - Close time: ${dayjs(slot.freezeTime).format("h:mm a")}`,
value: slot.id,
}));
}, [slotsData]);
// Get available slots for a specific product
const getAvailableSlotsForProduct = React.useMemo(() => {
return (productId: number) => {
@ -299,6 +330,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
);
if (upcomingSlots.length > 0) {
console.log({upcomingSlots, existingSlotId, cartSlotIds})
if (existingSlotId) {
const slotStillValid = upcomingSlots.some(slot => slot.id === existingSlotId);

View file

@ -19,6 +19,8 @@ export function useAuthenticatedRoute(options: AuthenticatedRouteOptions = {}) {
const { isAuthenticated, isLoading } = useAuth();
const router = useRouter();
console.log({ops: options.queryParams})
useFocusEffect(() => {
// Don't redirect while auth is loading
if (isLoading) return;

3
package-lock.json generated
View file

@ -24388,8 +24388,7 @@
"react-native-web": "~0.20.0",
"react-native-webview": "13.13.5",
"twrnc": "^4.9.1",
"yup": "^1.7.0",
"zustand": "^5.0.10"
"yup": "^1.7.0"
}
},
"packages/ui/node_modules/buffer": {

View file

@ -1,25 +0,0 @@
import { create } from 'zustand';
interface NavigationTargetState {
navigationTarget: string | null;
setNavigationTarget: (target: string | null) => void;
getNavigationTarget: () => string | null;
}
const useNavigationTargetStore = create<NavigationTargetState>((set, get) => ({
navigationTarget: null,
setNavigationTarget: (target: string | null) => {
set({ navigationTarget: target })
},
getNavigationTarget: () => {
const target = get().navigationTarget;
if (target) {
set({ navigationTarget: null });
}
return target;
},
}));
export function useNavigationTarget() {
return useNavigationTargetStore();
}

View file

@ -64,8 +64,8 @@ const isDevMode = Constants.executionEnvironment !== "standalone";
// const BASE_API_URL = 'http://10.0.2.2:4000';
// const BASE_API_URL = 'http://192.168.100.101:4000';
// const BASE_API_URL = 'http://192.168.1.14:4000';
// let BASE_API_URL = "https://mf.freshyo.in";
let BASE_API_URL = 'http://192.168.100.103:4000';
let BASE_API_URL = "https://mf.freshyo.in";
// let BASE_API_URL = 'http://192.168.100.103:4000';
// let BASE_API_URL = 'http://192.168.29.219:4000';
// if(isDevMode) {

View file

@ -57,7 +57,6 @@
"react-native-web": "~0.20.0",
"react-native-webview": "13.13.5",
"twrnc": "^4.9.1",
"yup": "^1.7.0",
"zustand": "^5.0.10"
"yup": "^1.7.0"
}
}