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 './notif-job';
import { initializeAllStores } from '../stores/store-initializer'; import { computeConstants } from './const-store';
/** /**
* Initialize all application services * Initialize all application services
@ -12,8 +13,13 @@ export const initFunc = async (): Promise<void> => {
try { try {
console.log('Starting application initialization...'); console.log('Starting application initialization...');
// Initialize all stores // Initialize role manager
await initializeAllStores(); await roleManager.fetchRoles();
console.log('Role manager initialized successfully');
// Compute and store constants in Redis
await computeConstants();
console.log('Const store initialized successfully');
// Notification queue and worker are initialized via import // Notification queue and worker are initialized via import
console.log('Notification queue and worker initialized'); console.log('Notification queue and worker initialized');

View file

@ -63,14 +63,6 @@ class RedisClient {
return await this.client.lPush(key, value); 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 { disconnect(): void {
if (this.isConnected) { if (this.isConnected) {
this.client.disconnect(); 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 { protectedProcedure, router } from '../trpc-index';
import { extractKeyFromPresignedUrl, generateSignedUrlFromS3Url } from '../../lib/s3-client'; import { extractKeyFromPresignedUrl, generateSignedUrlFromS3Url } from '../../lib/s3-client';
import { ApiError } from 'src/lib/api-error'; import { ApiError } from 'src/lib/api-error';
import { initializeAllStores } from '../../stores/store-initializer';
export const bannerRouter = router({ export const bannerRouter = router({
// Get all banners // Get all banners
@ -103,9 +102,6 @@ export const bannerRouter = router({
isActive: false, // Default to inactive isActive: false, // Default to inactive
}).returning(); }).returning();
// Reinitialize stores to reflect changes
await initializeAllStores();
return banner; return banner;
} catch (error) { } catch (error) {
console.error('Error creating banner:', error); console.error('Error creating banner:', error);
@ -149,10 +145,6 @@ export const bannerRouter = router({
.set({ ...finalData, lastUpdated: new Date(), }) .set({ ...finalData, lastUpdated: new Date(), })
.where(eq(homeBanners.id, id)) .where(eq(homeBanners.id, id))
.returning(); .returning();
// Reinitialize stores to reflect changes
await initializeAllStores();
return banner; return banner;
} catch (error) { } catch (error) {
console.error('Error updating banner:', error); console.error('Error updating banner:', error);
@ -165,10 +157,6 @@ export const bannerRouter = router({
.input(z.object({ id: z.number() })) .input(z.object({ id: z.number() }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await db.delete(homeBanners).where(eq(homeBanners.id, input.id)); await db.delete(homeBanners).where(eq(homeBanners.id, input.id));
// Reinitialize stores to reflect changes
await initializeAllStores();
return { success: true }; 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 { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUrl, claimUploadUrl } from '../../lib/s3-client';
import { deleteS3Image } from '../../lib/delete-image'; import { deleteS3Image } from '../../lib/delete-image';
import type { SpecialDeal } from '../../db/types'; import type { SpecialDeal } from '../../db/types';
import { initializeAllStores } from '../../stores/store-initializer';
type CreateDeal = { type CreateDeal = {
quantity: number; quantity: number;
@ -101,9 +100,6 @@ export const productRouter = router({
throw new ApiError("Product not found", 404); throw new ApiError("Product not found", 404);
} }
// Reinitialize stores to reflect changes
await initializeAllStores();
return { return {
message: "Product deleted successfully", message: "Product deleted successfully",
}; };
@ -132,9 +128,6 @@ export const productRouter = router({
.where(eq(productInfo.id, id)) .where(eq(productInfo.id, id))
.returning(); .returning();
// Reinitialize stores to reflect changes
await initializeAllStores();
return { return {
product: updatedProduct, product: updatedProduct,
message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`, 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); await db.insert(productSlots).values(newAssociations);
} }
// Reinitialize stores to reflect changes
await initializeAllStores();
return { return {
message: "Slot products updated successfully", message: "Slot products updated successfully",
added: productsToAdd.length, added: productsToAdd.length,
@ -390,9 +380,6 @@ export const productRouter = router({
await db.insert(productGroupMembership).values(memberships); await db.insert(productGroupMembership).values(memberships);
} }
// Reinitialize stores to reflect changes
await initializeAllStores();
return { return {
group: newGroup, group: newGroup,
message: 'Group created successfully', message: 'Group created successfully',
@ -438,9 +425,6 @@ export const productRouter = router({
} }
} }
// Reinitialize stores to reflect changes
await initializeAllStores();
return { return {
group: updatedGroup, group: updatedGroup,
message: 'Group updated successfully', message: 'Group updated successfully',
@ -467,9 +451,6 @@ export const productRouter = router({
throw new ApiError('Group not found', 404); throw new ApiError('Group not found', 404);
} }
// Reinitialize stores to reflect changes
await initializeAllStores();
return { return {
message: 'Group deleted successfully', message: 'Group deleted successfully',
}; };
@ -523,9 +504,6 @@ export const productRouter = router({
await Promise.all(updatePromises); await Promise.all(updatePromises);
// Reinitialize stores to reflect changes
await initializeAllStores();
return { return {
message: `Updated prices for ${updates.length} product(s)`, message: `Updated prices for ${updates.length} product(s)`,
updatedCount: updates.length, updatedCount: updates.length,

View file

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

View file

@ -6,7 +6,6 @@ import { eq, inArray } from 'drizzle-orm';
import { ApiError } from '../../lib/api-error'; import { ApiError } from '../../lib/api-error';
import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '../../lib/s3-client'; import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '../../lib/s3-client';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { initializeAllStores } from '../../stores/store-initializer';
export const storeRouter = router({ export const storeRouter = router({
getStores: protectedProcedure getStores: protectedProcedure
@ -84,9 +83,6 @@ export const storeRouter = router({
.where(inArray(productInfo.id, products)); .where(inArray(productInfo.id, products));
} }
// Reinitialize stores to reflect changes
await initializeAllStores();
return { return {
store: newStore, store: newStore,
message: "Store created successfully", message: "Store created successfully",
@ -163,9 +159,6 @@ export const storeRouter = router({
} }
} }
// Reinitialize stores to reflect changes
await initializeAllStores();
return { return {
store: updatedStore, store: updatedStore,
message: "Store updated successfully", message: "Store updated successfully",
@ -196,9 +189,6 @@ export const storeRouter = router({
throw new ApiError("Store not found", 404); throw new ApiError("Store not found", 404);
} }
// Reinitialize stores to reflect changes
await initializeAllStores();
return { return {
message: "Store deleted successfully", 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 { eq, gt, and, sql, inArray } from 'drizzle-orm';
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '../../lib/s3-client'; import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '../../lib/s3-client';
import { z } from 'zod'; 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> => { export const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
const result = await db const result = await db
@ -33,11 +31,22 @@ export const getNextDeliveryDate = async (productId: number): Promise<Date | nul
export const commonRouter = router({ export const commonRouter = router({
getDashboardTags: publicProcedure getDashboardTags: publicProcedure
.query(async () => { .query(async () => {
// Get dashboard tags from cache const tags = await db
const tags = await getDashboardTagsFromCache(); .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 { return {
tags: tags, tags: tagsWithSignedUrls,
}; };
}), }),
@ -49,60 +58,76 @@ export const commonRouter = router({
.query(async ({ input }) => { .query(async ({ input }) => {
const { searchQuery, tagId } = input; const { searchQuery, tagId } = input;
// Get all products from cache let productIds: number[] | null = null;
let products = await getAllProductsFromCache();
// Apply tag filtering if tagId is provided // If tagId is provided, get products that have this tag
if (tagId) { if (tagId) {
// Get products that have this tag from the database
const taggedProducts = await db const taggedProducts = await db
.select({ productId: productTags.productId }) .select({ productId: productTags.productId })
.from(productTags) .from(productTags)
.where(eq(productTags.tagId, tagId)); .where(eq(productTags.tagId, tagId));
const taggedProductIds = new Set(taggedProducts.map(tp => tp.productId)); productIds = taggedProducts.map(tp => tp.productId);
// Filter products based on tag
products = products.filter(product => taggedProductIds.has(product.id));
} }
// 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) { if (searchQuery) {
const searchLower = searchQuery.toLowerCase(); whereConditions.push(sql`LOWER(${productInfo.name}) LIKE LOWER(${ '%' + searchQuery + '%' })`);
products = products.filter(product =>
product.name.toLowerCase().includes(searchLower)
);
} }
// 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 // 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( const formattedProducts = await Promise.all(
products.map(async (product) => { productsWithUnits.map(async (product) => {
const nextDeliveryDate = await getNextDeliveryDate(product.id); const nextDeliveryDate = await getNextDeliveryDate(product.id);
return { return {
id: product.id, id: product.id,
name: product.name, name: product.name,
shortDescription: product.shortDescription, shortDescription: product.shortDescription,
price: parseFloat(product.price), price: product.price,
marketPrice: product.marketPrice ? parseFloat(product.marketPrice) : null, marketPrice: product.marketPrice,
unit: product.unitNotation, unit: product.unitShortNotation,
unitNotation: product.unitNotation,
incrementStep: product.incrementStep, incrementStep: product.incrementStep,
productQuantity: product.productQuantity, productQuantity: product.productQuantity,
storeId: product.store?.id || null, storeId: product.storeId,
isOutOfStock: product.isOutOfStock, isOutOfStock: product.isOutOfStock,
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null, 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 { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url, generateUploadUrl, claimUploadUrl, extractKeyFromPresignedUrl } from '../../lib/s3-client';
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';
// 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({ 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 getProductDetails: publicProcedure
.input(z.object({ .input(z.object({
id: z.string().regex(/^\d+$/, 'Invalid product ID'), id: z.string().regex(/^\d+$/, 'Invalid product ID'),
})) }))
.query(async ({ input }): Promise<Product> => { .query(async ({ input }) => {
const { id } = input; const { id } = input;
const productId = parseInt(id); const productId = parseInt(id);
@ -40,16 +40,7 @@ export const productRouter = router({
throw new Error('Invalid product ID'); throw new Error('Invalid product ID');
} }
console.log('from the api to get product details') // Fetch product with unit information
// 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)
const productData = await db const productData = await db
.select({ .select({
id: productInfo.id, id: productInfo.id,
@ -62,8 +53,6 @@ export const productRouter = router({
isOutOfStock: productInfo.isOutOfStock, isOutOfStock: productInfo.isOutOfStock,
storeId: productInfo.storeId, storeId: productInfo.storeId,
unitShortNotation: units.shortNotation, unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
productQuantity: productInfo.productQuantity,
isFlashAvailable: productInfo.isFlashAvailable, isFlashAvailable: productInfo.isFlashAvailable,
flashPrice: productInfo.flashPrice, flashPrice: productInfo.flashPrice,
}) })
@ -121,27 +110,25 @@ export const productRouter = router({
// Generate signed URLs for images // Generate signed URLs for images
const signedImages = await generateSignedUrlsFromS3Urls((product.images as string[]) || []); const signedImages = await generateSignedUrlsFromS3Urls((product.images as string[]) || []);
const response: Product = { const response = {
id: product.id, id: product.id,
name: product.name, name: product.name,
shortDescription: product.shortDescription, shortDescription: product.shortDescription,
longDescription: product.longDescription, longDescription: product.longDescription,
price: product.price.toString(), price: product.price,
marketPrice: product.marketPrice?.toString() || null, marketPrice: product.marketPrice,
unitNotation: product.unitShortNotation, unit: product.unitShortNotation,
images: signedImages, images: signedImages,
isOutOfStock: product.isOutOfStock, isOutOfStock: product.isOutOfStock,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice,
store: storeData ? { store: storeData ? {
id: storeData.id, id: storeData.id,
name: storeData.name, name: storeData.name,
description: storeData.description, description: storeData.description,
} : null, } : null,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice?.toString() || null,
deliverySlots: deliverySlotsData, deliverySlots: deliverySlotsData,
specialDeals: specialDealsData.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })), specialPackageDeals: specialDealsData,
}; };
return response; return response;
@ -235,19 +222,128 @@ export const productRouter = router({
}), }),
getAllProductsSummary: publicProcedure getAllProductsSummary: publicProcedure
.query(async (): Promise<Product[]> => { .query(async () => {
// Get all products from cache const products = await db
const allCachedProducts = await getAllProductsFromCache(); .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 // Generate signed URLs for images
// (with empty deliverySlots and specialDeals arrays for summary view) const productsWithSignedUrls = await Promise.all(
const transformedProducts = allCachedProducts.map(product => ({ products.map(async (product) => ({
...product, id: product.id,
deliverySlots: [], // Empty for summary view name: product.name,
specialDeals: [], // Empty for summary view 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 { router, publicProcedure } from '../trpc-index';
import { z } from "zod"; import { z } from 'zod';
import { db } from "../../db/db_index"; import { db } from '../../db/db_index';
import { import { deliverySlotInfo, productSlots, productInfo, units } from '../../db/schema';
deliverySlotInfo, import { eq, and, gt, asc } from 'drizzle-orm';
productSlots, import { generateSignedUrlsFromS3Urls } from '../../lib/s3-client';
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";
// Helper method to get formatted slot data by ID // Helper method to get formatted slot data by ID
async function getSlotData(slotId: number) { async function getSlotData(slotId: number) {
// Get slot from cache const slot = await db.query.deliverySlotInfo.findFirst({
const slot = await getSlotByIdFromCache(slotId); where: eq(deliverySlotInfo.id, slotId),
with: {
productSlots: {
with: {
product: {
with: {
unit: true,
store: true,
},
},
},
},
},
});
if (!slot) { if (!slot) {
return null; // Slot not found return null; // Slot not found
} }
// Filter out out-of-stock products and format to match home page display structure // 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 { return {
deliveryTime: slot.deliveryTime, deliveryTime: slot.deliveryTime,
@ -32,64 +54,95 @@ async function getSlotData(slotId: number) {
} }
export const slotsRouter = router({ export const slotsRouter = router({
getSlots: publicProcedure.query(async () => { getSlots: publicProcedure
const slots = await db.query.deliverySlotInfo.findMany({ .query(async () => {
where: eq(deliverySlotInfo.isActive, true), const slots = await db.query.deliverySlotInfo.findMany({
}); where: eq(deliverySlotInfo.isActive, true),
return { });
slots, return {
count: slots.length, slots,
}; count: slots.length,
}), };
}),
getSlotsWithProducts: publicProcedure.query(async () => { getSlotsWithProducts: publicProcedure
// Get all slots from cache .query(async () => {
const slotsWithProducts = await getAllSlotsFromCache(); const now = new Date();
return { // Fetch active delivery slots with future delivery times
slots: slotsWithProducts, const slots = await db.query.deliverySlotInfo.findMany({
count: slotsWithProducts.length, 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 () => { // Transform data for frontend
const now = new Date(); 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 return {
const nextSlot = await db.query.deliverySlotInfo.findFirst({ slots: slotsWithProducts,
where: and( count: slotsWithProducts.length,
eq(deliverySlotInfo.isActive, true), };
gt(deliverySlotInfo.deliveryTime, now), }),
),
orderBy: asc(deliverySlotInfo.deliveryTime),
});
if (!nextSlot) { nextMajorDelivery: publicProcedure
return null; // No upcoming delivery slots .query(async () => {
} const now = new Date();
// Get formatted data using helper method // Find the next upcoming active delivery slot ID
return await getSlotData(nextSlot.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 getSlotById: publicProcedure
.input(z.object({ slotId: z.number() })) .input(z.object({ slotId: z.number() }))
.query(async ({ input }) => { .query(async ({ input }) => {
// Get slot from cache return await getSlotData(input.slotId);
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,
};
}), }),
}); });

View file

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

View file

@ -142,7 +142,7 @@ export default function DeliverySlots() {
{product.name} {product.name}
</MyText> </MyText>
<MyText style={tw`text-xs text-gray-600`}> <MyText style={tw`text-xs text-gray-600`}>
{product.price} {product.unit && `per ${product.unit}`} {product.price} {product.unit && `per ${product.unit}`}
</MyText> </MyText>
</View> </View>
{product.isOutOfStock && ( {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 { View, Text, TouchableOpacity, ScrollView } from 'react-native';
import { tw, BottomDialog } from 'common-ui'; import { tw, BottomDialog } from 'common-ui';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
@ -17,7 +17,6 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
}) => { }) => {
const [showAddAddress, setShowAddAddress] = useState(false); const [showAddAddress, setShowAddAddress] = useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const scrollViewRef = useRef<ScrollView>(null);
const { data: addresses } = trpc.user.address.getUserAddresses.useQuery(); const { data: addresses } = trpc.user.address.getUserAddresses.useQuery();
// Sort addresses with selected first, then default, then others // Sort addresses with selected first, then default, then others
@ -47,11 +46,6 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
} }
}, [sortedAddresses, selectedAddress, onAddressSelect]); }, [sortedAddresses, selectedAddress, onAddressSelect]);
// Reset scroll to left when address is selected
const resetScrollToLeft = () => {
scrollViewRef.current?.scrollTo({ x: 0, y: 0, animated: true });
};
return ( return (
<> <>
<View style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}> <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> </TouchableOpacity>
</View> </View>
) : ( ) : (
<ScrollView <ScrollView horizontal showsHorizontalScrollIndicator={false} style={tw`pb-2`}>
ref={scrollViewRef}
horizontal
showsHorizontalScrollIndicator={false}
style={tw`pb-2`}
>
{sortedAddresses.map((address) => ( {sortedAddresses.map((address) => (
<TouchableOpacity <TouchableOpacity
key={address.id} key={address.id}
onPress={() => { onPress={() => onAddressSelect(address.id)}
onAddressSelect(address.id);
resetScrollToLeft();
}}
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' 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`} } shadow-sm`}
> >

View file

@ -128,7 +128,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
)} )}
</View> </View>
<View style={tw`flex-row items-center mb-2`}> <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> </View>
{showDeliveryInfo && item.nextDeliveryDate && ( {showDeliveryInfo && item.nextDeliveryDate && (
@ -151,7 +151,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
value={quantity} value={quantity}
setValue={handleQuantityChange} setValue={handleQuantityChange}
step={item.incrementStep} step={item.incrementStep}
unit={item.unitNotation} unit={item.unit}
/> />
) : ( ) : (
<MyTouchableOpacity <MyTouchableOpacity

View file

@ -29,13 +29,6 @@ interface ProductDetailProps {
isFlashDelivery?: boolean; 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 ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDelivery = false }) => {
const router = useRouter(); const router = useRouter();
const [showAllSlots, setShowAllSlots] = useState(false); 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`}> <MyText style={tw`text-3xl font-bold text-gray-900`}>
{productDetail.price} {productDetail.price}
</MyText> </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 */} {/* Show market price discount if available */}
{productDetail.marketPrice && ( {productDetail.marketPrice && (
@ -263,7 +256,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
{productDetail.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && ( {productDetail.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && (
<View style={tw`mt-1`}> <View style={tw`mt-1`}>
<MyText style={tw`text-pink-600 text-lg font-bold`}> <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> </MyText>
</View> </View>
)} )}
@ -280,7 +273,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
value={quantity} value={quantity}
setValue={handleQuantityChange} setValue={handleQuantityChange}
step={1} // Default step for product detail quantifier step={1} // Default step for product detail quantifier
unit={productDetail.unitNotation} unit={productDetail.unit}
/> />
</View> </View>
) : ( ) : (
@ -407,7 +400,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
</View> </View>
{/* Package Deals */} {/* Package Deals */}
{productDetail.specialDeals && productDetail.specialDeals.length > 0 && ( {productDetail.specialPackageDeals && productDetail.specialPackageDeals.length > 0 && (
<View style={tw`px-4 mb-4`}> <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`bg-white p-5 rounded-2xl shadow-sm border border-gray-100`}>
<View style={tw`flex-row items-center mb-4`}> <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> <MyText style={tw`text-lg font-bold text-gray-900`}>Bulk Savings</MyText>
</View> </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`}> <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> <MyText style={tw`text-amber-900 font-bold text-lg`}>{deal.price}</MyText>
</View> </View>
))} ))}

View file

@ -293,7 +293,7 @@ const CompactProductCard = ({
onChange={handleQuantityChange} onChange={handleQuantityChange}
step={item.incrementStep} step={item.incrementStep}
showUnits={true} showUnits={true}
unit={item.unitNotation} unit={item.unit}
/> />
) : ( ) : (
<MyTouchableOpacity <MyTouchableOpacity
@ -318,7 +318,7 @@ const CompactProductCard = ({
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && ( {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-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> </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 ( return (
<View style={tw`flex-1`}> <View style={tw`flex-1`}>
@ -470,7 +470,7 @@ export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProduc
let flashProducts: any[] = []; let flashProducts: any[] = [];
if (storeIdNum) { if (storeIdNum) {
// Filter by store and flash availability // 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 { } else {
// Show all flash-available products (no slot filtering) // Show all flash-available products (no slot filtering)
flashProducts = productsQuery?.data?.filter(p => p.isFlashAvailable) || []; 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`}> <MyText style={tw`text-xs text-gray-500 mr-2`}>
{(() => { {(() => {
const qty = item.product?.productQuantity || 1; const qty = item.product?.productQuantity || 1;
const unit = item.product?.unitNotation || ''; const unit = item.product?.unit || '';
if (unit?.toLowerCase() === 'kg' && qty < 1) { if (unit?.toLowerCase() === 'kg' && qty < 1) {
return `${Math.round(qty * 1000)}g`; return `${Math.round(qty * 1000)}g`;
} }
@ -506,7 +506,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
} }
}} }}
step={item.product.incrementStep} step={item.product.incrementStep}
unit={item.product?.unitNotation} unit={item.product?.unit}
/> />
</View> </View>
</View> </View>

View file

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

View file

@ -25,20 +25,11 @@ interface LocalCartItem {
interface ProductSummary { interface ProductSummary {
id: number; id: number;
name: string; name: string;
shortDescription?: string | null; price: number;
longDescription?: string | null; unit: string;
price: string;
marketPrice?: string | null;
unitNotation: string;
images: string[];
isOutOfStock: boolean; isOutOfStock: boolean;
store?: { id: number; name: string; description?: string | null } | null; images: string[];
incrementStep: number; 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 }>;
} }
interface CartItem { interface CartItem {

View file

@ -63,9 +63,9 @@ const isDevMode = Constants.executionEnvironment !== "standalone";
// const BASE_API_URL = API_URL; // const BASE_API_URL = API_URL;
// const BASE_API_URL = 'http://10.0.2.2:4000'; // 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.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"; // 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.100.103:4000';
// let BASE_API_URL = 'http://192.168.29.219:4000'; // let BASE_API_URL = 'http://192.168.29.219:4000';