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
|
JWT_SECRET=my_meatfarmer_jwt_secret_key
|
||||||
ASSETS_DOMAIN=https://assets.freshyo.in/
|
ASSETS_DOMAIN=https://assets.freshyo.in/
|
||||||
API_CACHE_KEY=api-cache-dev
|
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@5.223.55.14:6379
|
||||||
REDIS_URL=redis://default:redis_shafi_password@57.128.212.174:6379
|
REDIS_URL=redis://default:redis_shafi_password@57.128.212.174:6379
|
||||||
APP_URL=http://localhost:4000
|
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 { imageUploadS3, generateSignedUrlFromS3Url } from "@/src/lib/s3-client";
|
||||||
import { deleteS3Image } from "@/src/lib/delete-image";
|
import { deleteS3Image } from "@/src/lib/delete-image";
|
||||||
import { initializeAllStores } from '@/src/stores/store-initializer';
|
import { initializeAllStores } from '@/src/stores/store-initializer';
|
||||||
|
import { createStoresFile } from '@/src/lib/cloud_cache';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new product tag
|
* Create a new product tag
|
||||||
|
|
@ -60,10 +61,16 @@ export const createTag = async (req: Request, res: Response) => {
|
||||||
// Reinitialize stores to reflect changes in cache
|
// Reinitialize stores to reflect changes in cache
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
return res.status(201).json({
|
// Send response first
|
||||||
|
res.status(201).json({
|
||||||
tag: newTag,
|
tag: newTag,
|
||||||
message: "Tag created successfully",
|
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
|
// Reinitialize stores to reflect changes in cache
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
return res.status(200).json({
|
// Send response first
|
||||||
|
res.status(200).json({
|
||||||
tag: updatedTag,
|
tag: updatedTag,
|
||||||
message: "Tag updated successfully",
|
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
|
// Reinitialize stores to reflect changes in cache
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
return res.status(200).json({
|
// Send response first
|
||||||
|
res.status(200).json({
|
||||||
message: "Tag deleted successfully",
|
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 { deleteS3Image } from "@/src/lib/delete-image";
|
||||||
import type { SpecialDeal } from "@/src/db/types";
|
import type { SpecialDeal } from "@/src/db/types";
|
||||||
import { initializeAllStores } from '@/src/stores/store-initializer';
|
import { initializeAllStores } from '@/src/stores/store-initializer';
|
||||||
|
import { createProductsFile } from '@/src/lib/cloud_cache';
|
||||||
|
|
||||||
type CreateDeal = {
|
type CreateDeal = {
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
|
@ -110,11 +111,17 @@ export const createProduct = async (req: Request, res: Response) => {
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
return res.status(201).json({
|
// Send response first
|
||||||
|
res.status(201).json({
|
||||||
product: newProduct,
|
product: newProduct,
|
||||||
deals: createdDeals,
|
deals: createdDeals,
|
||||||
message: "Product created successfully",
|
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
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
return res.status(200).json({
|
// Send response first
|
||||||
|
res.status(200).json({
|
||||||
product: updatedProduct,
|
product: updatedProduct,
|
||||||
message: "Product updated successfully",
|
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 { scaffoldProducts } from '@/src/trpc/apis/common-apis/common'
|
||||||
import { scaffoldEssentialConsts } from '@/src/trpc/apis/common-apis/common-trpc-index'
|
import { scaffoldEssentialConsts } from '@/src/trpc/apis/common-apis/common-trpc-index'
|
||||||
import { scaffoldStores } from '@/src/trpc/apis/user-apis/apis/stores'
|
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 { storeInfo } from '@/src/db/schema'
|
||||||
import { db } from '@/src/db/db_index'
|
import { db } from '@/src/db/db_index'
|
||||||
import { imageUploadS3 } from '@/src/lib/s3-client'
|
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 { 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> {
|
export async function createProductsFile(): Promise<string> {
|
||||||
// Get products data from the API method
|
// 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
|
// Upload to S3 at the specified path using apiCacheKey
|
||||||
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.products}`)
|
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
|
return s3Key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +54,15 @@ export async function createEssentialConstsFile(): Promise<string> {
|
||||||
// Upload to S3 at the specified path using apiCacheKey
|
// Upload to S3 at the specified path using apiCacheKey
|
||||||
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.essentialConsts}`)
|
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
|
return s3Key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,6 +79,15 @@ export async function createStoresFile(): Promise<string> {
|
||||||
// Upload to S3 at the specified path using apiCacheKey
|
// Upload to S3 at the specified path using apiCacheKey
|
||||||
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.stores}`)
|
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
|
return s3Key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,6 +104,15 @@ export async function createSlotsFile(): Promise<string> {
|
||||||
// Upload to S3 at the specified path using apiCacheKey
|
// Upload to S3 at the specified path using apiCacheKey
|
||||||
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.slots}`)
|
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
|
return s3Key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,6 +129,15 @@ export async function createBannersFile(): Promise<string> {
|
||||||
// Upload to S3 at the specified path using apiCacheKey
|
// Upload to S3 at the specified path using apiCacheKey
|
||||||
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.banners}`)
|
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
|
return s3Key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,6 +154,15 @@ export async function createStoreFile(storeId: number): Promise<string> {
|
||||||
// Upload to S3 at the specified path using apiCacheKey
|
// Upload to S3 at the specified path using apiCacheKey
|
||||||
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/stores/${storeId}.json`)
|
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
|
return s3Key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,11 +170,95 @@ export async function createAllStoresFiles(): Promise<string[]> {
|
||||||
// Fetch all store IDs from database
|
// Fetch all store IDs from database
|
||||||
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
|
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
|
||||||
|
|
||||||
// Create cache files for all stores in parallel
|
// Create cache files for all stores and collect URLs
|
||||||
const results = await Promise.all(
|
const results: string[] = []
|
||||||
stores.map(store => createStoreFile(store.id))
|
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`)
|
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
|
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 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 s3Url = process.env.S3_URL as string
|
||||||
|
|
||||||
export const redisUrl = process.env.REDIS_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 { deleteS3Image } from '@/src/lib/delete-image'
|
||||||
import type { SpecialDeal } from '@/src/db/types'
|
import type { SpecialDeal } from '@/src/db/types'
|
||||||
import { initializeAllStores } from '@/src/stores/store-initializer'
|
import { initializeAllStores } from '@/src/stores/store-initializer'
|
||||||
|
import { createProductsFile } from '@/src/lib/cloud_cache'
|
||||||
|
|
||||||
type CreateDeal = {
|
type CreateDeal = {
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
|
@ -104,6 +105,11 @@ export const productRouter = router({
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate products cache (fire-and-forget)
|
||||||
|
createProductsFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate products cache after delete:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: "Product deleted successfully",
|
message: "Product deleted successfully",
|
||||||
};
|
};
|
||||||
|
|
@ -191,6 +197,11 @@ export const productRouter = router({
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate products cache (fire-and-forget)
|
||||||
|
createProductsFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate products cache after slot products update:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: "Slot products updated successfully",
|
message: "Slot products updated successfully",
|
||||||
added: productsToAdd.length,
|
added: productsToAdd.length,
|
||||||
|
|
@ -393,6 +404,11 @@ export const productRouter = router({
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate products cache (fire-and-forget)
|
||||||
|
createProductsFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate products cache after group creation:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
group: newGroup,
|
group: newGroup,
|
||||||
message: 'Group created successfully',
|
message: 'Group created successfully',
|
||||||
|
|
@ -441,6 +457,11 @@ export const productRouter = router({
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate products cache (fire-and-forget)
|
||||||
|
createProductsFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate products cache after group update:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
group: updatedGroup,
|
group: updatedGroup,
|
||||||
message: 'Group updated successfully',
|
message: 'Group updated successfully',
|
||||||
|
|
@ -470,6 +491,12 @@ export const productRouter = router({
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: 'Group deleted successfully',
|
message: 'Group deleted successfully',
|
||||||
};
|
};
|
||||||
|
|
@ -526,6 +553,11 @@ export const productRouter = router({
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate products cache (fire-and-forget)
|
||||||
|
createProductsFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate products cache after price update:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: `Updated prices for ${updates.length} product(s)`,
|
message: `Updated prices for ${updates.length} product(s)`,
|
||||||
updatedCount: updates.length,
|
updatedCount: updates.length,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { appUrl } from "@/src/lib/env-exporter"
|
||||||
import redisClient from "@/src/lib/redis-client"
|
import redisClient from "@/src/lib/redis-client"
|
||||||
import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
|
import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
|
||||||
import { initializeAllStores } from '@/src/stores/store-initializer'
|
import { initializeAllStores } from '@/src/stores/store-initializer'
|
||||||
|
import { createSlotsFile } from '@/src/lib/cloud_cache'
|
||||||
|
|
||||||
interface CachedDeliverySequence {
|
interface CachedDeliverySequence {
|
||||||
[userId: string]: number[];
|
[userId: string]: number[];
|
||||||
|
|
@ -217,6 +218,11 @@ export const slotsRouter = router({
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate slots cache (fire-and-forget)
|
||||||
|
createSlotsFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate slots cache after slot products update:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: "Slot products updated successfully",
|
message: "Slot products updated successfully",
|
||||||
added: productsToAdd.length,
|
added: productsToAdd.length,
|
||||||
|
|
@ -300,6 +306,11 @@ export const slotsRouter = router({
|
||||||
// Reinitialize stores to reflect changes (outside transaction)
|
// Reinitialize stores to reflect changes (outside transaction)
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate slots cache (fire-and-forget)
|
||||||
|
createSlotsFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate slots cache after slot creation:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
@ -459,6 +470,11 @@ export const slotsRouter = router({
|
||||||
// Reinitialize stores to reflect changes (outside transaction)
|
// Reinitialize stores to reflect changes (outside transaction)
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate slots cache (fire-and-forget)
|
||||||
|
createSlotsFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate slots cache after slot update:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch(e) {
|
catch(e) {
|
||||||
|
|
@ -489,6 +505,11 @@ export const slotsRouter = router({
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate slots cache (fire-and-forget)
|
||||||
|
createSlotsFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate slots cache after slot deletion:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: "Slot deleted successfully",
|
message: "Slot deleted successfully",
|
||||||
};
|
};
|
||||||
|
|
@ -569,6 +590,11 @@ export const slotsRouter = router({
|
||||||
console.warn('Redis cache write failed:', cacheError);
|
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 {
|
return {
|
||||||
slot: updatedSlot,
|
slot: updatedSlot,
|
||||||
message: "Delivery sequence updated successfully",
|
message: "Delivery sequence updated successfully",
|
||||||
|
|
@ -600,6 +626,11 @@ export const slotsRouter = router({
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate slots cache (fire-and-forget)
|
||||||
|
createSlotsFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate slots cache after slot capacity update:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
slot: updatedSlot,
|
slot: updatedSlot,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { ApiError } from '@/src/lib/api-error'
|
||||||
import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
||||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||||
import { initializeAllStores } from '@/src/stores/store-initializer'
|
import { initializeAllStores } from '@/src/stores/store-initializer'
|
||||||
|
import { createStoresFile } from '@/src/lib/cloud_cache'
|
||||||
|
|
||||||
export const storeRouter = router({
|
export const storeRouter = router({
|
||||||
getStores: protectedProcedure
|
getStores: protectedProcedure
|
||||||
|
|
@ -87,6 +88,11 @@ export const storeRouter = router({
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate stores cache (fire-and-forget)
|
||||||
|
createStoresFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate stores cache after store creation:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
store: newStore,
|
store: newStore,
|
||||||
message: "Store created successfully",
|
message: "Store created successfully",
|
||||||
|
|
@ -166,6 +172,11 @@ export const storeRouter = router({
|
||||||
// Reinitialize stores to reflect changes
|
// Reinitialize stores to reflect changes
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate stores cache (fire-and-forget)
|
||||||
|
createStoresFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate stores cache after store update:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
store: updatedStore,
|
store: updatedStore,
|
||||||
message: "Store updated successfully",
|
message: "Store updated successfully",
|
||||||
|
|
@ -204,6 +215,11 @@ export const storeRouter = router({
|
||||||
// Reinitialize stores to reflect changes (outside transaction)
|
// Reinitialize stores to reflect changes (outside transaction)
|
||||||
await initializeAllStores();
|
await initializeAllStores();
|
||||||
|
|
||||||
|
// Regenerate stores cache (fire-and-forget)
|
||||||
|
createStoresFile().catch(error => {
|
||||||
|
console.error('Failed to regenerate stores cache after store deletion:', error)
|
||||||
|
})
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -61,6 +61,7 @@ export async function scaffoldProducts() {
|
||||||
isFlashAvailable: product.isFlashAvailable,
|
isFlashAvailable: product.isFlashAvailable,
|
||||||
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
|
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
|
||||||
images: product.images,
|
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 { trpc } from "@/src/trpc-client";
|
||||||
import { useAllProducts, useStores, useSlots, useGetEssentialConsts } from "@/src/hooks/prominent-api-hooks";
|
import { useAllProducts, useStores, useSlots, useGetEssentialConsts } from "@/src/hooks/prominent-api-hooks";
|
||||||
import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
|
import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
|
||||||
|
import { useCentralSlotStore } from "@/src/store/centralSlotStore";
|
||||||
import FloatingCartBar from "@/components/floating-cart-bar";
|
import FloatingCartBar from "@/components/floating-cart-bar";
|
||||||
import BannerCarousel from "@/components/BannerCarousel";
|
import BannerCarousel from "@/components/BannerCarousel";
|
||||||
import { useUserDetails } from "@/src/contexts/AuthContext";
|
import { useUserDetails } from "@/src/contexts/AuthContext";
|
||||||
|
|
@ -367,6 +368,7 @@ export default function Dashboard() {
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
const { backgroundColor } = useStatusBarStore();
|
const { backgroundColor } = useStatusBarStore();
|
||||||
const { getQuickestSlot } = useProductSlotIdentifier();
|
const { getQuickestSlot } = useProductSlotIdentifier();
|
||||||
|
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -392,15 +394,18 @@ export default function Dashboard() {
|
||||||
const slotB = getQuickestSlot(b.id);
|
const slotB = getQuickestSlot(b.id);
|
||||||
if (slotA && !slotB) return -1;
|
if (slotA && !slotB) return -1;
|
||||||
if (!slotA && slotB) return 1;
|
if (!slotA && slotB) return 1;
|
||||||
if (a.isOutOfStock && !b.isOutOfStock) return 1;
|
const aOutOfStock = productSlotsMap[a.id]?.isOutOfStock;
|
||||||
if (!a.isOutOfStock && b.isOutOfStock) return -1;
|
const bOutOfStock = productSlotsMap[b.id]?.isOutOfStock;
|
||||||
|
if (aOutOfStock && !bOutOfStock) return 1;
|
||||||
|
if (!aOutOfStock && bOutOfStock) return -1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('setting the displayed products')
|
||||||
setDisplayedProducts(initialBatch);
|
setDisplayedProducts(initialBatch);
|
||||||
setHasMore(products.length > 10);
|
setHasMore(products.length > 10);
|
||||||
}
|
}
|
||||||
}, [productsData]);
|
}, [productsData, productSlotsMap]);
|
||||||
|
|
||||||
const popularItemIds = useMemo(() => {
|
const popularItemIds = useMemo(() => {
|
||||||
const popularItems = essentialConsts?.popularItems;
|
const popularItems = essentialConsts?.popularItems;
|
||||||
|
|
@ -507,7 +512,9 @@ export default function Dashboard() {
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let str = ''
|
||||||
|
displayedProducts.forEach(product => str += `${product.id}-`)
|
||||||
|
// console.log(str)
|
||||||
return (
|
return (
|
||||||
<TabLayoutWrapper>
|
<TabLayoutWrapper>
|
||||||
<View style={searchBarContainerStyle}>
|
<View style={searchBarContainerStyle}>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
|
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import { useCentralProductStore } from '@/src/store/centralProductStore';
|
import { useCentralProductStore } from '@/src/store/centralProductStore';
|
||||||
|
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||||
import { clearLocalCart } from '@/hooks/cart-query-hooks';
|
import { clearLocalCart } from '@/hooks/cart-query-hooks';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { FontAwesome5, FontAwesome6 } from '@expo/vector-icons';
|
import { FontAwesome5, FontAwesome6 } from '@expo/vector-icons';
|
||||||
|
|
@ -57,16 +58,17 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
|
||||||
|
|
||||||
const products = useCentralProductStore((state) => state.products);
|
const products = useCentralProductStore((state) => state.products);
|
||||||
const productsById = useCentralProductStore((state) => state.productsById);
|
const productsById = useCentralProductStore((state) => state.productsById);
|
||||||
|
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||||
|
|
||||||
// Memoized flash-eligible product IDs
|
// Memoized flash-eligible product IDs
|
||||||
const flashEligibleProductIds = useMemo(() => {
|
const flashEligibleProductIds = useMemo(() => {
|
||||||
if (!products.length) return new Set<number>();
|
if (!products.length) return new Set<number>();
|
||||||
return new Set(
|
return new Set(
|
||||||
products
|
products
|
||||||
.filter((product) => product.isFlashAvailable)
|
.filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
|
||||||
.map((product) => product.id)
|
.map((product) => product.id)
|
||||||
);
|
);
|
||||||
}, [products]);
|
}, [products, productSlotsMap]);
|
||||||
|
|
||||||
const placeOrderMutation = trpc.user.order.placeOrder.useMutation({
|
const placeOrderMutation = trpc.user.order.placeOrder.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
|
@ -128,7 +130,7 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
|
||||||
|
|
||||||
const availableItems = cartItems
|
const availableItems = cartItems
|
||||||
.filter(item => {
|
.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
|
// For flash delivery, check if product supports flash delivery
|
||||||
if (isFlashDelivery) {
|
if (isFlashDelivery) {
|
||||||
return flashEligibleProductIds.has(item.productId);
|
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 (
|
return (
|
||||||
<ContainerComp>
|
<ContainerComp>
|
||||||
<MyTouchableOpacity
|
<MyTouchableOpacity
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { useSlots } from '@/src/hooks/prominent-api-hooks';
|
||||||
import FloatingCartBar from './floating-cart-bar';
|
import FloatingCartBar from './floating-cart-bar';
|
||||||
import { useStoreHeaderStore } from '@/src/store/storeHeaderStore';
|
import { useStoreHeaderStore } from '@/src/store/storeHeaderStore';
|
||||||
import { useCartStore } from '@/src/store/cartStore';
|
import { useCartStore } from '@/src/store/cartStore';
|
||||||
|
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||||
|
|
||||||
const { width: screenWidth } = Dimensions.get("window");
|
const { width: screenWidth } = Dimensions.get("window");
|
||||||
const carouselWidth = screenWidth;
|
const carouselWidth = screenWidth;
|
||||||
|
|
@ -59,6 +60,12 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
||||||
const { setShouldNavigateToCart } = useFlashNavigationStore();
|
const { setShouldNavigateToCart } = useFlashNavigationStore();
|
||||||
const { setAddedToCartProduct } = useCartStore();
|
const { setAddedToCartProduct } = useCartStore();
|
||||||
const { data: slotsData } = useSlots();
|
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(() => {
|
const sortedDeliverySlots = useMemo(() => {
|
||||||
if (!slotsData?.slots || !productDetail) return []
|
if (!slotsData?.slots || !productDetail) return []
|
||||||
|
|
@ -102,7 +109,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
||||||
|
|
||||||
const handleAddToCart = (productId: number) => {
|
const handleAddToCart = (productId: number) => {
|
||||||
if (isFlashDelivery) {
|
if (isFlashDelivery) {
|
||||||
if (!productDetail?.isFlashAvailable) {
|
if (!productAvailability?.isFlashAvailable) {
|
||||||
Alert.alert("Error", "This product is not available for flash delivery");
|
Alert.alert("Error", "This product is not available for flash delivery");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +128,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
||||||
|
|
||||||
const handleBuyNow = (productId: number) => {
|
const handleBuyNow = (productId: number) => {
|
||||||
if (isFlashDelivery) {
|
if (isFlashDelivery) {
|
||||||
if (!productDetail?.isFlashAvailable) {
|
if (!productAvailability?.isFlashAvailable) {
|
||||||
Alert.alert("Error", "This product is not available for flash delivery");
|
Alert.alert("Error", "This product is not available for flash delivery");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -249,13 +256,13 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
||||||
<View style={tw`flex-row justify-between items-start mb-2`}>
|
<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>
|
<MyText style={tw`text-2xl font-bold text-gray-900 flex-1 mr-2`}>{productDetail.name}</MyText>
|
||||||
<View style={tw`flex-row gap-2`}>
|
<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`}>
|
<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`} />
|
<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>
|
<MyText style={tw`text-pink-700 text-xs font-bold`}>1 Hr Delivery</MyText>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{productDetail.isOutOfStock && (
|
{productAvailability?.isOutOfStock && (
|
||||||
<View style={tw`bg-red-100 px-3 py-1 rounded-full`}>
|
<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>
|
<MyText style={tw`text-red-700 text-xs font-bold`}>Out of Stock</MyText>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -285,7 +292,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Flash price on separate line - smaller and less prominent */}
|
{/* 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`}>
|
<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`}>
|
||||||
1 Hr Delivery: ₹{productDetail.flashPrice} / {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display}
|
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
|
// Show "Add to Cart" button when not in cart
|
||||||
<MyTouchableOpacity
|
<MyTouchableOpacity
|
||||||
style={[tw`flex-1 py-3.5 rounded-xl items-center border`, {
|
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'
|
backgroundColor: 'white'
|
||||||
}]}
|
}]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) {
|
if (productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isFlashDelivery) {
|
if (isFlashDelivery) {
|
||||||
|
|
@ -327,10 +334,10 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
||||||
setAddedToCartProduct({ productId: productDetail.id, product: productDetail });
|
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 }]}>
|
<MyText style={[tw`font-bold text-base`, { color: (productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500 }]}>
|
||||||
{(productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) ? 'Unavailable' : 'Add to Cart'}
|
{(productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) ? 'Unavailable' : 'Add to Cart'}
|
||||||
</MyText>
|
</MyText>
|
||||||
</MyTouchableOpacity>
|
</MyTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
@ -338,17 +345,17 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
||||||
{isFlashDelivery ? (
|
{isFlashDelivery ? (
|
||||||
<MyTouchableOpacity
|
<MyTouchableOpacity
|
||||||
style={[tw`flex-1 py-3.5 rounded-xl items-center shadow-md`, {
|
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)}
|
onPress={() => !(productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable) && handleBuyNow(productDetail.id)}
|
||||||
disabled={productDetail.isOutOfStock || !productDetail.isFlashAvailable}
|
disabled={productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable}
|
||||||
>
|
>
|
||||||
<MyText style={tw`text-base font-bold ${productDetail.isOutOfStock || !productDetail.isFlashAvailable ? 'text-gray-400' : 'text-pink-600'}`}>
|
<MyText style={tw`text-base font-bold ${productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable ? 'text-gray-400' : 'text-pink-600'}`}>
|
||||||
{productDetail.isOutOfStock ? 'Out of Stock' :
|
{productAvailability?.isOutOfStock ? 'Out of Stock' :
|
||||||
(!productDetail.isFlashAvailable ? 'Not Flash Eligible' : 'Get in 1 Hour')}
|
(!productAvailability?.isFlashAvailable ? 'Not Flash Eligible' : 'Get in 1 Hour')}
|
||||||
</MyText>
|
</MyText>
|
||||||
</MyTouchableOpacity>
|
</MyTouchableOpacity>
|
||||||
) : productDetail.isFlashAvailable ? (
|
) : productAvailability?.isFlashAvailable ? (
|
||||||
<MyTouchableOpacity
|
<MyTouchableOpacity
|
||||||
style={[tw`flex-1 py-3.5 rounded-xl items-center shadow-md`, {
|
style={[tw`flex-1 py-3.5 rounded-xl items-center shadow-md`, {
|
||||||
backgroundColor: sortedDeliverySlots.length === 0 ? '#9ca3af' : '#FDF2F8'
|
backgroundColor: sortedDeliverySlots.length === 0 ? '#9ca3af' : '#FDF2F8'
|
||||||
|
|
@ -386,7 +393,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
||||||
key={index}
|
key={index}
|
||||||
style={tw`flex-row items-start mb-4 bg-gray-50 p-3 rounded-xl border border-gray-100`}
|
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)}
|
onPress={() => handleSlotAddToCart(productDetail.id, slot.id)}
|
||||||
disabled={productDetail.isOutOfStock}
|
disabled={productAvailability?.isOutOfStock}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} />
|
<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}
|
key={index}
|
||||||
style={tw`flex-row items-start mb-4 bg-gray-50 p-4 rounded-xl border border-gray-100`}
|
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)}
|
onPress={() => handleSlotAddToCart(productDetail.id, slot.id)}
|
||||||
disabled={productDetail.isOutOfStock}
|
disabled={productAvailability?.isOutOfStock}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} />
|
<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 { trpc } from '@/src/trpc-client';
|
||||||
import { useAllProducts } from '@/src/hooks/prominent-api-hooks';
|
import { useAllProducts } from '@/src/hooks/prominent-api-hooks';
|
||||||
import { useQuickDeliveryStore } from '@/src/store/quickDeliveryStore';
|
import { useQuickDeliveryStore } from '@/src/store/quickDeliveryStore';
|
||||||
|
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||||
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
|
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
|
||||||
import { useHideTabNav } from '@/src/hooks/useHideTabNav';
|
import { useHideTabNav } from '@/src/hooks/useHideTabNav';
|
||||||
import CartIcon from '@/components/icons/CartIcon';
|
import CartIcon from '@/components/icons/CartIcon';
|
||||||
|
|
@ -244,6 +245,7 @@ const CompactProductCard = ({
|
||||||
|
|
||||||
// Cart management for miniView
|
// Cart management for miniView
|
||||||
const { data: cartData } = useGetCart({}, cartType);
|
const { data: cartData } = useGetCart({}, cartType);
|
||||||
|
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||||
const updateCartItem = useUpdateCartItem({
|
const updateCartItem = useUpdateCartItem({
|
||||||
showSuccessAlert: false,
|
showSuccessAlert: false,
|
||||||
showErrorAlert: false,
|
showErrorAlert: false,
|
||||||
|
|
@ -257,6 +259,7 @@ const CompactProductCard = ({
|
||||||
|
|
||||||
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id);
|
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id);
|
||||||
const quantity = cartItem?.quantity || 0;
|
const quantity = cartItem?.quantity || 0;
|
||||||
|
const isOutOfStock = productSlotsMap[item.id]?.isOutOfStock;
|
||||||
|
|
||||||
const handleQuantityChange = (newQuantity: number) => {
|
const handleQuantityChange = (newQuantity: number) => {
|
||||||
if (newQuantity === 0 && cartItem) {
|
if (newQuantity === 0 && cartItem) {
|
||||||
|
|
@ -282,7 +285,7 @@ const CompactProductCard = ({
|
||||||
source={{ uri: item.images?.[0] }}
|
source={{ uri: item.images?.[0] }}
|
||||||
style={{ width: "100%", height: itemWidth, resizeMode: "cover" }}
|
style={{ width: "100%", height: itemWidth, resizeMode: "cover" }}
|
||||||
/>
|
/>
|
||||||
{item.isOutOfStock && (
|
{isOutOfStock && (
|
||||||
<View style={tw`absolute inset-0 bg-black/30 items-center justify-center`}>
|
<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>
|
<MyText style={tw`text-white text-xs font-bold`}>Out of Stock</MyText>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -450,6 +453,7 @@ export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProduc
|
||||||
const storeIdNum = storeId;
|
const storeIdNum = storeId;
|
||||||
|
|
||||||
const productsQuery = useAllProducts();
|
const productsQuery = useAllProducts();
|
||||||
|
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||||
|
|
||||||
const { addToCart = () => { } } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, "flash") || {};
|
const { addToCart = () => { } } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, "flash") || {};
|
||||||
|
|
||||||
|
|
@ -490,17 +494,19 @@ export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProduc
|
||||||
let flashProducts: any[] = [];
|
let flashProducts: any[] = [];
|
||||||
if (storeIdNum) {
|
if (storeIdNum) {
|
||||||
// Filter by store, flash availability, and stock status
|
// Filter by store, flash availability, and stock status
|
||||||
flashProducts = productsQuery?.data?.products?.filter(p =>
|
flashProducts = productsQuery?.data?.products?.filter(p => {
|
||||||
p.storeId === storeIdNum &&
|
const productInfo = productSlotsMap[p.id];
|
||||||
p.isFlashAvailable &&
|
return p.storeId === storeIdNum &&
|
||||||
!p.isOutOfStock
|
productInfo?.isFlashAvailable &&
|
||||||
) || [];
|
!productInfo?.isOutOfStock;
|
||||||
|
}) || [];
|
||||||
} else {
|
} else {
|
||||||
// Show all flash-available products that are in stock
|
// Show all flash-available products that are in stock
|
||||||
flashProducts = productsQuery?.data?.products?.filter(p =>
|
flashProducts = productsQuery?.data?.products?.filter(p => {
|
||||||
p.isFlashAvailable &&
|
const productInfo = productSlotsMap[p.id];
|
||||||
!p.isOutOfStock
|
return productInfo?.isFlashAvailable &&
|
||||||
) || [];
|
!productInfo?.isOutOfStock;
|
||||||
|
}) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import TestingPhaseNote from "@/components/TestingPhaseNote";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { trpc } from "@/src/trpc-client";
|
import { trpc } from "@/src/trpc-client";
|
||||||
import { useCentralProductStore } from '@/src/store/centralProductStore';
|
import { useCentralProductStore } from '@/src/store/centralProductStore';
|
||||||
|
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||||
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
|
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
|
||||||
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-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 { data: constsData } = useGetEssentialConsts();
|
||||||
const products = useCentralProductStore((state) => state.products);
|
const products = useCentralProductStore((state) => state.products);
|
||||||
const productsById = useCentralProductStore((state) => state.productsById);
|
const productsById = useCentralProductStore((state) => state.productsById);
|
||||||
|
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||||
|
|
||||||
const cartItems = cartData?.items || [];
|
const cartItems = cartData?.items || [];
|
||||||
|
|
||||||
|
|
@ -92,16 +94,16 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
if (!products.length) return new Set<number>();
|
if (!products.length) return new Set<number>();
|
||||||
return new Set(
|
return new Set(
|
||||||
products
|
products
|
||||||
.filter((product) => product.isFlashAvailable)
|
.filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
|
||||||
.map((product) => product.id)
|
.map((product) => product.id)
|
||||||
);
|
);
|
||||||
}, [products]);
|
}, [products, productSlotsMap]);
|
||||||
|
|
||||||
// Base total price without discounts for coupon eligibility check
|
// Base total price without discounts for coupon eligibility check
|
||||||
const baseTotalPrice = useMemo(
|
const baseTotalPrice = useMemo(
|
||||||
() =>
|
() =>
|
||||||
cartItems
|
cartItems
|
||||||
.filter((item) => !productsById[item.productId]?.isOutOfStock)
|
.filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
|
||||||
.reduce((sum, item) => {
|
.reduce((sum, item) => {
|
||||||
const product = productsById[item.productId];
|
const product = productsById[item.productId];
|
||||||
const price = product?.price || 0;
|
const price = product?.price || 0;
|
||||||
|
|
@ -201,7 +203,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalPrice = cartItems
|
const totalPrice = cartItems
|
||||||
.filter((item) => !productsById[item.productId]?.isOutOfStock)
|
.filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
|
||||||
.reduce((sum, item) => {
|
.reduce((sum, item) => {
|
||||||
const product = productsById[item.productId];
|
const product = productsById[item.productId];
|
||||||
const quantity = quantities[item.id] || item.quantity;
|
const quantity = quantities[item.id] || item.quantity;
|
||||||
|
|
@ -275,7 +277,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
|
|
||||||
const finalTotalWithDelivery = finalTotal + deliveryCharge;
|
const finalTotalWithDelivery = finalTotal + deliveryCharge;
|
||||||
|
|
||||||
const hasAvailableItems = cartItems.some(item => !productsById[item.productId]?.isOutOfStock);
|
const hasAvailableItems = cartItems.some(item => !productSlotsMap[item.productId]?.isOutOfStock);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initial: Record<number, number> = {};
|
const initial: Record<number, number> = {};
|
||||||
|
|
@ -413,10 +415,11 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
const selectedSlotForItem = selectedSlots[item.id];
|
const selectedSlotForItem = selectedSlots[item.id];
|
||||||
const isFlashEligible = isFlashDelivery ? flashEligibleProductIds.has(item.productId) : true;
|
const isFlashEligible = isFlashDelivery ? flashEligibleProductIds.has(item.productId) : true;
|
||||||
const product = productsById[item.productId];
|
const product = productsById[item.productId];
|
||||||
|
const productSlotInfo = productSlotsMap[item.productId];
|
||||||
// const isAvailable = (productSlots.length > 0 || isFlashDelivery) && !item.product?.isOutOfStock && isFlashEligible;
|
// const isAvailable = (productSlots.length > 0 || isFlashDelivery) && !item.product?.isOutOfStock && isFlashEligible;
|
||||||
let isAvailable = true;
|
let isAvailable = true;
|
||||||
|
|
||||||
if (product?.isOutOfStock) {
|
if (productSlotInfo?.isOutOfStock) {
|
||||||
isAvailable = false;
|
isAvailable = false;
|
||||||
} else if(isFlashDelivery) {
|
} else if(isFlashDelivery) {
|
||||||
if(!isFlashEligible) {
|
if(!isFlashEligible) {
|
||||||
|
|
@ -677,7 +680,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
style={tw`bg-red-50 self-start px-2 py-1 rounded-md mt-2`}
|
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`}>
|
<MyText style={tw`text-xs font-bold text-red-600`}>
|
||||||
{product?.isOutOfStock
|
{productSlotInfo?.isOutOfStock
|
||||||
? "Out of Stock"
|
? "Out of Stock"
|
||||||
: isFlashDelivery && !flashEligibleProductIds.has(item.productId)
|
: isFlashDelivery && !flashEligibleProductIds.has(item.productId)
|
||||||
? "Not available for flash delivery. Please remove"
|
? "Not available for flash delivery. Please remove"
|
||||||
|
|
@ -911,7 +914,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const availableItems = cartItems
|
const availableItems = cartItems
|
||||||
.filter(item => {
|
.filter(item => {
|
||||||
if (productsById[item.productId]?.isOutOfStock) return false;
|
if (productSlotsMap[item.productId]?.isOutOfStock) return false;
|
||||||
if (isFlashDelivery) {
|
if (isFlashDelivery) {
|
||||||
// Check if product supports flash delivery
|
// Check if product supports flash delivery
|
||||||
return flashEligibleProductIds.has(item.productId);
|
return flashEligibleProductIds.has(item.productId);
|
||||||
|
|
@ -920,12 +923,10 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
})
|
})
|
||||||
.map(item => item.id);
|
.map(item => item.id);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (availableItems.length === 0) {
|
if (availableItems.length === 0) {
|
||||||
// Determine why no items are available
|
// Determine why no items are available
|
||||||
const outOfStockItems = cartItems.filter(item => productsById[item.productId]?.isOutOfStock);
|
const outOfStockItems = cartItems.filter(item => productSlotsMap[item.productId]?.isOutOfStock);
|
||||||
const inStockItems = cartItems.filter(item => !productsById[item.productId]?.isOutOfStock);
|
const inStockItems = cartItems.filter(item => !productSlotsMap[item.productId]?.isOutOfStock);
|
||||||
|
|
||||||
let errorTitle = "Cannot Proceed";
|
let errorTitle = "Cannot Proceed";
|
||||||
let errorMessage = "";
|
let errorMessage = "";
|
||||||
|
|
@ -964,7 +965,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
|
|
||||||
// Check if there are items without slots (for regular delivery)
|
// Check if there are items without slots (for regular delivery)
|
||||||
if (!isFlashDelivery && availableItems.length < cartItems.length) {
|
if (!isFlashDelivery && availableItems.length < cartItems.length) {
|
||||||
const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !productsById[item.productId]?.isOutOfStock);
|
const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !productSlotsMap[item.productId]?.isOutOfStock);
|
||||||
if (itemsWithoutSlots.length > 0) {
|
if (itemsWithoutSlots.length > 0) {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Delivery Slot Required",
|
"Delivery Slot Required",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { useAuthenticatedRoute } from '@/hooks/useAuthenticatedRoute';
|
||||||
|
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import { useCentralProductStore } from '@/src/store/centralProductStore';
|
import { useCentralProductStore } from '@/src/store/centralProductStore';
|
||||||
|
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||||
import { useGetCart } from '@/hooks/cart-query-hooks';
|
import { useGetCart } from '@/hooks/cart-query-hooks';
|
||||||
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
|
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
|
||||||
import PaymentAndOrderComponent from '@/components/PaymentAndOrderComponent';
|
import PaymentAndOrderComponent from '@/components/PaymentAndOrderComponent';
|
||||||
|
|
@ -38,6 +39,7 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
|
||||||
const { data: constsData } = useGetEssentialConsts();
|
const { data: constsData } = useGetEssentialConsts();
|
||||||
const products = useCentralProductStore((state) => state.products);
|
const products = useCentralProductStore((state) => state.products);
|
||||||
const productsById = useCentralProductStore((state) => state.productsById);
|
const productsById = useCentralProductStore((state) => state.productsById);
|
||||||
|
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||||
|
|
||||||
useMarkDataFetchers(() => {
|
useMarkDataFetchers(() => {
|
||||||
refetchCart();
|
refetchCart();
|
||||||
|
|
@ -58,10 +60,10 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
|
||||||
if (!products.length) return new Set<number>();
|
if (!products.length) return new Set<number>();
|
||||||
return new Set(
|
return new Set(
|
||||||
products
|
products
|
||||||
.filter((product) => product.isFlashAvailable)
|
.filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
|
||||||
.map((product) => product.id)
|
.map((product) => product.id)
|
||||||
);
|
);
|
||||||
}, [products]);
|
}, [products, productSlotsMap]);
|
||||||
|
|
||||||
// Parse slots parameter from URL (format: "1:1,2,3;2:4,5")
|
// Parse slots parameter from URL (format: "1:1,2,3;2:4,5")
|
||||||
const selectedSlots = useMemo(() => {
|
const selectedSlots = useMemo(() => {
|
||||||
|
|
@ -125,7 +127,7 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
|
||||||
|
|
||||||
|
|
||||||
const totalPrice = selectedItems
|
const totalPrice = selectedItems
|
||||||
.filter((item) => !productsById[item.productId]?.isOutOfStock)
|
.filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
|
||||||
.reduce(
|
.reduce(
|
||||||
(sum, item) => {
|
(sum, item) => {
|
||||||
const product = productsById[item.productId];
|
const product = productsById[item.productId];
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ export interface CartItem {
|
||||||
productId: number;
|
productId: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
addedAt: string;
|
addedAt: string;
|
||||||
product: ProductSummary;
|
|
||||||
subtotal: number;
|
subtotal: number;
|
||||||
slotId: number;
|
slotId: number;
|
||||||
}
|
}
|
||||||
|
|
@ -119,20 +118,6 @@ interface UseRemoveFromCartReturn {
|
||||||
removeFromCartAsync: (itemId: number) => Promise<LocalCartItem[]>;
|
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 getLocalCart = async (cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
|
||||||
const key = getCartStorageKey(cartType);
|
const key = getCartStorageKey(cartType);
|
||||||
const data = await StorageServiceCasual.getItem(key);
|
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 {
|
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 productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||||
|
|
||||||
const query: UseQueryResult<CartData, Error> = useQuery({
|
const query: UseQueryResult<CartData, Error> = useQuery({
|
||||||
|
|
@ -236,12 +221,6 @@ export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType =
|
||||||
productId: cartItem.productId,
|
productId: cartItem.productId,
|
||||||
quantity: cartItem.quantity,
|
quantity: cartItem.quantity,
|
||||||
addedAt: cartItem.addedAt,
|
addedAt: cartItem.addedAt,
|
||||||
product: {
|
|
||||||
...productBasic,
|
|
||||||
isOutOfStock: productAvailability.isOutOfStock,
|
|
||||||
isFlashAvailable: productAvailability.isFlashAvailable,
|
|
||||||
},
|
|
||||||
incrementStep: productBasic.incrementStep,
|
|
||||||
subtotal: Number(productBasic.price) * cartItem.quantity,
|
subtotal: Number(productBasic.price) * cartItem.quantity,
|
||||||
slotId: cartItem.slotId,
|
slotId: cartItem.slotId,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { tw, BottomDialog, MyText, MyTouchableOpacity, Quantifier } from 'common
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
import { useCartStore } from '@/src/store/cartStore';
|
import { useCartStore } from '@/src/store/cartStore';
|
||||||
import { useFlashCartStore } from '@/src/store/flashCartStore';
|
import { useFlashCartStore } from '@/src/store/flashCartStore';
|
||||||
|
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
|
||||||
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
|
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
|
||||||
import { useGetEssentialConsts, useSlots } from '@/src/hooks/prominent-api-hooks';
|
import { useGetEssentialConsts, useSlots } from '@/src/hooks/prominent-api-hooks';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
@ -33,6 +34,7 @@ export default function AddToCartDialog() {
|
||||||
const { data: slotsData } = useSlots();
|
const { data: slotsData } = useSlots();
|
||||||
const { data: cartData } = useGetCart();
|
const { data: cartData } = useGetCart();
|
||||||
const { data: constsData } = useGetEssentialConsts();
|
const { data: constsData } = useGetEssentialConsts();
|
||||||
|
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||||
// const isFlashDeliveryEnabled = constsData?.isFlashDeliveryEnabled === true;
|
// const isFlashDeliveryEnabled = constsData?.isFlashDeliveryEnabled === true;
|
||||||
const isFlashDeliveryEnabled = true;
|
const isFlashDeliveryEnabled = true;
|
||||||
|
|
||||||
|
|
@ -112,7 +114,7 @@ export default function AddToCartDialog() {
|
||||||
const isUpdate = (cartItem?.quantity || 0) >= 1;
|
const isUpdate = (cartItem?.quantity || 0) >= 1;
|
||||||
|
|
||||||
// Check if flash delivery option should be shown
|
// Check if flash delivery option should be shown
|
||||||
const showFlashOption = product?.isFlashAvailable === true && isFlashDeliveryEnabled;
|
const showFlashOption = productSlotsMap[product?.id]?.isFlashAvailable === true && isFlashDeliveryEnabled;
|
||||||
|
|
||||||
const handleAddToCart = () => {
|
const handleAddToCart = () => {
|
||||||
if (selectedFlashDelivery) {
|
if (selectedFlashDelivery) {
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,10 @@ 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.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://mf.freshyo.in";
|
||||||
// let BASE_API_URL = "https://freshyo.technocracy.ovh";
|
// 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';
|
// let BASE_API_URL = 'http://192.168.29.176:4000';
|
||||||
|
|
||||||
// if(isDevMode) {
|
// if(isDevMode) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue