Compare commits

..

No commits in common. "903c5c27bcccd46e6e44be7e0819af9d24b271f5" and "44318541f838b8a705498df5739c8d37a8e2af40" have entirely different histories.

25 changed files with 368 additions and 829 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,6 @@
import roleManager from './roles-manager';
import './notif-job';
import { initializeAllStores } from '../stores/store-initializer';
import { computeConstants } from './const-store';
/**
* Initialize all application services
@ -11,13 +12,18 @@ import { initializeAllStores } from '../stores/store-initializer';
export const initFunc = async (): Promise<void> => {
try {
console.log('Starting application initialization...');
// Initialize role manager
await roleManager.fetchRoles();
console.log('Role manager initialized successfully');
// Initialize all stores
await initializeAllStores();
// Compute and store constants in Redis
await computeConstants();
console.log('Const store initialized successfully');
// Notification queue and worker are initialized via import
console.log('Notification queue and worker initialized');
console.log('Application initialization completed successfully');
} catch (error) {
console.error('Application initialization failed:', error);

View file

@ -63,14 +63,6 @@ class RedisClient {
return await this.client.lPush(key, value);
}
async KEYS(pattern: string): Promise<string[]> {
return await this.client.KEYS(pattern);
}
async MGET(keys: string[]): Promise<(string | null)[]> {
return await this.client.MGET(keys);
}
disconnect(): void {
if (this.isConnected) {
this.client.disconnect();

View file

@ -1,88 +0,0 @@
// import redisClient from './redis-client';
import redisClient from 'src/lib/redis-client';
import { db } from '../db/db_index';
import { homeBanners } from '../db/schema';
import { isNotNull, asc } from 'drizzle-orm';
import { generateSignedUrlFromS3Url } from 'src/lib/s3-client';
// Banner Type (matches getBanners return)
interface Banner {
id: number;
name: string;
imageUrl: string | null;
serialNum: number | null;
productIds: number[] | null;
createdAt: Date;
// updatedAt: Date;
}
export async function initializeBannerStore(): Promise<void> {
try {
console.log('Initializing banner store in Redis...');
const banners = await db.query.homeBanners.findMany({
where: isNotNull(homeBanners.serialNum), // Only show assigned banners
orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4
});
// Store each banner in Redis
for (const banner of banners) {
const signedImageUrl = banner.imageUrl ? await generateSignedUrlFromS3Url(banner.imageUrl) : banner.imageUrl;
const bannerObj: Banner = {
id: banner.id,
name: banner.name,
imageUrl: signedImageUrl,
serialNum: banner.serialNum,
productIds: banner.productIds,
createdAt: banner.createdAt,
// updatedAt: banner.updatedAt,
};
await redisClient.set(`banner:${banner.id}`, JSON.stringify(bannerObj));
}
console.log('Banner store initialized successfully');
} catch (error) {
console.error('Error initializing banner store:', error);
}
}
export async function getBannerById(id: number): Promise<Banner | null> {
try {
const key = `banner:${id}`;
const data = await redisClient.get(key);
if (!data) return null;
return JSON.parse(data) as Banner;
} catch (error) {
console.error(`Error getting banner ${id}:`, error);
return null;
}
}
export async function getAllBanners(): Promise<Banner[]> {
try {
// Get all keys matching the pattern "banner:*"
const keys = await redisClient.KEYS('banner:*');
if (keys.length === 0) return [];
// Get all banners using MGET for better performance
const bannersData = await redisClient.MGET(keys);
const banners: Banner[] = [];
for (const bannerData of bannersData) {
if (bannerData) {
banners.push(JSON.parse(bannerData) as Banner);
}
}
// Sort by serialNum to maintain the same order as the original query
banners.sort((a, b) => (a.serialNum || 0) - (b.serialNum || 0));
return banners;
} catch (error) {
console.error('Error getting all banners:', error);
return [];
}
}

View file

@ -1,168 +0,0 @@
// import redisClient from './redis-client';
import redisClient from 'src/lib/redis-client';
import { db } from '../db/db_index';
import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, storeInfo } from '../db/schema';
import { eq, and, gt, sql } from 'drizzle-orm';
import { generateSignedUrlsFromS3Urls } from 'src/lib/s3-client';
// Uniform Product Type (matches getProductDetails return)
interface Product {
id: number;
name: string;
shortDescription: string | null;
longDescription: string | null;
price: string;
marketPrice: string | null;
unitNotation: string;
images: string[];
isOutOfStock: boolean;
store: { id: number; name: string; description: string | null } | null;
incrementStep: number;
productQuantity: number;
isFlashAvailable: boolean;
flashPrice: string | null;
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date }>;
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>;
}
export async function initializeProducts(): Promise<void> {
try {
console.log('Initializing product store in Redis...');
// Fetch all products with full details (similar to productMega logic)
const productsData = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
longDescription: productInfo.longDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
storeId: productInfo.storeId,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
productQuantity: productInfo.productQuantity,
isFlashAvailable: productInfo.isFlashAvailable,
flashPrice: productInfo.flashPrice,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id));
// Fetch all stores
const allStores = await db.query.storeInfo.findMany({
columns: { id: true, name: true, description: true },
});
const storeMap = new Map(allStores.map(s => [s.id, s]));
// Fetch all delivery slots
const allDeliverySlots = await db
.select({
productId: productSlots.productId,
id: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime,
})
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
)
);
const deliverySlotsMap = new Map<number, typeof allDeliverySlots>();
for (const slot of allDeliverySlots) {
if (!deliverySlotsMap.has(slot.productId)) deliverySlotsMap.set(slot.productId, []);
deliverySlotsMap.get(slot.productId)!.push(slot);
}
// Fetch all special deals
const allSpecialDeals = await db
.select({
productId: specialDeals.productId,
quantity: specialDeals.quantity,
price: specialDeals.price,
validTill: specialDeals.validTill,
})
.from(specialDeals)
.where(gt(specialDeals.validTill, sql`NOW()`));
const specialDealsMap = new Map<number, typeof allSpecialDeals>();
for (const deal of allSpecialDeals) {
if (!specialDealsMap.has(deal.productId)) specialDealsMap.set(deal.productId, []);
specialDealsMap.get(deal.productId)!.push(deal);
}
// Store each product in Redis
for (const product of productsData) {
const signedImages = await generateSignedUrlsFromS3Urls((product.images as string[]) || []);
const store = product.storeId ? storeMap.get(product.storeId) || null : null;
const deliverySlots = deliverySlotsMap.get(product.id) || [];
const specialDeals = specialDealsMap.get(product.id) || [];
const productObj: Product = {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
longDescription: product.longDescription,
price: product.price.toString(),
marketPrice: product.marketPrice?.toString() || null,
unitNotation: product.unitShortNotation,
images: signedImages,
isOutOfStock: product.isOutOfStock,
store: store ? { id: store.id, name: store.name, description: store.description } : null,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice?.toString() || null,
deliverySlots: deliverySlots.map(s => ({ id: s.id, deliveryTime: s.deliveryTime, freezeTime: s.freezeTime })),
specialDeals: specialDeals.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })),
};
await redisClient.set(`product:${product.id}`, JSON.stringify(productObj));
}
console.log('Product store initialized successfully');
} catch (error) {
console.error('Error initializing product store:', error);
}
}
export async function getProductById(id: number): Promise<Product | null> {
try {
const key = `product:${id}`;
const data = await redisClient.get(key);
if (!data) return null;
return JSON.parse(data) as Product;
} catch (error) {
console.error(`Error getting product ${id}:`, error);
return null;
}
}
export async function getAllProducts(): Promise<Product[]> {
try {
// Get all keys matching the pattern "product:*"
const keys = await redisClient.KEYS('product:*');
if (keys.length === 0) {
return [];
}
// Get all products using MGET for better performance
const productsData = await redisClient.MGET(keys);
const products: Product[] = [];
for (const productData of productsData) {
if (productData) {
products.push(JSON.parse(productData) as Product);
}
}
return products;
} catch (error) {
console.error('Error getting all products:', error);
return [];
}
}

View file

@ -1,115 +0,0 @@
// import redisClient from './redis-client';
import redisClient from 'src/lib/redis-client';
import { db } from '../db/db_index';
import { productTagInfo } from '../db/schema';
import { eq } from 'drizzle-orm';
import { generateSignedUrlFromS3Url } from 'src/lib/s3-client';
// Tag Type (matches getDashboardTags return)
interface Tag {
id: number;
tagName: string;
imageUrl: string | null;
isDashboardTag: boolean;
}
export async function initializeProductTagStore(): Promise<void> {
try {
console.log('Initializing product tag store in Redis...');
// Fetch all tags
const tagsData = await db
.select({
id: productTagInfo.id,
tagName: productTagInfo.tagName,
imageUrl: productTagInfo.imageUrl,
isDashboardTag: productTagInfo.isDashboardTag,
})
.from(productTagInfo);
// Store each tag in Redis
for (const tag of tagsData) {
const signedImageUrl = tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null;
const tagObj: Tag = {
id: tag.id,
tagName: tag.tagName,
imageUrl: signedImageUrl,
isDashboardTag: tag.isDashboardTag,
};
await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj));
}
console.log('Product tag store initialized successfully');
} catch (error) {
console.error('Error initializing product tag store:', error);
}
}
export async function getTagById(id: number): Promise<Tag | null> {
try {
const key = `tag:${id}`;
const data = await redisClient.get(key);
if (!data) return null;
return JSON.parse(data) as Tag;
} catch (error) {
console.error(`Error getting tag ${id}:`, error);
return null;
}
}
export async function getAllTags(): Promise<Tag[]> {
try {
// Get all keys matching the pattern "tag:*"
const keys = await redisClient.KEYS('tag:*');
if (keys.length === 0) {
return [];
}
// Get all tags using MGET for better performance
const tagsData = await redisClient.MGET(keys);
const tags: Tag[] = [];
for (const tagData of tagsData) {
if (tagData) {
tags.push(JSON.parse(tagData) as Tag);
}
}
return tags;
} catch (error) {
console.error('Error getting all tags:', error);
return [];
}
}
export async function getDashboardTags(): Promise<Tag[]> {
try {
// Get all keys matching the pattern "tag:*"
const keys = await redisClient.KEYS('tag:*');
if (keys.length === 0) {
return [];
}
// Get all tags using MGET for better performance
const tagsData = await redisClient.MGET(keys);
const dashboardTags: Tag[] = [];
for (const tagData of tagsData) {
if (tagData) {
const tag = JSON.parse(tagData) as Tag;
if (tag.isDashboardTag) {
dashboardTags.push(tag);
}
}
}
return dashboardTags;
} catch (error) {
console.error('Error getting dashboard tags:', error);
return [];
}
}

View file

@ -1,125 +0,0 @@
import redisClient from 'src/lib/redis-client';
import { db } from '../db/db_index';
import { deliverySlotInfo, productSlots, productInfo, units } from '../db/schema';
import { eq, and, gt, asc } from 'drizzle-orm';
import { generateSignedUrlsFromS3Urls } from 'src/lib/s3-client';
// Define the structure for slot with products
interface SlotWithProducts {
id: number;
deliveryTime: Date;
freezeTime: Date;
isActive: boolean;
products: Array<{
id: number;
name: string;
shortDescription: string | null;
price: string;
marketPrice: string | null;
unit: string | null;
images: string[];
isOutOfStock: boolean;
storeId: number | null;
nextDeliveryDate: Date;
}>;
}
export async function initializeSlotStore(): Promise<void> {
try {
console.log('Initializing slot store in Redis...');
const now = new Date();
// Fetch active delivery slots with future delivery times
const slots = await db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, now), // Only future slots
),
with: {
productSlots: {
with: {
product: {
with: {
unit: true,
store: true,
},
},
},
},
},
orderBy: asc(deliverySlotInfo.deliveryTime),
});
// Transform data for storage
const slotsWithProducts = await Promise.all(
slots.map(async (slot) => ({
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isActive: slot.isActive,
products: await Promise.all(
slot.productSlots.map(async (productSlot) => ({
id: productSlot.product.id,
name: productSlot.product.name,
shortDescription: productSlot.product.shortDescription,
price: productSlot.product.price.toString(),
marketPrice: productSlot.product.marketPrice?.toString() || null,
unit: productSlot.product.unit?.shortNotation || null,
images: await generateSignedUrlsFromS3Urls(
(productSlot.product.images as string[]) || [],
),
isOutOfStock: productSlot.product.isOutOfStock,
storeId: productSlot.product.storeId,
nextDeliveryDate: slot.deliveryTime,
})),
),
})),
);
// Store each slot in Redis with key pattern "slot:{id}"
for (const slot of slotsWithProducts) {
await redisClient.set(`slot:${slot.id}`, JSON.stringify(slot));
}
console.log('Slot store initialized successfully');
} catch (error) {
console.error('Error initializing slot store:', error);
}
}
export async function getSlotById(slotId: number): Promise<SlotWithProducts | null> {
try {
const key = `slot:${slotId}`;
const data = await redisClient.get(key);
if (!data) return null;
return JSON.parse(data) as SlotWithProducts;
} catch (error) {
console.error(`Error getting slot ${slotId}:`, error);
return null;
}
}
export async function getAllSlots(): Promise<SlotWithProducts[]> {
try {
// Get all keys matching the pattern "slot:*"
const keys = await redisClient.KEYS('slot:*');
if (keys.length === 0) return [];
// Get all slots using MGET for better performance
const slotsData = await redisClient.MGET(keys);
const slots: SlotWithProducts[] = [];
for (const slotData of slotsData) {
if (slotData) {
slots.push(JSON.parse(slotData) as SlotWithProducts);
}
}
return slots;
} catch (error) {
console.error('Error getting all slots:', error);
return [];
}
}

View file

@ -1,51 +0,0 @@
import roleManager from '../lib/roles-manager';
import { computeConstants } from '../lib/const-store';
import { initializeProducts } from './product-store';
import { initializeProductTagStore } from './product-tag-store';
import { initializeSlotStore } from './slot-store';
import { initializeBannerStore } from './banner-store';
/**
* Initialize all application stores
* This function handles initialization of:
* - Role Manager (fetches and caches all roles)
* - Const Store (syncs constants from DB to Redis)
* - Product Store (caches all products in Redis)
* - Product Tag Store (caches all product tags in Redis)
* - Slot Store (caches all delivery slots with products in Redis)
* - Banner Store (caches all banners in Redis)
*/
export const initializeAllStores = async (): Promise<void> => {
try {
console.log('Starting application stores initialization...');
// Initialize role manager
await roleManager.fetchRoles();
console.log('Role manager initialized successfully');
// Compute and store constants in Redis
await computeConstants();
console.log('Const store initialized successfully');
// Initialize product store in Redis
await initializeProducts();
console.log('Product store initialized successfully');
// Initialize product tag store in Redis
await initializeProductTagStore();
console.log('Product tag store initialized successfully');
// Initialize slot store in Redis
await initializeSlotStore();
console.log('Slot store initialized successfully');
// Initialize banner store in Redis
await initializeBannerStore();
console.log('Banner store initialized successfully');
console.log('All application stores initialized successfully');
} catch (error) {
console.error('Application stores initialization failed:', error);
throw error;
}
};

View file

@ -5,7 +5,6 @@ import { eq, and, desc, sql } from 'drizzle-orm';
import { protectedProcedure, router } from '../trpc-index';
import { extractKeyFromPresignedUrl, generateSignedUrlFromS3Url } from '../../lib/s3-client';
import { ApiError } from 'src/lib/api-error';
import { initializeAllStores } from '../../stores/store-initializer';
export const bannerRouter = router({
// Get all banners
@ -103,9 +102,6 @@ export const bannerRouter = router({
isActive: false, // Default to inactive
}).returning();
// Reinitialize stores to reflect changes
await initializeAllStores();
return banner;
} catch (error) {
console.error('Error creating banner:', error);
@ -149,10 +145,6 @@ export const bannerRouter = router({
.set({ ...finalData, lastUpdated: new Date(), })
.where(eq(homeBanners.id, id))
.returning();
// Reinitialize stores to reflect changes
await initializeAllStores();
return banner;
} catch (error) {
console.error('Error updating banner:', error);
@ -165,10 +157,6 @@ export const bannerRouter = router({
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.delete(homeBanners).where(eq(homeBanners.id, input.id));
// Reinitialize stores to reflect changes
await initializeAllStores();
return { success: true };
}),
});

View file

@ -7,7 +7,6 @@ import { ApiError } from '../../lib/api-error';
import { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUrl, claimUploadUrl } from '../../lib/s3-client';
import { deleteS3Image } from '../../lib/delete-image';
import type { SpecialDeal } from '../../db/types';
import { initializeAllStores } from '../../stores/store-initializer';
type CreateDeal = {
quantity: number;
@ -101,9 +100,6 @@ export const productRouter = router({
throw new ApiError("Product not found", 404);
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
message: "Product deleted successfully",
};
@ -132,9 +128,6 @@ export const productRouter = router({
.where(eq(productInfo.id, id))
.returning();
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
product: updatedProduct,
message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`,
@ -188,9 +181,6 @@ export const productRouter = router({
await db.insert(productSlots).values(newAssociations);
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
message: "Slot products updated successfully",
added: productsToAdd.length,
@ -390,9 +380,6 @@ export const productRouter = router({
await db.insert(productGroupMembership).values(memberships);
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
group: newGroup,
message: 'Group created successfully',
@ -438,9 +425,6 @@ export const productRouter = router({
}
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
group: updatedGroup,
message: 'Group updated successfully',
@ -467,9 +451,6 @@ export const productRouter = router({
throw new ApiError('Group not found', 404);
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
message: 'Group deleted successfully',
};
@ -523,9 +504,6 @@ export const productRouter = router({
await Promise.all(updatePromises);
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
message: `Updated prices for ${updates.length} product(s)`,
updatedCount: updates.length,

View file

@ -8,7 +8,6 @@ import { ApiError } from "../../lib/api-error";
import { appUrl } from "../../lib/env-exporter";
import redisClient from "../../lib/redis-client";
import { getSlotSequenceKey } from "../../lib/redisKeyGetters";
import { initializeAllStores } from '../../stores/store-initializer';
interface CachedDeliverySequence {
[userId: string]: number[];
@ -212,9 +211,6 @@ export const slotsRouter = router({
await db.insert(productSlots).values(newAssociations);
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
message: "Slot products updated successfully",
added: productsToAdd.length,
@ -287,9 +283,6 @@ export const slotsRouter = router({
}
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
slot: newSlot,
createdSnippets,
@ -432,9 +425,6 @@ export const slotsRouter = router({
}
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
slot: updatedSlot,
createdSnippets,
@ -467,9 +457,6 @@ export const slotsRouter = router({
throw new ApiError("Slot not found", 404);
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
message: "Slot deleted successfully",
};

View file

@ -6,7 +6,6 @@ import { eq, inArray } from 'drizzle-orm';
import { ApiError } from '../../lib/api-error';
import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '../../lib/s3-client';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { initializeAllStores } from '../../stores/store-initializer';
export const storeRouter = router({
getStores: protectedProcedure
@ -84,9 +83,6 @@ export const storeRouter = router({
.where(inArray(productInfo.id, products));
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
store: newStore,
message: "Store created successfully",
@ -163,9 +159,6 @@ export const storeRouter = router({
}
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
store: updatedStore,
message: "Store updated successfully",
@ -196,9 +189,6 @@ export const storeRouter = router({
throw new ApiError("Store not found", 404);
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
message: "Store deleted successfully",
};

View file

@ -4,8 +4,6 @@ import { productInfo, units, productSlots, deliverySlotInfo, storeInfo, productT
import { eq, gt, and, sql, inArray } from 'drizzle-orm';
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '../../lib/s3-client';
import { z } from 'zod';
import { getAllProducts as getAllProductsFromCache } from '../../stores/product-store';
import { getDashboardTags as getDashboardTagsFromCache } from '../../stores/product-tag-store';
export const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
const result = await db
@ -33,11 +31,22 @@ export const getNextDeliveryDate = async (productId: number): Promise<Date | nul
export const commonRouter = router({
getDashboardTags: publicProcedure
.query(async () => {
// Get dashboard tags from cache
const tags = await getDashboardTagsFromCache();
const tags = await db
.select()
.from(productTagInfo)
.where(eq(productTagInfo.isDashboardTag, true))
.orderBy(productTagInfo.tagName);
// Generate signed URLs for tag images
const tagsWithSignedUrls = await Promise.all(
tags.map(async (tag) => ({
...tag,
imageUrl: tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null,
}))
);
return {
tags: tags,
tags: tagsWithSignedUrls,
};
}),
@ -49,60 +58,76 @@ export const commonRouter = router({
.query(async ({ input }) => {
const { searchQuery, tagId } = input;
// Get all products from cache
let products = await getAllProductsFromCache();
let productIds: number[] | null = null;
// Apply tag filtering if tagId is provided
// If tagId is provided, get products that have this tag
if (tagId) {
// Get products that have this tag from the database
const taggedProducts = await db
.select({ productId: productTags.productId })
.from(productTags)
.where(eq(productTags.tagId, tagId));
const taggedProductIds = new Set(taggedProducts.map(tp => tp.productId));
// Filter products based on tag
products = products.filter(product => taggedProductIds.has(product.id));
productIds = taggedProducts.map(tp => tp.productId);
}
// Apply search filtering if searchQuery is provided
let whereConditions = [];
// Add tag filtering
if (productIds && productIds.length > 0) {
whereConditions.push(inArray(productInfo.id, productIds));
} else if (tagId) {
// If tagId was provided but no products found, return empty array
return {
products: [],
count: 0,
};
}
// Add search filtering
if (searchQuery) {
const searchLower = searchQuery.toLowerCase();
products = products.filter(product =>
product.name.toLowerCase().includes(searchLower)
);
whereConditions.push(sql`LOWER(${productInfo.name}) LIKE LOWER(${ '%' + searchQuery + '%' })`);
}
// Get suspended product IDs to filter them out
const suspendedProducts = await db
.select({ id: productInfo.id })
.from(productInfo)
.where(eq(productInfo.isSuspended, true));
const suspendedProductIds = new Set(suspendedProducts.map(sp => sp.id));
// Filter out suspended products
products = products.filter(product => !suspendedProductIds.has(product.id));
whereConditions.push(eq(productInfo.isSuspended, false));
// Format products to match the expected response structure
const whereCondition = whereConditions.length > 0 ? and(...whereConditions) : undefined;
const productsWithUnits = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
productQuantity: productInfo.productQuantity,
storeId: productInfo.storeId,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(whereCondition);
// Generate signed URLs for product images
const formattedProducts = await Promise.all(
products.map(async (product) => {
productsWithUnits.map(async (product) => {
const nextDeliveryDate = await getNextDeliveryDate(product.id);
return {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
price: parseFloat(product.price),
marketPrice: product.marketPrice ? parseFloat(product.marketPrice) : null,
unit: product.unitNotation,
unitNotation: product.unitNotation,
price: product.price,
marketPrice: product.marketPrice,
unit: product.unitShortNotation,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
storeId: product.store?.id || null,
storeId: product.storeId,
isOutOfStock: product.isOutOfStock,
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
images: product.images, // Already signed URLs from cache
images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []),
};
})
);

View file

@ -5,34 +5,34 @@ import { productInfo, units, productSlots, deliverySlotInfo, specialDeals, store
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url, generateUploadUrl, claimUploadUrl, extractKeyFromPresignedUrl } from '../../lib/s3-client';
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';
// Uniform Product Type
interface Product {
id: number;
name: string;
shortDescription: string | null;
longDescription: string | null;
price: string;
marketPrice: string | null;
unitNotation: string;
images: string[];
isOutOfStock: boolean;
store: { id: number; name: string; description: string | null } | null;
incrementStep: number;
productQuantity: number;
isFlashAvailable: boolean;
flashPrice: string | null;
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date }>;
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>;
}
export const productRouter = router({
getDashboardTags: publicProcedure
.query(async () => {
const tags = await db
.select()
.from(productTagInfo)
.where(eq(productTagInfo.isDashboardTag, true))
.orderBy(productTagInfo.tagName);
// Generate signed URLs for tag images
const tagsWithSignedUrls = await Promise.all(
tags.map(async (tag) => ({
...tag,
imageUrl: tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null,
}))
);
return {
tags: tagsWithSignedUrls,
};
}),
getProductDetails: publicProcedure
.input(z.object({
id: z.string().regex(/^\d+$/, 'Invalid product ID'),
}))
.query(async ({ input }): Promise<Product> => {
.query(async ({ input }) => {
const { id } = input;
const productId = parseInt(id);
@ -40,16 +40,7 @@ export const productRouter = router({
throw new Error('Invalid product ID');
}
console.log('from the api to get product details')
// First, try to get the product from Redis cache
const cachedProduct = await getProductByIdFromCache(productId);
if (cachedProduct) {
return cachedProduct;
}
// If not in cache, fetch from database (fallback)
// Fetch product with unit information
const productData = await db
.select({
id: productInfo.id,
@ -62,8 +53,6 @@ export const productRouter = router({
isOutOfStock: productInfo.isOutOfStock,
storeId: productInfo.storeId,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
productQuantity: productInfo.productQuantity,
isFlashAvailable: productInfo.isFlashAvailable,
flashPrice: productInfo.flashPrice,
})
@ -121,27 +110,25 @@ export const productRouter = router({
// Generate signed URLs for images
const signedImages = await generateSignedUrlsFromS3Urls((product.images as string[]) || []);
const response: Product = {
const response = {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
longDescription: product.longDescription,
price: product.price.toString(),
marketPrice: product.marketPrice?.toString() || null,
unitNotation: product.unitShortNotation,
price: product.price,
marketPrice: product.marketPrice,
unit: product.unitShortNotation,
images: signedImages,
isOutOfStock: product.isOutOfStock,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice,
store: storeData ? {
id: storeData.id,
name: storeData.name,
description: storeData.description,
} : null,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice?.toString() || null,
deliverySlots: deliverySlotsData,
specialDeals: specialDealsData.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })),
specialPackageDeals: specialDealsData,
};
return response;
@ -235,19 +222,128 @@ export const productRouter = router({
}),
getAllProductsSummary: publicProcedure
.query(async (): Promise<Product[]> => {
// Get all products from cache
const allCachedProducts = await getAllProductsFromCache();
.query(async () => {
const products = await db
.select({
id: productInfo.id,
name: productInfo.name,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
storeId: productInfo.storeId,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
isFlashAvailable: productInfo.isFlashAvailable,
flashPrice: productInfo.flashPrice,
productQuantity: productInfo.productQuantity,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id));
// Transform the cached products to match the expected summary format
// (with empty deliverySlots and specialDeals arrays for summary view)
const transformedProducts = allCachedProducts.map(product => ({
...product,
deliverySlots: [], // Empty for summary view
specialDeals: [], // Empty for summary view
}));
// Generate signed URLs for images
const productsWithSignedUrls = await Promise.all(
products.map(async (product) => ({
id: product.id,
name: product.name,
price: product.price,
marketPrice: product.marketPrice,
unit: product.unitShortNotation,
isOutOfStock: product.isOutOfStock,
storeId: product.storeId,
images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []),
incrementStep: product.incrementStep,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice,
productQuantity: product.productQuantity,
}))
);
return transformedProducts;
return productsWithSignedUrls;
}),
productMega: publicProcedure
.query(async () => {
// Fetch all products with unit info
const productsData = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
longDescription: productInfo.longDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
storeId: productInfo.storeId,
unitShortNotation: units.shortNotation,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id));
// Fetch all stores
const allStores = await db.query.storeInfo.findMany({
columns: { id: true, name: true, description: true },
});
const storeMap = new Map(allStores.map(s => [s.id, s]));
// Fetch all delivery slots for all products
const allDeliverySlots = await db
.select({
productId: productSlots.productId,
id: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime,
})
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
)
)
.orderBy(deliverySlotInfo.deliveryTime);
// Group by productId
const deliverySlotsMap = new Map<number, { productId: number; id: number; deliveryTime: Date; freezeTime: Date; }[]>();
for (const slot of allDeliverySlots) {
if (!deliverySlotsMap.has(slot.productId)) {
deliverySlotsMap.set(slot.productId, []);
}
deliverySlotsMap.get(slot.productId)!.push(slot);
}
// Build the response
const response = await Promise.all(
productsData.map(async (product) => {
const signedImages = await generateSignedUrlsFromS3Urls((product.images as string[]) || []);
const store = product.storeId ? storeMap.get(product.storeId) || null : null;
const deliverySlots = deliverySlotsMap.get(product.id) || [];
return {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
longDescription: product.longDescription,
price: product.price,
marketPrice: product.marketPrice,
unit: product.unitShortNotation,
images: signedImages,
isOutOfStock: product.isOutOfStock,
store: store ? {
id: store.id,
name: store.name,
description: store.description,
} : null,
deliverySlots: deliverySlots.map(s => ({ id: s.id, deliveryTime: s.deliveryTime, freezeTime: s.freezeTime })),
specialPackageDeals: [], // Empty since not fetching
};
})
);
return response;
}),
});

View file

@ -1,27 +1,49 @@
import { router, publicProcedure } from "../trpc-index";
import { z } from "zod";
import { db } from "../../db/db_index";
import {
deliverySlotInfo,
productSlots,
productInfo,
units,
} from "../../db/schema";
import { eq, and, gt, asc } from "drizzle-orm";
import { generateSignedUrlsFromS3Urls } from "../../lib/s3-client";
import { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "../../stores/slot-store";
import { router, publicProcedure } from '../trpc-index';
import { z } from 'zod';
import { db } from '../../db/db_index';
import { deliverySlotInfo, productSlots, productInfo, units } from '../../db/schema';
import { eq, and, gt, asc } from 'drizzle-orm';
import { generateSignedUrlsFromS3Urls } from '../../lib/s3-client';
// Helper method to get formatted slot data by ID
async function getSlotData(slotId: number) {
// Get slot from cache
const slot = await getSlotByIdFromCache(slotId);
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
with: {
productSlots: {
with: {
product: {
with: {
unit: true,
store: true,
},
},
},
},
},
});
if (!slot) {
return null; // Slot not found
}
// Filter out out-of-stock products and format to match home page display structure
const products = slot.products.filter((product) => !product.isOutOfStock);
const products = await Promise.all(
slot.productSlots
.filter(productSlot => !productSlot.product.isOutOfStock)
.map(async (productSlot) => ({
id: productSlot.product.id,
name: productSlot.product.name,
price: productSlot.product.price,
unit: productSlot.product.unit?.shortNotation || 'unit',
images: await generateSignedUrlsFromS3Urls(
(productSlot.product.images as string[]) || []
),
isOutOfStock: productSlot.product.isOutOfStock,
storeId: productSlot.product.storeId,
nextDeliveryDate: slot.deliveryTime, // For home page compatibility
}))
);
return {
deliveryTime: slot.deliveryTime,
@ -32,64 +54,95 @@ async function getSlotData(slotId: number) {
}
export const slotsRouter = router({
getSlots: publicProcedure.query(async () => {
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
});
return {
slots,
count: slots.length,
};
}),
getSlots: publicProcedure
.query(async () => {
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
});
return {
slots,
count: slots.length,
};
}),
getSlotsWithProducts: publicProcedure.query(async () => {
// Get all slots from cache
const slotsWithProducts = await getAllSlotsFromCache();
getSlotsWithProducts: publicProcedure
.query(async () => {
const now = new Date();
return {
slots: slotsWithProducts,
count: slotsWithProducts.length,
};
}),
// Fetch active delivery slots with future delivery times
const slots = await db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, now) // Only future slots
),
with: {
productSlots: {
with: {
product: {
with: {
unit: true,
},
},
},
},
},
orderBy: asc(deliverySlotInfo.deliveryTime),
});
nextMajorDelivery: publicProcedure.query(async () => {
const now = new Date();
// Transform data for frontend
const slotsWithProducts = await Promise.all(
slots.map(async (slot) => ({
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isActive: slot.isActive,
products: await Promise.all(
slot.productSlots.map(async (productSlot) => ({
id: productSlot.product.id,
name: productSlot.product.name,
shortDescription: productSlot.product.shortDescription,
price: productSlot.product.price,
marketPrice: productSlot.product.marketPrice,
unit: productSlot.product.unit?.shortNotation,
images: await generateSignedUrlsFromS3Urls(
(productSlot.product.images as string[]) || []
),
isOutOfStock: productSlot.product.isOutOfStock,
}))
),
}))
);
// Find the next upcoming active delivery slot ID
const nextSlot = await db.query.deliverySlotInfo.findFirst({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, now),
),
orderBy: asc(deliverySlotInfo.deliveryTime),
});
return {
slots: slotsWithProducts,
count: slotsWithProducts.length,
};
}),
if (!nextSlot) {
return null; // No upcoming delivery slots
}
nextMajorDelivery: publicProcedure
.query(async () => {
const now = new Date();
// Get formatted data using helper method
return await getSlotData(nextSlot.id);
}),
// Find the next upcoming active delivery slot ID
const nextSlot = await db.query.deliverySlotInfo.findFirst({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, now)
),
orderBy: asc(deliverySlotInfo.deliveryTime),
});
if (!nextSlot) {
return null; // No upcoming delivery slots
}
// Get formatted data using helper method
return await getSlotData(nextSlot.id);
}),
getSlotById: publicProcedure
.input(z.object({ slotId: z.number() }))
.query(async ({ input }) => {
// Get slot from cache
const slot = await getSlotByIdFromCache(input.slotId);
if (!slot) {
return null; // Slot not found
}
// Filter out out-of-stock products and format to match home page display structure
const products = slot.products.filter((product) => !product.isOutOfStock);
return {
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
slotId: slot.id,
products,
};
return await getSlotData(input.slotId);
}),
});
});

View file

@ -111,6 +111,7 @@ export default function Dashboard() {
const { data: essentialConsts, isLoading: isLoadingConsts, error: constsError } = useGetEssentialConsts();
const { data: tagsData } = trpc.common.product.getDashboardTags.useQuery();
const { data: cartData, refetch: refetchCart } = useGetCart();
const { data: storesData } = trpc.user.stores.getStores.useQuery();
const { data: defaultAddressResponse } =
trpc.user.address.getDefaultAddress.useQuery();

View file

@ -142,7 +142,7 @@ export default function DeliverySlots() {
{product.name}
</MyText>
<MyText style={tw`text-xs text-gray-600`}>
{product.price} {product.unit && `per ${product.unit}`}
{product.price} {product.unit && `per ${product.unit}`}
</MyText>
</View>
{product.isOutOfStock && (

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
import { tw, BottomDialog } from 'common-ui';
import { useQueryClient } from '@tanstack/react-query';
@ -17,7 +17,6 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
}) => {
const [showAddAddress, setShowAddAddress] = useState(false);
const queryClient = useQueryClient();
const scrollViewRef = useRef<ScrollView>(null);
const { data: addresses } = trpc.user.address.getUserAddresses.useQuery();
// Sort addresses with selected first, then default, then others
@ -47,11 +46,6 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
}
}, [sortedAddresses, selectedAddress, onAddressSelect]);
// Reset scroll to left when address is selected
const resetScrollToLeft = () => {
scrollViewRef.current?.scrollTo({ x: 0, y: 0, animated: true });
};
return (
<>
<View style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}>
@ -80,19 +74,11 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
</TouchableOpacity>
</View>
) : (
<ScrollView
ref={scrollViewRef}
horizontal
showsHorizontalScrollIndicator={false}
style={tw`pb-2`}
>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={tw`pb-2`}>
{sortedAddresses.map((address) => (
<TouchableOpacity
key={address.id}
onPress={() => {
onAddressSelect(address.id);
resetScrollToLeft();
}}
onPress={() => onAddressSelect(address.id)}
style={tw`w-72 p-4 mr-3 bg-gray-50 rounded-xl border-2 ${selectedAddress === address.id ? 'border-brand500 bg-blue-50' : 'border-gray-200'
} shadow-sm`}
>

View file

@ -36,7 +36,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
onPress,
showDeliveryInfo = true,
miniView = false,
}) => {
}) => {
const { data: cartData } = useGetCart();
const { getQuickestSlot } = useProductSlotIdentifier();
const updateCartItem = useUpdateCartItem({
@ -128,7 +128,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
)}
</View>
<View style={tw`flex-row items-center mb-2`}>
<MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(item.productQuantity || 1, item.unitNotation).display}</MyText></MyText>
<MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(item.productQuantity || 1, item.unit).display}</MyText></MyText>
</View>
{showDeliveryInfo && item.nextDeliveryDate && (
@ -151,7 +151,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
value={quantity}
setValue={handleQuantityChange}
step={item.incrementStep}
unit={item.unitNotation}
unit={item.unit}
/>
) : (
<MyTouchableOpacity

View file

@ -29,13 +29,6 @@ interface ProductDetailProps {
isFlashDelivery?: boolean;
}
const formatQuantity = (quantity: number, unit: string): { value: string; display: string } => {
if (unit?.toLowerCase() === 'kg' && quantity < 1) {
return { value: `${Math.round(quantity * 1000)} g`, display: `${Math.round(quantity * 1000)}g` };
}
return { value: `${quantity} ${unit}(s)`, display: `${quantity}${unit}` };
};
const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDelivery = false }) => {
const router = useRouter();
const [showAllSlots, setShowAllSlots] = useState(false);
@ -246,7 +239,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
<MyText style={tw`text-3xl font-bold text-gray-900`}>
{productDetail.price}
</MyText>
<MyText style={tw`text-gray-500 text-lg mb-1 ml-1`}>/ {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display}</MyText>
<MyText style={tw`text-gray-500 text-lg mb-1 ml-1`}>/ {productDetail.unit}</MyText>
{/* Show market price discount if available */}
{productDetail.marketPrice && (
@ -263,7 +256,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
{productDetail.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && (
<View style={tw`mt-1`}>
<MyText style={tw`text-pink-600 text-lg font-bold`}>
Flash Delivery: {productDetail.flashPrice} / {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display}
Flash Delivery: {productDetail.flashPrice} / {productDetail.unit}
</MyText>
</View>
)}
@ -280,7 +273,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
value={quantity}
setValue={handleQuantityChange}
step={1} // Default step for product detail quantifier
unit={productDetail.unitNotation}
unit={productDetail.unit}
/>
</View>
) : (
@ -407,7 +400,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
</View>
{/* Package Deals */}
{productDetail.specialDeals && productDetail.specialDeals.length > 0 && (
{productDetail.specialPackageDeals && productDetail.specialPackageDeals.length > 0 && (
<View style={tw`px-4 mb-4`}>
<View style={tw`bg-white p-5 rounded-2xl shadow-sm border border-gray-100`}>
<View style={tw`flex-row items-center mb-4`}>
@ -417,9 +410,9 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
<MyText style={tw`text-lg font-bold text-gray-900`}>Bulk Savings</MyText>
</View>
{productDetail.specialDeals.map((deal: { quantity: string; price: string; validTill: string }, index: number) => (
{productDetail.specialPackageDeals.map((deal, index) => (
<View key={index} style={tw`flex-row justify-between items-center p-3 bg-amber-50 rounded-xl border border-amber-100 mb-2`}>
<MyText style={tw`text-amber-900 font-medium`}>Buy {deal.quantity} {formatQuantity(parseFloat(deal.quantity), productDetail.unitNotation).display}</MyText>
<MyText style={tw`text-amber-900 font-medium`}>Buy {deal.quantity} {productDetail.unit}</MyText>
<MyText style={tw`text-amber-900 font-bold text-lg`}>{deal.price}</MyText>
</View>
))}

View file

@ -293,7 +293,7 @@ const CompactProductCard = ({
onChange={handleQuantityChange}
step={item.incrementStep}
showUnits={true}
unit={item.unitNotation}
unit={item.unit}
/>
) : (
<MyTouchableOpacity
@ -318,7 +318,7 @@ const CompactProductCard = ({
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
<MyText style={tw`text-gray-400 text-xs ml-1 line-through`}>{item.marketPrice}</MyText>
)}
<MyText style={tw`text-gray-600 text-xs ml-1`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(item.productQuantity || 1, item.unitNotation).display}</MyText></MyText>
<MyText style={tw`text-gray-600 text-xs ml-1`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(item.productQuantity || 1, item.unit).display}</MyText></MyText>
</View>
</View>
</View>
@ -390,7 +390,7 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
);
}
const filteredProducts: any[] = storeIdNum ? productsQuery?.data?.filter(p => p.store?.id === storeIdNum) || [] : slotQuery.data.products;
const filteredProducts: any[] = storeIdNum ? productsQuery?.data?.filter(p => p.storeId === storeIdNum) || [] : slotQuery.data.products;
return (
<View style={tw`flex-1`}>
@ -470,7 +470,7 @@ export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProduc
let flashProducts: any[] = [];
if (storeIdNum) {
// Filter by store and flash availability
flashProducts = productsQuery?.data?.filter(p => p.store?.id === storeIdNum && p.isFlashAvailable) || [];
flashProducts = productsQuery?.data?.filter(p => p.storeId === storeIdNum && p.isFlashAvailable) || [];
} else {
// Show all flash-available products (no slot filtering)
flashProducts = productsQuery?.data?.filter(p => p.isFlashAvailable) || [];

View file

@ -444,7 +444,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
<MyText style={tw`text-xs text-gray-500 mr-2`}>
{(() => {
const qty = item.product?.productQuantity || 1;
const unit = item.product?.unitNotation || '';
const unit = item.product?.unit || '';
if (unit?.toLowerCase() === 'kg' && qty < 1) {
return `${Math.round(qty * 1000)}g`;
}
@ -506,7 +506,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
}
}}
step={item.product.incrementStep}
unit={item.product?.unitNotation}
unit={item.product?.unit}
/>
</View>
</View>

View file

@ -249,7 +249,7 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
}}
step={item.product.incrementStep}
showUnits={true}
unit={item.product?.unitNotation}
unit={item.product?.unit}
/>
</View>
<View style={tw`flex-row items-center justify-between`}>

View file

@ -25,20 +25,11 @@ interface LocalCartItem {
interface ProductSummary {
id: number;
name: string;
shortDescription?: string | null;
longDescription?: string | null;
price: string;
marketPrice?: string | null;
unitNotation: string;
images: string[];
price: number;
unit: string;
isOutOfStock: boolean;
store?: { id: number; name: string; description?: string | null } | null;
incrementStep: number;
productQuantity: number;
isFlashAvailable: boolean;
flashPrice?: string | null;
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date }>;
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>;
images: string[];
incrementStep?: number;
}
interface CartItem {

View file

@ -63,9 +63,9 @@ const isDevMode = Constants.executionEnvironment !== "standalone";
// const BASE_API_URL = API_URL;
// 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';
// const BASE_API_URL = 'http://192.168.1.9:4000';
// const BASE_API_URL = "https://mf.technocracy.ovh";
// let BASE_API_URL = "https://mf.freshyo.in";
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';