enh
This commit is contained in:
parent
ca9eb8a7d2
commit
5df040de9a
21 changed files with 433 additions and 141 deletions
|
|
@ -22,6 +22,8 @@ EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK-
|
|||
JWT_SECRET=my_meatfarmer_jwt_secret_key
|
||||
ASSETS_DOMAIN=https://assets.freshyo.in/
|
||||
API_CACHE_KEY=api-cache-dev
|
||||
CLOUDFLARE_API_TOKEN=I8Vp4E9TX58E8qEDeH0nTFDS2d2zXNYiXvbs4Ckj
|
||||
CLOUDFLARE_ZONE_ID=edefbf750bfc3ff26ccd11e8e28dc8d7
|
||||
# REDIS_URL=redis://default:redis_shafi_password@5.223.55.14:6379
|
||||
REDIS_URL=redis://default:redis_shafi_password@57.128.212.174:6379
|
||||
APP_URL=http://localhost:4000
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -6,6 +6,7 @@ import { ApiError } from "@/src/lib/api-error";
|
|||
import { imageUploadS3, generateSignedUrlFromS3Url } from "@/src/lib/s3-client";
|
||||
import { deleteS3Image } from "@/src/lib/delete-image";
|
||||
import { initializeAllStores } from '@/src/stores/store-initializer';
|
||||
import { createStoresFile } from '@/src/lib/cloud_cache';
|
||||
|
||||
/**
|
||||
* Create a new product tag
|
||||
|
|
@ -60,10 +61,16 @@ export const createTag = async (req: Request, res: Response) => {
|
|||
// Reinitialize stores to reflect changes in cache
|
||||
await initializeAllStores();
|
||||
|
||||
return res.status(201).json({
|
||||
// Send response first
|
||||
res.status(201).json({
|
||||
tag: newTag,
|
||||
message: "Tag created successfully",
|
||||
});
|
||||
|
||||
// Then regenerate stores cache (fire-and-forget)
|
||||
createStoresFile().catch(error => {
|
||||
console.error('Failed to regenerate stores cache after tag creation:', error)
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -179,10 +186,16 @@ export const updateTag = async (req: Request, res: Response) => {
|
|||
// Reinitialize stores to reflect changes in cache
|
||||
await initializeAllStores();
|
||||
|
||||
return res.status(200).json({
|
||||
// Send response first
|
||||
res.status(200).json({
|
||||
tag: updatedTag,
|
||||
message: "Tag updated successfully",
|
||||
});
|
||||
|
||||
// Then regenerate stores cache (fire-and-forget)
|
||||
createStoresFile().catch(error => {
|
||||
console.error('Failed to regenerate stores cache after tag update:', error)
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -216,7 +229,13 @@ export const deleteTag = async (req: Request, res: Response) => {
|
|||
// Reinitialize stores to reflect changes in cache
|
||||
await initializeAllStores();
|
||||
|
||||
return res.status(200).json({
|
||||
// Send response first
|
||||
res.status(200).json({
|
||||
message: "Tag deleted successfully",
|
||||
});
|
||||
|
||||
// Then regenerate stores cache (fire-and-forget)
|
||||
createStoresFile().catch(error => {
|
||||
console.error('Failed to regenerate stores cache after tag deletion:', error)
|
||||
})
|
||||
};
|
||||
|
|
@ -7,6 +7,7 @@ import { imageUploadS3, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client"
|
|||
import { deleteS3Image } from "@/src/lib/delete-image";
|
||||
import type { SpecialDeal } from "@/src/db/types";
|
||||
import { initializeAllStores } from '@/src/stores/store-initializer';
|
||||
import { createProductsFile } from '@/src/lib/cloud_cache';
|
||||
|
||||
type CreateDeal = {
|
||||
quantity: number;
|
||||
|
|
@ -110,11 +111,17 @@ export const createProduct = async (req: Request, res: Response) => {
|
|||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
return res.status(201).json({
|
||||
// Send response first
|
||||
res.status(201).json({
|
||||
product: newProduct,
|
||||
deals: createdDeals,
|
||||
message: "Product created successfully",
|
||||
});
|
||||
|
||||
// Then regenerate products cache (fire-and-forget)
|
||||
createProductsFile().catch(error => {
|
||||
console.error('Failed to regenerate products cache after create:', error)
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -296,8 +303,14 @@ export const updateProduct = async (req: Request, res: Response) => {
|
|||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
return res.status(200).json({
|
||||
// Send response first
|
||||
res.status(200).json({
|
||||
product: updatedProduct,
|
||||
message: "Product updated successfully",
|
||||
});
|
||||
|
||||
// Then regenerate products cache (fire-and-forget)
|
||||
createProductsFile().catch(error => {
|
||||
console.error('Failed to regenerate products cache after update:', error)
|
||||
})
|
||||
};
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import axios from 'axios'
|
||||
import { scaffoldProducts } from '@/src/trpc/apis/common-apis/common'
|
||||
import { scaffoldEssentialConsts } from '@/src/trpc/apis/common-apis/common-trpc-index'
|
||||
import { scaffoldStores } from '@/src/trpc/apis/user-apis/apis/stores'
|
||||
|
|
@ -7,8 +8,13 @@ import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores
|
|||
import { storeInfo } from '@/src/db/schema'
|
||||
import { db } from '@/src/db/db_index'
|
||||
import { imageUploadS3 } from '@/src/lib/s3-client'
|
||||
import { apiCacheKey } from '@/src/lib/env-exporter'
|
||||
import { apiCacheKey, cloudflareApiToken, cloudflareZoneId, assetsDomain } from '@/src/lib/env-exporter'
|
||||
import { CACHE_FILENAMES } from '@packages/shared'
|
||||
import { retryWithExponentialBackoff } from '@/src/lib/retry'
|
||||
|
||||
function constructCacheUrl(path: string): string {
|
||||
return `${assetsDomain}${apiCacheKey}/${path}`
|
||||
}
|
||||
|
||||
export async function createProductsFile(): Promise<string> {
|
||||
// Get products data from the API method
|
||||
|
|
@ -23,6 +29,15 @@ export async function createProductsFile(): Promise<string> {
|
|||
// Upload to S3 at the specified path using apiCacheKey
|
||||
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.products}`)
|
||||
|
||||
// Purge cache with retry
|
||||
const url = constructCacheUrl(CACHE_FILENAMES.products)
|
||||
try {
|
||||
await retryWithExponentialBackoff(() => clearUrlCache([url]))
|
||||
console.log(`Cache purged for ${url}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
|
||||
}
|
||||
|
||||
return s3Key
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +54,15 @@ export async function createEssentialConstsFile(): Promise<string> {
|
|||
// Upload to S3 at the specified path using apiCacheKey
|
||||
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.essentialConsts}`)
|
||||
|
||||
// Purge cache with retry
|
||||
const url = constructCacheUrl(CACHE_FILENAMES.essentialConsts)
|
||||
try {
|
||||
await retryWithExponentialBackoff(() => clearUrlCache([url]))
|
||||
console.log(`Cache purged for ${url}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
|
||||
}
|
||||
|
||||
return s3Key
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +79,15 @@ export async function createStoresFile(): Promise<string> {
|
|||
// Upload to S3 at the specified path using apiCacheKey
|
||||
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.stores}`)
|
||||
|
||||
// Purge cache with retry
|
||||
const url = constructCacheUrl(CACHE_FILENAMES.stores)
|
||||
try {
|
||||
await retryWithExponentialBackoff(() => clearUrlCache([url]))
|
||||
console.log(`Cache purged for ${url}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
|
||||
}
|
||||
|
||||
return s3Key
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +104,15 @@ export async function createSlotsFile(): Promise<string> {
|
|||
// Upload to S3 at the specified path using apiCacheKey
|
||||
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.slots}`)
|
||||
|
||||
// Purge cache with retry
|
||||
const url = constructCacheUrl(CACHE_FILENAMES.slots)
|
||||
try {
|
||||
await retryWithExponentialBackoff(() => clearUrlCache([url]))
|
||||
console.log(`Cache purged for ${url}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
|
||||
}
|
||||
|
||||
return s3Key
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +129,15 @@ export async function createBannersFile(): Promise<string> {
|
|||
// Upload to S3 at the specified path using apiCacheKey
|
||||
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.banners}`)
|
||||
|
||||
// Purge cache with retry
|
||||
const url = constructCacheUrl(CACHE_FILENAMES.banners)
|
||||
try {
|
||||
await retryWithExponentialBackoff(() => clearUrlCache([url]))
|
||||
console.log(`Cache purged for ${url}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
|
||||
}
|
||||
|
||||
return s3Key
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +154,15 @@ export async function createStoreFile(storeId: number): Promise<string> {
|
|||
// Upload to S3 at the specified path using apiCacheKey
|
||||
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/stores/${storeId}.json`)
|
||||
|
||||
// Purge cache with retry
|
||||
const url = constructCacheUrl(`stores/${storeId}.json`)
|
||||
try {
|
||||
await retryWithExponentialBackoff(() => clearUrlCache([url]))
|
||||
console.log(`Cache purged for ${url}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
|
||||
}
|
||||
|
||||
return s3Key
|
||||
}
|
||||
|
||||
|
|
@ -110,11 +170,95 @@ export async function createAllStoresFiles(): Promise<string[]> {
|
|||
// Fetch all store IDs from database
|
||||
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
|
||||
|
||||
// Create cache files for all stores in parallel
|
||||
const results = await Promise.all(
|
||||
stores.map(store => createStoreFile(store.id))
|
||||
)
|
||||
// Create cache files for all stores and collect URLs
|
||||
const results: string[] = []
|
||||
const urls: string[] = []
|
||||
|
||||
for (const store of stores) {
|
||||
const s3Key = await createStoreFile(store.id)
|
||||
results.push(s3Key)
|
||||
urls.push(constructCacheUrl(`stores/${store.id}.json`))
|
||||
}
|
||||
|
||||
console.log(`Created ${results.length} store cache files`)
|
||||
|
||||
// Purge all store caches in one batch with retry
|
||||
try {
|
||||
await retryWithExponentialBackoff(() => clearUrlCache(urls))
|
||||
console.log(`Cache purged for ${urls.length} store files`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to purge cache for store files after 3 retries. URLs: ${urls.join(', ')}`, error)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
export async function clearUrlCache(urls: string[]): Promise<{ success: boolean; errors?: string[] }> {
|
||||
if (!cloudflareApiToken || !cloudflareZoneId) {
|
||||
console.warn('Cloudflare credentials not configured, skipping cache clear')
|
||||
return { success: false, errors: ['Cloudflare credentials not configured'] }
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`https://api.cloudflare.com/client/v4/zones/${cloudflareZoneId}/purge_cache`,
|
||||
{ files: urls },
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${cloudflareApiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const result = response.data as { success: boolean; errors?: { message: string }[] }
|
||||
|
||||
if (!result.success) {
|
||||
const errorMessages = result.errors?.map(e => e.message) || ['Unknown error']
|
||||
console.error(`Cloudflare cache purge failed for URLs: ${urls.join(', ')}`, errorMessages)
|
||||
return { success: false, errors: errorMessages }
|
||||
}
|
||||
|
||||
console.log(`Successfully purged ${urls.length} URLs from Cloudflare cache: ${urls.join(', ')}`)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
console.error(`Error clearing Cloudflare cache for URLs: ${urls.join(', ')}`, errorMessage)
|
||||
return { success: false, errors: [errorMessage] }
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearAllCache(): Promise<{ success: boolean; errors?: string[] }> {
|
||||
if (!cloudflareApiToken || !cloudflareZoneId) {
|
||||
console.warn('Cloudflare credentials not configured, skipping cache clear')
|
||||
return { success: false, errors: ['Cloudflare credentials not configured'] }
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`https://api.cloudflare.com/client/v4/zones/${cloudflareZoneId}/purge_cache`,
|
||||
{ purge_everything: true },
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${cloudflareApiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const result = response.data as { success: boolean; errors?: { message: string }[] }
|
||||
|
||||
if (!result.success) {
|
||||
const errorMessages = result.errors?.map(e => e.message) || ['Unknown error']
|
||||
console.error('Cloudflare cache purge failed:', errorMessages)
|
||||
return { success: false, errors: errorMessages }
|
||||
}
|
||||
|
||||
console.log('Successfully purged all cache from Cloudflare')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
console.error('Error clearing Cloudflare cache:', errorMessage)
|
||||
return { success: false, errors: [errorMessage] }
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,10 @@ export const assetsDomain = process.env.ASSETS_DOMAIN as string;
|
|||
|
||||
export const apiCacheKey = process.env.API_CACHE_KEY as string;
|
||||
|
||||
export const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN as string;
|
||||
|
||||
export const cloudflareZoneId = process.env.CLOUDFLARE_ZONE_ID as string;
|
||||
|
||||
export const s3Url = process.env.S3_URL as string
|
||||
|
||||
export const redisUrl = process.env.REDIS_URL as string
|
||||
|
|
|
|||
23
apps/backend/src/lib/retry.ts
Normal file
23
apps/backend/src/lib/retry.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
export async function retryWithExponentialBackoff<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxRetries: number = 3,
|
||||
delayMs: number = 1000
|
||||
): Promise<T> {
|
||||
let lastError: Error | undefined
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error))
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`Attempt ${attempt} failed, retrying in ${delayMs}ms...`)
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs))
|
||||
delayMs *= 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUr
|
|||
import { deleteS3Image } from '@/src/lib/delete-image'
|
||||
import type { SpecialDeal } from '@/src/db/types'
|
||||
import { initializeAllStores } from '@/src/stores/store-initializer'
|
||||
import { createProductsFile } from '@/src/lib/cloud_cache'
|
||||
|
||||
type CreateDeal = {
|
||||
quantity: number;
|
||||
|
|
@ -104,6 +105,11 @@ export const productRouter = router({
|
|||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
// Regenerate products cache (fire-and-forget)
|
||||
createProductsFile().catch(error => {
|
||||
console.error('Failed to regenerate products cache after delete:', error)
|
||||
})
|
||||
|
||||
return {
|
||||
message: "Product deleted successfully",
|
||||
};
|
||||
|
|
@ -191,6 +197,11 @@ export const productRouter = router({
|
|||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
// Regenerate products cache (fire-and-forget)
|
||||
createProductsFile().catch(error => {
|
||||
console.error('Failed to regenerate products cache after slot products update:', error)
|
||||
})
|
||||
|
||||
return {
|
||||
message: "Slot products updated successfully",
|
||||
added: productsToAdd.length,
|
||||
|
|
@ -393,6 +404,11 @@ export const productRouter = router({
|
|||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
// Regenerate products cache (fire-and-forget)
|
||||
createProductsFile().catch(error => {
|
||||
console.error('Failed to regenerate products cache after group creation:', error)
|
||||
})
|
||||
|
||||
return {
|
||||
group: newGroup,
|
||||
message: 'Group created successfully',
|
||||
|
|
@ -441,6 +457,11 @@ export const productRouter = router({
|
|||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
// Regenerate products cache (fire-and-forget)
|
||||
createProductsFile().catch(error => {
|
||||
console.error('Failed to regenerate products cache after group update:', error)
|
||||
})
|
||||
|
||||
return {
|
||||
group: updatedGroup,
|
||||
message: 'Group updated successfully',
|
||||
|
|
@ -467,15 +488,21 @@ export const productRouter = router({
|
|||
throw new ApiError('Group not found', 404);
|
||||
}
|
||||
|
||||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
return {
|
||||
message: 'Group deleted successfully',
|
||||
};
|
||||
}),
|
||||
// Regenerate products cache (non-blocking)
|
||||
// Regenerate products cache (fire-and-forget)
|
||||
createProductsFile().catch(error => {
|
||||
console.error('Failed to regenerate products cache after group deletion:', error)
|
||||
})
|
||||
|
||||
updateProductPrices: protectedProcedure
|
||||
return {
|
||||
message: 'Group deleted successfully',
|
||||
};
|
||||
}),
|
||||
|
||||
updateProductPrices: protectedProcedure
|
||||
.input(z.object({
|
||||
updates: z.array(z.object({
|
||||
productId: z.number(),
|
||||
|
|
@ -523,12 +550,17 @@ export const productRouter = router({
|
|||
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
return {
|
||||
message: `Updated prices for ${updates.length} product(s)`,
|
||||
updatedCount: updates.length,
|
||||
};
|
||||
}),
|
||||
});
|
||||
// Regenerate products cache (fire-and-forget)
|
||||
createProductsFile().catch(error => {
|
||||
console.error('Failed to regenerate products cache after price update:', error)
|
||||
})
|
||||
|
||||
return {
|
||||
message: `Updated prices for ${updates.length} product(s)`,
|
||||
updatedCount: updates.length,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { appUrl } from "@/src/lib/env-exporter"
|
|||
import redisClient from "@/src/lib/redis-client"
|
||||
import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
|
||||
import { initializeAllStores } from '@/src/stores/store-initializer'
|
||||
import { createSlotsFile } from '@/src/lib/cloud_cache'
|
||||
|
||||
interface CachedDeliverySequence {
|
||||
[userId: string]: number[];
|
||||
|
|
@ -217,6 +218,11 @@ export const slotsRouter = router({
|
|||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
// Regenerate slots cache (fire-and-forget)
|
||||
createSlotsFile().catch(error => {
|
||||
console.error('Failed to regenerate slots cache after slot products update:', error)
|
||||
})
|
||||
|
||||
return {
|
||||
message: "Slot products updated successfully",
|
||||
added: productsToAdd.length,
|
||||
|
|
@ -300,6 +306,11 @@ export const slotsRouter = router({
|
|||
// Reinitialize stores to reflect changes (outside transaction)
|
||||
await initializeAllStores();
|
||||
|
||||
// Regenerate slots cache (fire-and-forget)
|
||||
createSlotsFile().catch(error => {
|
||||
console.error('Failed to regenerate slots cache after slot creation:', error)
|
||||
})
|
||||
|
||||
return result;
|
||||
}),
|
||||
|
||||
|
|
@ -459,6 +470,11 @@ export const slotsRouter = router({
|
|||
// Reinitialize stores to reflect changes (outside transaction)
|
||||
await initializeAllStores();
|
||||
|
||||
// Regenerate slots cache (fire-and-forget)
|
||||
createSlotsFile().catch(error => {
|
||||
console.error('Failed to regenerate slots cache after slot update:', error)
|
||||
})
|
||||
|
||||
return result;
|
||||
}
|
||||
catch(e) {
|
||||
|
|
@ -489,6 +505,11 @@ export const slotsRouter = router({
|
|||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
// Regenerate slots cache (fire-and-forget)
|
||||
createSlotsFile().catch(error => {
|
||||
console.error('Failed to regenerate slots cache after slot deletion:', error)
|
||||
})
|
||||
|
||||
return {
|
||||
message: "Slot deleted successfully",
|
||||
};
|
||||
|
|
@ -569,6 +590,11 @@ export const slotsRouter = router({
|
|||
console.warn('Redis cache write failed:', cacheError);
|
||||
}
|
||||
|
||||
// Regenerate slots cache (fire-and-forget)
|
||||
createSlotsFile().catch(error => {
|
||||
console.error('Failed to regenerate slots cache after delivery sequence update:', error)
|
||||
})
|
||||
|
||||
return {
|
||||
slot: updatedSlot,
|
||||
message: "Delivery sequence updated successfully",
|
||||
|
|
@ -600,6 +626,11 @@ export const slotsRouter = router({
|
|||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
// Regenerate slots cache (fire-and-forget)
|
||||
createSlotsFile().catch(error => {
|
||||
console.error('Failed to regenerate slots cache after slot capacity update:', error)
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
slot: updatedSlot,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { ApiError } from '@/src/lib/api-error'
|
|||
import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { initializeAllStores } from '@/src/stores/store-initializer'
|
||||
import { createStoresFile } from '@/src/lib/cloud_cache'
|
||||
|
||||
export const storeRouter = router({
|
||||
getStores: protectedProcedure
|
||||
|
|
@ -87,6 +88,11 @@ export const storeRouter = router({
|
|||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
// Regenerate stores cache (fire-and-forget)
|
||||
createStoresFile().catch(error => {
|
||||
console.error('Failed to regenerate stores cache after store creation:', error)
|
||||
})
|
||||
|
||||
return {
|
||||
store: newStore,
|
||||
message: "Store created successfully",
|
||||
|
|
@ -163,16 +169,21 @@ export const storeRouter = router({
|
|||
}
|
||||
}
|
||||
|
||||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
// Reinitialize stores to reflect changes
|
||||
await initializeAllStores();
|
||||
|
||||
return {
|
||||
store: updatedStore,
|
||||
message: "Store updated successfully",
|
||||
};
|
||||
}),
|
||||
// Regenerate stores cache (fire-and-forget)
|
||||
createStoresFile().catch(error => {
|
||||
console.error('Failed to regenerate stores cache after store update:', error)
|
||||
})
|
||||
|
||||
deleteStore: protectedProcedure
|
||||
return {
|
||||
store: updatedStore,
|
||||
message: "Store updated successfully",
|
||||
};
|
||||
}),
|
||||
|
||||
deleteStore: protectedProcedure
|
||||
.input(z.object({
|
||||
storeId: z.number(),
|
||||
}))
|
||||
|
|
@ -201,9 +212,14 @@ export const storeRouter = router({
|
|||
};
|
||||
});
|
||||
|
||||
// Reinitialize stores to reflect changes (outside transaction)
|
||||
await initializeAllStores();
|
||||
// Reinitialize stores to reflect changes (outside transaction)
|
||||
await initializeAllStores();
|
||||
|
||||
return result;
|
||||
}),
|
||||
// Regenerate stores cache (fire-and-forget)
|
||||
createStoresFile().catch(error => {
|
||||
console.error('Failed to regenerate stores cache after store deletion:', error)
|
||||
})
|
||||
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
|
|
@ -61,6 +61,7 @@ export async function scaffoldProducts() {
|
|||
isFlashAvailable: product.isFlashAvailable,
|
||||
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
|
||||
images: product.images,
|
||||
flashPrice: product.flashPrice
|
||||
};
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import MyFlatList from "common-ui/src/components/flat-list";
|
|||
import { trpc } from "@/src/trpc-client";
|
||||
import { useAllProducts, useStores, useSlots, useGetEssentialConsts } from "@/src/hooks/prominent-api-hooks";
|
||||
import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
|
||||
import { useCentralSlotStore } from "@/src/store/centralSlotStore";
|
||||
import FloatingCartBar from "@/components/floating-cart-bar";
|
||||
import BannerCarousel from "@/components/BannerCarousel";
|
||||
import { useUserDetails } from "@/src/contexts/AuthContext";
|
||||
|
|
@ -367,6 +368,7 @@ export default function Dashboard() {
|
|||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const { backgroundColor } = useStatusBarStore();
|
||||
const { getQuickestSlot } = useProductSlotIdentifier();
|
||||
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const {
|
||||
|
|
@ -392,15 +394,18 @@ export default function Dashboard() {
|
|||
const slotB = getQuickestSlot(b.id);
|
||||
if (slotA && !slotB) return -1;
|
||||
if (!slotA && slotB) return 1;
|
||||
if (a.isOutOfStock && !b.isOutOfStock) return 1;
|
||||
if (!a.isOutOfStock && b.isOutOfStock) return -1;
|
||||
const aOutOfStock = productSlotsMap[a.id]?.isOutOfStock;
|
||||
const bOutOfStock = productSlotsMap[b.id]?.isOutOfStock;
|
||||
if (aOutOfStock && !bOutOfStock) return 1;
|
||||
if (!aOutOfStock && bOutOfStock) return -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
console.log('setting the displayed products')
|
||||
setDisplayedProducts(initialBatch);
|
||||
setHasMore(products.length > 10);
|
||||
}
|
||||
}, [productsData]);
|
||||
}, [productsData, productSlotsMap]);
|
||||
|
||||
const popularItemIds = useMemo(() => {
|
||||
const popularItems = essentialConsts?.popularItems;
|
||||
|
|
@ -507,7 +512,9 @@ export default function Dashboard() {
|
|||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let str = ''
|
||||
displayedProducts.forEach(product => str += `${product.id}-`)
|
||||
// console.log(str)
|
||||
return (
|
||||
<TabLayoutWrapper>
|
||||
<View style={searchBarContainerStyle}>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|||
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import { useCentralProductStore } from '@/src/store/centralProductStore';
|
||||
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||
import { clearLocalCart } from '@/hooks/cart-query-hooks';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FontAwesome5, FontAwesome6 } from '@expo/vector-icons';
|
||||
|
|
@ -57,16 +58,17 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
|
|||
|
||||
const products = useCentralProductStore((state) => state.products);
|
||||
const productsById = useCentralProductStore((state) => state.productsById);
|
||||
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||
|
||||
// Memoized flash-eligible product IDs
|
||||
const flashEligibleProductIds = useMemo(() => {
|
||||
if (!products.length) return new Set<number>();
|
||||
return new Set(
|
||||
products
|
||||
.filter((product) => product.isFlashAvailable)
|
||||
.filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
|
||||
.map((product) => product.id)
|
||||
);
|
||||
}, [products]);
|
||||
}, [products, productSlotsMap]);
|
||||
|
||||
const placeOrderMutation = trpc.user.order.placeOrder.useMutation({
|
||||
onSuccess: (data) => {
|
||||
|
|
@ -128,7 +130,7 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
|
|||
|
||||
const availableItems = cartItems
|
||||
.filter(item => {
|
||||
if (productsById[item.productId]?.isOutOfStock) return false;
|
||||
if (productSlotsMap[item.productId]?.isOutOfStock) return false;
|
||||
// For flash delivery, check if product supports flash delivery
|
||||
if (isFlashDelivery) {
|
||||
return flashEligibleProductIds.has(item.productId);
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
// console.log('rendering the product cart for id', item.id)
|
||||
return (
|
||||
<ContainerComp>
|
||||
<MyTouchableOpacity
|
||||
|
|
@ -137,7 +138,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
|||
<Image
|
||||
source={{ uri: item.images?.[0] }}
|
||||
style={{ width: "100%", height: itemWidth, resizeMode: "cover" }}
|
||||
/>
|
||||
/>
|
||||
{displayIsOutOfStock && (
|
||||
<View style={tw`absolute inset-0 bg-black/40 items-center justify-center`}>
|
||||
<View style={tw`bg-red-500 px-3 py-1 rounded-full`}>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { useSlots } from '@/src/hooks/prominent-api-hooks';
|
|||
import FloatingCartBar from './floating-cart-bar';
|
||||
import { useStoreHeaderStore } from '@/src/store/storeHeaderStore';
|
||||
import { useCartStore } from '@/src/store/cartStore';
|
||||
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||
|
||||
const { width: screenWidth } = Dimensions.get("window");
|
||||
const carouselWidth = screenWidth;
|
||||
|
|
@ -59,6 +60,12 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
const { setShouldNavigateToCart } = useFlashNavigationStore();
|
||||
const { setAddedToCartProduct } = useCartStore();
|
||||
const { data: slotsData } = useSlots();
|
||||
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||
|
||||
const productAvailability = useMemo(() => {
|
||||
if (!productDetail) return null;
|
||||
return productSlotsMap[productDetail.id];
|
||||
}, [productDetail, productSlotsMap]);
|
||||
|
||||
const sortedDeliverySlots = useMemo(() => {
|
||||
if (!slotsData?.slots || !productDetail) return []
|
||||
|
|
@ -102,7 +109,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
|
||||
const handleAddToCart = (productId: number) => {
|
||||
if (isFlashDelivery) {
|
||||
if (!productDetail?.isFlashAvailable) {
|
||||
if (!productAvailability?.isFlashAvailable) {
|
||||
Alert.alert("Error", "This product is not available for flash delivery");
|
||||
return;
|
||||
}
|
||||
|
|
@ -121,7 +128,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
|
||||
const handleBuyNow = (productId: number) => {
|
||||
if (isFlashDelivery) {
|
||||
if (!productDetail?.isFlashAvailable) {
|
||||
if (!productAvailability?.isFlashAvailable) {
|
||||
Alert.alert("Error", "This product is not available for flash delivery");
|
||||
return;
|
||||
}
|
||||
|
|
@ -249,13 +256,13 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
<View style={tw`flex-row justify-between items-start mb-2`}>
|
||||
<MyText style={tw`text-2xl font-bold text-gray-900 flex-1 mr-2`}>{productDetail.name}</MyText>
|
||||
<View style={tw`flex-row gap-2`}>
|
||||
{productDetail.isFlashAvailable && (
|
||||
{productAvailability?.isFlashAvailable && (
|
||||
<View style={tw`bg-pink-100 px-3 py-1 rounded-full flex-row items-center`}>
|
||||
<MaterialIcons name="bolt" size={12} color="#EC4899" style={tw`mr-1`} />
|
||||
<MyText style={tw`text-pink-700 text-xs font-bold`}>1 Hr Delivery</MyText>
|
||||
</View>
|
||||
)}
|
||||
{productDetail.isOutOfStock && (
|
||||
{productAvailability?.isOutOfStock && (
|
||||
<View style={tw`bg-red-100 px-3 py-1 rounded-full`}>
|
||||
<MyText style={tw`text-red-700 text-xs font-bold`}>Out of Stock</MyText>
|
||||
</View>
|
||||
|
|
@ -285,7 +292,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
</View>
|
||||
|
||||
{/* Flash price on separate line - smaller and less prominent */}
|
||||
{productDetail.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && (
|
||||
{productAvailability?.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && (
|
||||
<View style={tw`mt-1`}>
|
||||
<MyText style={tw`text-pink-600 text-lg font-bold`}>
|
||||
1 Hr Delivery: ₹{productDetail.flashPrice} / {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display}
|
||||
|
|
@ -312,11 +319,11 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
// Show "Add to Cart" button when not in cart
|
||||
<MyTouchableOpacity
|
||||
style={[tw`flex-1 py-3.5 rounded-xl items-center border`, {
|
||||
borderColor: (productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500,
|
||||
borderColor: (productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500,
|
||||
backgroundColor: 'white'
|
||||
}]}
|
||||
onPress={() => {
|
||||
if (productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) {
|
||||
if (productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) {
|
||||
return;
|
||||
}
|
||||
if (isFlashDelivery) {
|
||||
|
|
@ -327,10 +334,10 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
setAddedToCartProduct({ productId: productDetail.id, product: productDetail });
|
||||
}
|
||||
}}
|
||||
disabled={productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)}
|
||||
disabled={productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)}
|
||||
>
|
||||
<MyText style={[tw`font-bold text-base`, { color: (productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500 }]}>
|
||||
{(productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) ? 'Unavailable' : 'Add to Cart'}
|
||||
<MyText style={[tw`font-bold text-base`, { color: (productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500 }]}>
|
||||
{(productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) ? 'Unavailable' : 'Add to Cart'}
|
||||
</MyText>
|
||||
</MyTouchableOpacity>
|
||||
)}
|
||||
|
|
@ -338,17 +345,17 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
{isFlashDelivery ? (
|
||||
<MyTouchableOpacity
|
||||
style={[tw`flex-1 py-3.5 rounded-xl items-center shadow-md`, {
|
||||
backgroundColor: (productDetail.isOutOfStock || !productDetail.isFlashAvailable) ? '#9ca3af' : '#FDF2F8'
|
||||
backgroundColor: (productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable) ? '#9ca3af' : '#FDF2F8'
|
||||
}]}
|
||||
onPress={() => !(productDetail.isOutOfStock || !productDetail.isFlashAvailable) && handleBuyNow(productDetail.id)}
|
||||
disabled={productDetail.isOutOfStock || !productDetail.isFlashAvailable}
|
||||
onPress={() => !(productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable) && handleBuyNow(productDetail.id)}
|
||||
disabled={productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable}
|
||||
>
|
||||
<MyText style={tw`text-base font-bold ${productDetail.isOutOfStock || !productDetail.isFlashAvailable ? 'text-gray-400' : 'text-pink-600'}`}>
|
||||
{productDetail.isOutOfStock ? 'Out of Stock' :
|
||||
(!productDetail.isFlashAvailable ? 'Not Flash Eligible' : 'Get in 1 Hour')}
|
||||
<MyText style={tw`text-base font-bold ${productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable ? 'text-gray-400' : 'text-pink-600'}`}>
|
||||
{productAvailability?.isOutOfStock ? 'Out of Stock' :
|
||||
(!productAvailability?.isFlashAvailable ? 'Not Flash Eligible' : 'Get in 1 Hour')}
|
||||
</MyText>
|
||||
</MyTouchableOpacity>
|
||||
) : productDetail.isFlashAvailable ? (
|
||||
) : productAvailability?.isFlashAvailable ? (
|
||||
<MyTouchableOpacity
|
||||
style={[tw`flex-1 py-3.5 rounded-xl items-center shadow-md`, {
|
||||
backgroundColor: sortedDeliverySlots.length === 0 ? '#9ca3af' : '#FDF2F8'
|
||||
|
|
@ -386,7 +393,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
key={index}
|
||||
style={tw`flex-row items-start mb-4 bg-gray-50 p-3 rounded-xl border border-gray-100`}
|
||||
onPress={() => handleSlotAddToCart(productDetail.id, slot.id)}
|
||||
disabled={productDetail.isOutOfStock}
|
||||
disabled={productAvailability?.isOutOfStock}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} />
|
||||
|
|
@ -598,7 +605,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
key={index}
|
||||
style={tw`flex-row items-start mb-4 bg-gray-50 p-4 rounded-xl border border-gray-100`}
|
||||
onPress={() => handleSlotAddToCart(productDetail.id, slot.id)}
|
||||
disabled={productDetail.isOutOfStock}
|
||||
disabled={productAvailability?.isOutOfStock}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} />
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { tw, theme, MyText, MyTouchableOpacity, MyFlatList, AppContainer, MiniQu
|
|||
import { trpc } from '@/src/trpc-client';
|
||||
import { useAllProducts } from '@/src/hooks/prominent-api-hooks';
|
||||
import { useQuickDeliveryStore } from '@/src/store/quickDeliveryStore';
|
||||
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
|
||||
import { useHideTabNav } from '@/src/hooks/useHideTabNav';
|
||||
import CartIcon from '@/components/icons/CartIcon';
|
||||
|
|
@ -244,6 +245,7 @@ const CompactProductCard = ({
|
|||
|
||||
// Cart management for miniView
|
||||
const { data: cartData } = useGetCart({}, cartType);
|
||||
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||
const updateCartItem = useUpdateCartItem({
|
||||
showSuccessAlert: false,
|
||||
showErrorAlert: false,
|
||||
|
|
@ -257,6 +259,7 @@ const CompactProductCard = ({
|
|||
|
||||
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id);
|
||||
const quantity = cartItem?.quantity || 0;
|
||||
const isOutOfStock = productSlotsMap[item.id]?.isOutOfStock;
|
||||
|
||||
const handleQuantityChange = (newQuantity: number) => {
|
||||
if (newQuantity === 0 && cartItem) {
|
||||
|
|
@ -282,7 +285,7 @@ const CompactProductCard = ({
|
|||
source={{ uri: item.images?.[0] }}
|
||||
style={{ width: "100%", height: itemWidth, resizeMode: "cover" }}
|
||||
/>
|
||||
{item.isOutOfStock && (
|
||||
{isOutOfStock && (
|
||||
<View style={tw`absolute inset-0 bg-black/30 items-center justify-center`}>
|
||||
<MyText style={tw`text-white text-xs font-bold`}>Out of Stock</MyText>
|
||||
</View>
|
||||
|
|
@ -450,6 +453,7 @@ export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProduc
|
|||
const storeIdNum = storeId;
|
||||
|
||||
const productsQuery = useAllProducts();
|
||||
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||
|
||||
const { addToCart = () => { } } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, "flash") || {};
|
||||
|
||||
|
|
@ -490,17 +494,19 @@ export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProduc
|
|||
let flashProducts: any[] = [];
|
||||
if (storeIdNum) {
|
||||
// Filter by store, flash availability, and stock status
|
||||
flashProducts = productsQuery?.data?.products?.filter(p =>
|
||||
p.storeId === storeIdNum &&
|
||||
p.isFlashAvailable &&
|
||||
!p.isOutOfStock
|
||||
) || [];
|
||||
flashProducts = productsQuery?.data?.products?.filter(p => {
|
||||
const productInfo = productSlotsMap[p.id];
|
||||
return p.storeId === storeIdNum &&
|
||||
productInfo?.isFlashAvailable &&
|
||||
!productInfo?.isOutOfStock;
|
||||
}) || [];
|
||||
} else {
|
||||
// Show all flash-available products that are in stock
|
||||
flashProducts = productsQuery?.data?.products?.filter(p =>
|
||||
p.isFlashAvailable &&
|
||||
!p.isOutOfStock
|
||||
) || [];
|
||||
flashProducts = productsQuery?.data?.products?.filter(p => {
|
||||
const productInfo = productSlotsMap[p.id];
|
||||
return productInfo?.isFlashAvailable &&
|
||||
!productInfo?.isOutOfStock;
|
||||
}) || [];
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import TestingPhaseNote from "@/components/TestingPhaseNote";
|
|||
import dayjs from "dayjs";
|
||||
import { trpc } from "@/src/trpc-client";
|
||||
import { useCentralProductStore } from '@/src/store/centralProductStore';
|
||||
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
|
||||
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
|
||||
|
||||
|
|
@ -83,6 +84,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
|||
const { data: constsData } = useGetEssentialConsts();
|
||||
const products = useCentralProductStore((state) => state.products);
|
||||
const productsById = useCentralProductStore((state) => state.productsById);
|
||||
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||
|
||||
const cartItems = cartData?.items || [];
|
||||
|
||||
|
|
@ -92,20 +94,20 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
|||
if (!products.length) return new Set<number>();
|
||||
return new Set(
|
||||
products
|
||||
.filter((product) => product.isFlashAvailable)
|
||||
.filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
|
||||
.map((product) => product.id)
|
||||
);
|
||||
}, [products]);
|
||||
}, [products, productSlotsMap]);
|
||||
|
||||
// Base total price without discounts for coupon eligibility check
|
||||
const baseTotalPrice = useMemo(
|
||||
() =>
|
||||
cartItems
|
||||
.filter((item) => !productsById[item.productId]?.isOutOfStock)
|
||||
.reduce((sum, item) => {
|
||||
const product = productsById[item.productId];
|
||||
const price = product?.price || 0;
|
||||
return sum + price * (quantities[item.id] || item.quantity);
|
||||
const baseTotalPrice = useMemo(
|
||||
() =>
|
||||
cartItems
|
||||
.filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
|
||||
.reduce((sum, item) => {
|
||||
const product = productsById[item.productId];
|
||||
const price = product?.price || 0;
|
||||
return sum + price * (quantities[item.id] || item.quantity);
|
||||
}, 0),
|
||||
[cartItems, quantities, productsById]
|
||||
);
|
||||
|
|
@ -201,7 +203,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
|||
);
|
||||
|
||||
const totalPrice = cartItems
|
||||
.filter((item) => !productsById[item.productId]?.isOutOfStock)
|
||||
.filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
|
||||
.reduce((sum, item) => {
|
||||
const product = productsById[item.productId];
|
||||
const quantity = quantities[item.id] || item.quantity;
|
||||
|
|
@ -275,7 +277,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
|||
|
||||
const finalTotalWithDelivery = finalTotal + deliveryCharge;
|
||||
|
||||
const hasAvailableItems = cartItems.some(item => !productsById[item.productId]?.isOutOfStock);
|
||||
const hasAvailableItems = cartItems.some(item => !productSlotsMap[item.productId]?.isOutOfStock);
|
||||
|
||||
useEffect(() => {
|
||||
const initial: Record<number, number> = {};
|
||||
|
|
@ -413,10 +415,11 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
|||
const selectedSlotForItem = selectedSlots[item.id];
|
||||
const isFlashEligible = isFlashDelivery ? flashEligibleProductIds.has(item.productId) : true;
|
||||
const product = productsById[item.productId];
|
||||
const productSlotInfo = productSlotsMap[item.productId];
|
||||
// const isAvailable = (productSlots.length > 0 || isFlashDelivery) && !item.product?.isOutOfStock && isFlashEligible;
|
||||
let isAvailable = true;
|
||||
|
||||
if (product?.isOutOfStock) {
|
||||
if (productSlotInfo?.isOutOfStock) {
|
||||
isAvailable = false;
|
||||
} else if(isFlashDelivery) {
|
||||
if(!isFlashEligible) {
|
||||
|
|
@ -673,12 +676,12 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
|||
)}
|
||||
|
||||
{!isAvailable && (
|
||||
<View
|
||||
style={tw`bg-red-50 self-start px-2 py-1 rounded-md mt-2`}
|
||||
>
|
||||
<MyText style={tw`text-xs font-bold text-red-600`}>
|
||||
{product?.isOutOfStock
|
||||
? "Out of Stock"
|
||||
<View
|
||||
style={tw`bg-red-50 self-start px-2 py-1 rounded-md mt-2`}
|
||||
>
|
||||
<MyText style={tw`text-xs font-bold text-red-600`}>
|
||||
{productSlotInfo?.isOutOfStock
|
||||
? "Out of Stock"
|
||||
: isFlashDelivery && !flashEligibleProductIds.has(item.productId)
|
||||
? "Not available for flash delivery. Please remove"
|
||||
: "No delivery slots available"}
|
||||
|
|
@ -911,7 +914,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
|||
onPress={() => {
|
||||
const availableItems = cartItems
|
||||
.filter(item => {
|
||||
if (productsById[item.productId]?.isOutOfStock) return false;
|
||||
if (productSlotsMap[item.productId]?.isOutOfStock) return false;
|
||||
if (isFlashDelivery) {
|
||||
// Check if product supports flash delivery
|
||||
return flashEligibleProductIds.has(item.productId);
|
||||
|
|
@ -920,12 +923,10 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
|||
})
|
||||
.map(item => item.id);
|
||||
|
||||
|
||||
|
||||
if (availableItems.length === 0) {
|
||||
// Determine why no items are available
|
||||
const outOfStockItems = cartItems.filter(item => productsById[item.productId]?.isOutOfStock);
|
||||
const inStockItems = cartItems.filter(item => !productsById[item.productId]?.isOutOfStock);
|
||||
if (availableItems.length === 0) {
|
||||
// Determine why no items are available
|
||||
const outOfStockItems = cartItems.filter(item => productSlotsMap[item.productId]?.isOutOfStock);
|
||||
const inStockItems = cartItems.filter(item => !productSlotsMap[item.productId]?.isOutOfStock);
|
||||
|
||||
let errorTitle = "Cannot Proceed";
|
||||
let errorMessage = "";
|
||||
|
|
@ -962,9 +963,9 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if there are items without slots (for regular delivery)
|
||||
if (!isFlashDelivery && availableItems.length < cartItems.length) {
|
||||
const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !productsById[item.productId]?.isOutOfStock);
|
||||
// Check if there are items without slots (for regular delivery)
|
||||
if (!isFlashDelivery && availableItems.length < cartItems.length) {
|
||||
const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !productSlotsMap[item.productId]?.isOutOfStock);
|
||||
if (itemsWithoutSlots.length > 0) {
|
||||
Alert.alert(
|
||||
"Delivery Slot Required",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useAuthenticatedRoute } from '@/hooks/useAuthenticatedRoute';
|
|||
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import { useCentralProductStore } from '@/src/store/centralProductStore';
|
||||
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||
import { useGetCart } from '@/hooks/cart-query-hooks';
|
||||
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
|
||||
import PaymentAndOrderComponent from '@/components/PaymentAndOrderComponent';
|
||||
|
|
@ -38,6 +39,7 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
|
|||
const { data: constsData } = useGetEssentialConsts();
|
||||
const products = useCentralProductStore((state) => state.products);
|
||||
const productsById = useCentralProductStore((state) => state.productsById);
|
||||
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||
|
||||
useMarkDataFetchers(() => {
|
||||
refetchCart();
|
||||
|
|
@ -53,15 +55,15 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
|
|||
|
||||
const cartItems = cartData?.items || [];
|
||||
|
||||
// Memoized flash-eligible product IDs
|
||||
const flashEligibleProductIds = useMemo(() => {
|
||||
if (!products.length) return new Set<number>();
|
||||
return new Set(
|
||||
products
|
||||
.filter((product) => product.isFlashAvailable)
|
||||
.map((product) => product.id)
|
||||
);
|
||||
}, [products]);
|
||||
// Memoized flash-eligible product IDs
|
||||
const flashEligibleProductIds = useMemo(() => {
|
||||
if (!products.length) return new Set<number>();
|
||||
return new Set(
|
||||
products
|
||||
.filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
|
||||
.map((product) => product.id)
|
||||
);
|
||||
}, [products, productSlotsMap]);
|
||||
|
||||
// Parse slots parameter from URL (format: "1:1,2,3;2:4,5")
|
||||
const selectedSlots = useMemo(() => {
|
||||
|
|
@ -125,7 +127,7 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
|
|||
|
||||
|
||||
const totalPrice = selectedItems
|
||||
.filter((item) => !productsById[item.productId]?.isOutOfStock)
|
||||
.filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
|
||||
.reduce(
|
||||
(sum, item) => {
|
||||
const product = productsById[item.productId];
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ export interface CartItem {
|
|||
productId: number;
|
||||
quantity: number;
|
||||
addedAt: string;
|
||||
product: ProductSummary;
|
||||
subtotal: number;
|
||||
slotId: number;
|
||||
}
|
||||
|
|
@ -119,20 +118,6 @@ interface UseRemoveFromCartReturn {
|
|||
removeFromCartAsync: (itemId: number) => Promise<LocalCartItem[]>;
|
||||
}
|
||||
|
||||
interface AllProductsResponse {
|
||||
products: Array<{
|
||||
id: number;
|
||||
price: number;
|
||||
incrementStep: number;
|
||||
marketPrice?: number | null;
|
||||
name?: string;
|
||||
flashPrice?: string | null;
|
||||
images?: string[];
|
||||
productQuantity?: number;
|
||||
unitNotation?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const getLocalCart = async (cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
|
||||
const key = getCartStorageKey(cartType);
|
||||
const data = await StorageServiceCasual.getItem(key);
|
||||
|
|
@ -199,7 +184,7 @@ const clearLocalCart = async (cartType: CartType = "regular"): Promise<void> =>
|
|||
};
|
||||
|
||||
export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType = "regular"): UseGetCartReturn {
|
||||
const { data: products } = useAllProducts() as { data: AllProductsResponse | undefined };
|
||||
const { data: products } = useAllProducts();
|
||||
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||
|
||||
const query: UseQueryResult<CartData, Error> = useQuery({
|
||||
|
|
@ -236,12 +221,6 @@ export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType =
|
|||
productId: cartItem.productId,
|
||||
quantity: cartItem.quantity,
|
||||
addedAt: cartItem.addedAt,
|
||||
product: {
|
||||
...productBasic,
|
||||
isOutOfStock: productAvailability.isOutOfStock,
|
||||
isFlashAvailable: productAvailability.isFlashAvailable,
|
||||
},
|
||||
incrementStep: productBasic.incrementStep,
|
||||
subtotal: Number(productBasic.price) * cartItem.quantity,
|
||||
slotId: cartItem.slotId,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { tw, BottomDialog, MyText, MyTouchableOpacity, Quantifier } from 'common
|
|||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useCartStore } from '@/src/store/cartStore';
|
||||
import { useFlashCartStore } from '@/src/store/flashCartStore';
|
||||
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
|
||||
import { useGetEssentialConsts, useSlots } from '@/src/hooks/prominent-api-hooks';
|
||||
import dayjs from 'dayjs';
|
||||
|
|
@ -33,6 +34,7 @@ export default function AddToCartDialog() {
|
|||
const { data: slotsData } = useSlots();
|
||||
const { data: cartData } = useGetCart();
|
||||
const { data: constsData } = useGetEssentialConsts();
|
||||
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||
// const isFlashDeliveryEnabled = constsData?.isFlashDeliveryEnabled === true;
|
||||
const isFlashDeliveryEnabled = true;
|
||||
|
||||
|
|
@ -112,7 +114,7 @@ export default function AddToCartDialog() {
|
|||
const isUpdate = (cartItem?.quantity || 0) >= 1;
|
||||
|
||||
// Check if flash delivery option should be shown
|
||||
const showFlashOption = product?.isFlashAvailable === true && isFlashDeliveryEnabled;
|
||||
const showFlashOption = productSlotsMap[product?.id]?.isFlashAvailable === true && isFlashDeliveryEnabled;
|
||||
|
||||
const handleAddToCart = () => {
|
||||
if (selectedFlashDelivery) {
|
||||
|
|
|
|||
|
|
@ -63,10 +63,10 @@ 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.5:4000';
|
||||
const BASE_API_URL = 'http://192.168.1.5:4000';
|
||||
// let BASE_API_URL = "https://mf.freshyo.in";
|
||||
// let BASE_API_URL = "https://freshyo.technocracy.ovh";
|
||||
let BASE_API_URL = 'http://192.168.100.107:4000';
|
||||
// let BASE_API_URL = 'http://192.168.100.107:4000';
|
||||
// let BASE_API_URL = 'http://192.168.29.176:4000';
|
||||
|
||||
// if(isDevMode) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue