Compare commits
No commits in common. "b41980736a51dc2463ea0e6637ee434fa98f04ba" and "db6cc71bdb24cee10a1f656600abebb98304be13" have entirely different histories.
b41980736a
...
db6cc71bdb
26 changed files with 1156 additions and 276 deletions
|
|
@ -10,4 +10,3 @@
|
|||
- Prefers embedding related data into existing cache files over creating or calling separate API endpoints. Confidence: 0.9
|
||||
- Prefers using icons from Hicons (rather than custom/hand-authored SVGs or other icon libraries). Confidence: 0.9
|
||||
- When a feature or page is repurposed, wants file names, route names, and code references renamed to match the new purpose (not just UI content/icons). Confidence: 0.8
|
||||
- Wants analysis findings and plans persisted in a markdown file before any implementation begins, with explicit confirmation that implementation is paused until instructed. Confidence: 0.8
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
"wrangler:dev": "wrangler dev worker.ts --config wrangler.toml",
|
||||
"wrangler:deploy": "wrangler deploy worker.ts --config wrangler.toml",
|
||||
"pull_db": "rm -rf .wrangler/state/v3/d1/* && wrangler d1 export freshyo-dev --config wrangler.prod.toml --remote --output ./dumps/latest.sql && bash ./scripts/populate_localdb.sh",
|
||||
"pull_dev_db": "rm -rf .wrangler/state/v3/d1/* && wrangler d1 export freshyo-backend-dev --config wrangler.dev.toml --remote --output ./dumps/latest.sql && bash ./scripts/populate_localdb.sh --dev",
|
||||
"docker:build": "cd .. && docker buildx build --platform linux/amd64 -t mohdshafiuddin54/health_petal:latest --progress=plain -f backend/Dockerfile .",
|
||||
"docker:push": "docker push mohdshafiuddin54/health_petal:latest"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ export type {
|
|||
UserAddressDeleteResponse,
|
||||
UserBanner,
|
||||
UserBannersResponse,
|
||||
UserCartProduct,
|
||||
UserCartItem,
|
||||
UserCartResponse,
|
||||
UserComplaint,
|
||||
UserComplaintsResponse,
|
||||
UserRaiseComplaintResponse,
|
||||
|
|
@ -119,6 +122,7 @@ export type {
|
|||
UserCreateReviewResponse,
|
||||
UserSlotProduct,
|
||||
UserSlotWithProducts,
|
||||
UserSlotData,
|
||||
UserSlotAvailability,
|
||||
UserDeliverySlot,
|
||||
UserSlotsResponse,
|
||||
|
|
@ -132,6 +136,7 @@ export type {
|
|||
UserAuthResult,
|
||||
UserOtpVerifyResponse,
|
||||
UserPasswordUpdateResponse,
|
||||
UserProfileResponse,
|
||||
UserDeleteAccountResponse,
|
||||
UserCouponUsage,
|
||||
UserCouponApplicableUser,
|
||||
|
|
@ -151,6 +156,8 @@ export type {
|
|||
UserOrderDetail,
|
||||
UserCancelOrderResponse,
|
||||
UserUpdateNotesResponse,
|
||||
UserRecentProduct,
|
||||
UserRecentProductsResponse,
|
||||
// Store types
|
||||
StoreSummary,
|
||||
StoresSummaryResponse,
|
||||
|
|
|
|||
|
|
@ -157,6 +157,15 @@
|
|||
// hasOngoingOrdersForAddress,
|
||||
// // User - Banners
|
||||
// getUserActiveBanners,
|
||||
// // User - Cart
|
||||
// getUserCartItemsWithProducts,
|
||||
// getUserProductById,
|
||||
// getUserCartItemByUserProduct,
|
||||
// incrementUserCartItemQuantity,
|
||||
// insertUserCartItem,
|
||||
// updateUserCartItemQuantity,
|
||||
// deleteUserCartItem,
|
||||
// clearUserCart,
|
||||
// // User - Complaint
|
||||
// getUserComplaints,
|
||||
// createUserComplaint,
|
||||
|
|
@ -226,6 +235,9 @@
|
|||
// getUserOrderBasic,
|
||||
// cancelUserOrderTransaction,
|
||||
// updateUserOrderNotes,
|
||||
// getUserRecentlyDeliveredOrderIds,
|
||||
// getUserProductIdsFromOrders,
|
||||
// getUserProductsForRecentOrders,
|
||||
// // Store Helpers
|
||||
// getAllBannersForCache,
|
||||
// getAllProductsForCache,
|
||||
|
|
|
|||
|
|
@ -168,6 +168,15 @@ export {
|
|||
hasOngoingOrdersForAddress,
|
||||
// User - Banners
|
||||
getUserActiveBanners,
|
||||
// User - Cart
|
||||
getUserCartItemsWithProducts,
|
||||
getUserProductById,
|
||||
getUserCartItemByUserProduct,
|
||||
incrementUserCartItemQuantity,
|
||||
insertUserCartItem,
|
||||
updateUserCartItemQuantity,
|
||||
deleteUserCartItem,
|
||||
clearUserCart,
|
||||
// User - Complaint
|
||||
getUserComplaints,
|
||||
createUserComplaint,
|
||||
|
|
@ -237,6 +246,9 @@ export {
|
|||
getUserOrderBasic,
|
||||
cancelUserOrderTransaction,
|
||||
updateUserOrderNotes,
|
||||
getUserRecentlyDeliveredOrderIds,
|
||||
getUserProductIdsFromOrders,
|
||||
getUserProductsForRecentOrders,
|
||||
// Store Helpers
|
||||
getAllBannersForCache,
|
||||
getAllProductsForCache,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import type {
|
|||
UserAuthResponse,
|
||||
UserOtpVerifyResponse,
|
||||
UserPasswordUpdateResponse,
|
||||
UserProfileResponse,
|
||||
UserDeleteAccountResponse,
|
||||
} from '@packages/shared'
|
||||
|
||||
|
|
@ -395,6 +396,31 @@ export const authRouter = router({
|
|||
}
|
||||
}),
|
||||
|
||||
getProfile: protectedProcedure
|
||||
.query(async ({ ctx }): Promise<UserProfileResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new ApiError('User not authenticated', 401);
|
||||
}
|
||||
|
||||
const user = await getUserAuthByIdInDb(userId)
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError('User not found', 404);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
mobile: user.mobile,
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
||||
deleteAccount: protectedProcedure
|
||||
.input(z.object({
|
||||
mobile: z.string().min(10, 'Mobile number is required'),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { publicProcedure, router } from '@/src/trpc/trpc-index'
|
||||
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||
import { getUserActiveBanners as getUserActiveBannersInDb } from '@/src/dbService'
|
||||
import type { UserBannersResponse } from '@packages/shared'
|
||||
|
|
@ -22,3 +23,11 @@ export async function scaffoldBanners(): Promise<UserBannersResponse> {
|
|||
banners: bannersWithSignedUrls,
|
||||
}
|
||||
}
|
||||
|
||||
export const bannerRouter = router({
|
||||
getBanners: publicProcedure
|
||||
.query(async () => {
|
||||
const response = await scaffoldBanners();
|
||||
return response;
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,264 @@
|
|||
import { router, publicProcedure } from '@/src/trpc/trpc-index'
|
||||
import { router, protectedProcedure, publicProcedure } from '@/src/trpc/trpc-index'
|
||||
import { z } from 'zod'
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||
import { getMultipleProductsSlots } from '@/src/stores/slot-store'
|
||||
import {
|
||||
getUserCartItemsWithProducts as getUserCartItemsWithProductsInDb,
|
||||
getUserProductById as getUserProductByIdInDb,
|
||||
getUserCartItemByUserProduct as getUserCartItemByUserProductInDb,
|
||||
incrementUserCartItemQuantity as incrementUserCartItemQuantityInDb,
|
||||
insertUserCartItem as insertUserCartItemInDb,
|
||||
updateUserCartItemQuantity as updateUserCartItemQuantityInDb,
|
||||
deleteUserCartItem as deleteUserCartItemInDb,
|
||||
clearUserCart as clearUserCartInDb,
|
||||
} from '@/src/dbService'
|
||||
import type { UserCartResponse } from '@packages/shared'
|
||||
|
||||
const getCartData = async (userId: number): Promise<UserCartResponse> => {
|
||||
const cartItemsWithProducts = await getUserCartItemsWithProductsInDb(userId)
|
||||
|
||||
/*
|
||||
// Old implementation - direct DB queries:
|
||||
const cartItemsWithProducts = await db
|
||||
.select({
|
||||
cartId: cartItems.id,
|
||||
productId: productInfo.id,
|
||||
productName: productInfo.name,
|
||||
productPrice: productInfo.price,
|
||||
productImages: productInfo.images,
|
||||
productQuantity: productInfo.productQuantity,
|
||||
isOutOfStock: productInfo.isOutOfStock,
|
||||
unitShortNotation: units.shortNotation,
|
||||
quantity: cartItems.quantity,
|
||||
addedAt: cartItems.addedAt,
|
||||
})
|
||||
.from(cartItems)
|
||||
.innerJoin(productInfo, eq(cartItems.productId, productInfo.id))
|
||||
.innerJoin(units, eq(productInfo.unitId, units.id))
|
||||
.where(eq(cartItems.userId, userId));
|
||||
*/
|
||||
|
||||
const cartWithSignedUrls = cartItemsWithProducts.map((item) => ({
|
||||
...item,
|
||||
product: {
|
||||
...item.product,
|
||||
images: scaffoldAssetUrl(item.product.images || []),
|
||||
},
|
||||
}))
|
||||
|
||||
const totalAmount = cartWithSignedUrls.reduce((sum, item) => sum + item.subtotal, 0)
|
||||
|
||||
return {
|
||||
items: cartWithSignedUrls,
|
||||
totalItems: cartWithSignedUrls.length,
|
||||
totalAmount,
|
||||
}
|
||||
}
|
||||
|
||||
export const cartRouter = router({
|
||||
getCart: protectedProcedure
|
||||
.query(async ({ ctx }): Promise<UserCartResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
return await getCartData(userId);
|
||||
}),
|
||||
|
||||
addToCart: protectedProcedure
|
||||
.input(z.object({
|
||||
productId: z.number().int().positive(),
|
||||
quantity: z.number().int().positive(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }): Promise<UserCartResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
const { productId, quantity } = input;
|
||||
|
||||
// Validate input
|
||||
if (!productId || !quantity || quantity <= 0) {
|
||||
throw new ApiError("Product ID and positive quantity required", 400);
|
||||
}
|
||||
|
||||
// Check if product exists
|
||||
const product = await getUserProductByIdInDb(productId)
|
||||
|
||||
if (!product) {
|
||||
throw new ApiError('Product not found', 404)
|
||||
}
|
||||
|
||||
const existingItem = await getUserCartItemByUserProductInDb(userId, productId)
|
||||
|
||||
if (existingItem) {
|
||||
await incrementUserCartItemQuantityInDb(existingItem.id, quantity)
|
||||
} else {
|
||||
await insertUserCartItemInDb(userId, productId, quantity)
|
||||
}
|
||||
|
||||
/*
|
||||
// Old implementation - direct DB queries:
|
||||
const product = await db.query.productInfo.findFirst({
|
||||
where: eq(productInfo.id, productId),
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new ApiError("Product not found", 404);
|
||||
}
|
||||
|
||||
const existingItem = await db.query.cartItems.findFirst({
|
||||
where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)),
|
||||
});
|
||||
|
||||
if (existingItem) {
|
||||
await db.update(cartItems)
|
||||
.set({
|
||||
quantity: sql`${cartItems.quantity} + ${quantity}`,
|
||||
})
|
||||
.where(eq(cartItems.id, existingItem.id));
|
||||
} else {
|
||||
await db.insert(cartItems).values({
|
||||
userId,
|
||||
productId,
|
||||
quantity: quantity.toString(),
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
// Return updated cart
|
||||
return await getCartData(userId)
|
||||
}),
|
||||
|
||||
updateCartItem: protectedProcedure
|
||||
.input(z.object({
|
||||
itemId: z.number().int().positive(),
|
||||
quantity: z.number().int().min(0),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }): Promise<UserCartResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
const { itemId, quantity } = input;
|
||||
|
||||
if (!quantity || quantity <= 0) {
|
||||
throw new ApiError("Positive quantity required", 400);
|
||||
}
|
||||
|
||||
const updated = await updateUserCartItemQuantityInDb(userId, itemId, quantity)
|
||||
|
||||
/*
|
||||
// Old implementation - direct DB queries:
|
||||
const [updatedItem] = await db.update(cartItems)
|
||||
.set({ quantity: quantity.toString() })
|
||||
.where(and(
|
||||
eq(cartItems.id, itemId),
|
||||
eq(cartItems.userId, userId)
|
||||
))
|
||||
.returning();
|
||||
|
||||
if (!updatedItem) {
|
||||
throw new ApiError("Cart item not found", 404);
|
||||
}
|
||||
*/
|
||||
|
||||
if (!updated) {
|
||||
throw new ApiError('Cart item not found', 404)
|
||||
}
|
||||
|
||||
// Return updated cart
|
||||
return await getCartData(userId)
|
||||
}),
|
||||
|
||||
removeFromCart: protectedProcedure
|
||||
.input(z.object({
|
||||
itemId: z.number().int().positive(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }): Promise<UserCartResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
const { itemId } = input;
|
||||
|
||||
const deleted = await deleteUserCartItemInDb(userId, itemId)
|
||||
|
||||
/*
|
||||
// Old implementation - direct DB queries:
|
||||
const [deletedItem] = await db.delete(cartItems)
|
||||
.where(and(
|
||||
eq(cartItems.id, itemId),
|
||||
eq(cartItems.userId, userId)
|
||||
))
|
||||
.returning();
|
||||
|
||||
if (!deletedItem) {
|
||||
throw new ApiError("Cart item not found", 404);
|
||||
}
|
||||
*/
|
||||
|
||||
if (!deleted) {
|
||||
throw new ApiError('Cart item not found', 404)
|
||||
}
|
||||
|
||||
// Return updated cart
|
||||
return await getCartData(userId)
|
||||
}),
|
||||
|
||||
clearCart: protectedProcedure
|
||||
.mutation(async ({ ctx }): Promise<UserCartResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
await clearUserCartInDb(userId)
|
||||
|
||||
/*
|
||||
// Old implementation - direct DB query:
|
||||
await db.delete(cartItems).where(eq(cartItems.userId, userId));
|
||||
*/
|
||||
|
||||
return {
|
||||
items: [],
|
||||
totalItems: 0,
|
||||
totalAmount: 0,
|
||||
message: "Cart cleared successfully",
|
||||
}
|
||||
}),
|
||||
|
||||
// Original DB-based getCartSlots (commented out)
|
||||
// getCartSlots: publicProcedure
|
||||
// .input(z.object({
|
||||
// productIds: z.array(z.number().int().positive())
|
||||
// }))
|
||||
// .query(async ({ input }) => {
|
||||
// const { productIds } = input;
|
||||
//
|
||||
// if (productIds.length === 0) {
|
||||
// return {};
|
||||
// }
|
||||
//
|
||||
// // Get slots for these products where freeze time is after current time
|
||||
// const slotsData = await db
|
||||
// .select({
|
||||
// productId: productSlots.productId,
|
||||
// slotId: deliverySlotInfo.id,
|
||||
// deliveryTime: deliverySlotInfo.deliveryTime,
|
||||
// freezeTime: deliverySlotInfo.freezeTime,
|
||||
// isActive: deliverySlotInfo.isActive,
|
||||
// })
|
||||
// .from(productSlots)
|
||||
// .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
|
||||
// .where(and(
|
||||
// inArray(productSlots.productId, productIds),
|
||||
// gt(deliverySlotInfo.freezeTime, sql`NOW()`),
|
||||
// eq(deliverySlotInfo.isActive, true)
|
||||
// ));
|
||||
//
|
||||
// // Group by productId
|
||||
// const result: Record<number, any[]> = {};
|
||||
// slotsData.forEach(slot => {
|
||||
// if (!result[slot.productId]) {
|
||||
// result[slot.productId] = [];
|
||||
// }
|
||||
// result[slot.productId].push({
|
||||
// id: slot.slotId,
|
||||
// deliveryTime: slot.deliveryTime,
|
||||
// freezeTime: slot.freezeTime,
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// return result;
|
||||
// }),
|
||||
|
||||
// Cache-based getCartSlots
|
||||
getCartSlots: publicProcedure
|
||||
.input(z.object({
|
||||
|
|
|
|||
|
|
@ -86,6 +86,57 @@ export const userCouponRouter = router({
|
|||
}
|
||||
}),
|
||||
|
||||
getProductCoupons: protectedProcedure
|
||||
.input(z.object({ skuId: z.number().int().positive() }))
|
||||
.query(async ({ input, ctx }): Promise<UserEligibleCouponsResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
const { skuId } = input;
|
||||
|
||||
// Get all active, non-expired coupons
|
||||
const allCoupons = await getUserActiveCouponsWithRelationsInDb(userId)
|
||||
|
||||
/*
|
||||
// Old implementation - direct DB queries:
|
||||
const allCoupons = await db.query.coupons.findMany({
|
||||
where: and(
|
||||
eq(coupons.isInvalidated, false),
|
||||
or(
|
||||
isNull(coupons.validTill),
|
||||
gt(coupons.validTill, new Date())
|
||||
)
|
||||
),
|
||||
with: {
|
||||
usages: {
|
||||
where: eq(couponUsage.userId, userId)
|
||||
},
|
||||
applicableUsers: {
|
||||
with: {
|
||||
user: true
|
||||
}
|
||||
},
|
||||
applicableProducts: {
|
||||
with: {
|
||||
product: true
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// Filter to only coupons applicable to current user and product
|
||||
const applicableCoupons = allCoupons.filter(coupon => {
|
||||
const applicableUsers = coupon.applicableUsers || [];
|
||||
const userApplicable = !coupon.isUserBased || applicableUsers.some(au => au.userId === userId);
|
||||
|
||||
const applicableProducts = coupon.applicableProducts || [];
|
||||
const productApplicable = applicableProducts.length === 0 || applicableProducts.some(ap => ap.skuId === skuId);
|
||||
|
||||
return userApplicable && productApplicable;
|
||||
});
|
||||
|
||||
return { success: true, data: applicableCoupons };
|
||||
}),
|
||||
|
||||
getMyCoupons: protectedProcedure
|
||||
.query(async ({ ctx }): Promise<UserMyCouponsResponse> => {
|
||||
const userId = ctx.user.userId;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import {
|
|||
getUserOrderByIdWithRelations,
|
||||
getUserOrderCount,
|
||||
getUserOrdersWithRelations,
|
||||
getUserProductIdsFromOrders,
|
||||
getUserProductsForRecentOrders,
|
||||
getUserRecentlyDeliveredOrderIds,
|
||||
getUserSlotCapacityStatus,
|
||||
orders,
|
||||
orderItems,
|
||||
|
|
@ -22,6 +25,7 @@ import {
|
|||
updateUserOrderNotes,
|
||||
validateAndGetUserCoupon,
|
||||
} from "@/src/dbService";
|
||||
import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common";
|
||||
import { scaffoldAssetUrl } from "@/src/lib/s3-client";
|
||||
import { ApiError } from "@/src/lib/api-error";
|
||||
import {
|
||||
|
|
@ -30,11 +34,13 @@ import {
|
|||
} from "@/src/lib/notif-job";
|
||||
import { CONST_KEYS, getConstant, getConstants } from "@/src/lib/const-store";
|
||||
import { publishFormattedOrder, publishCancellation } from "@/src/lib/post-order-handler";
|
||||
import { getSlotById } from "@/src/stores/slot-store";
|
||||
import type {
|
||||
UserOrdersResponse,
|
||||
UserOrderDetail,
|
||||
UserCancelOrderResponse,
|
||||
UserUpdateNotesResponse,
|
||||
UserRecentProductsResponse,
|
||||
} from "@/src/dbService";
|
||||
|
||||
const placeOrderUtil = async (params: {
|
||||
|
|
@ -658,4 +664,60 @@ export const orderRouter = router({
|
|||
|
||||
return { success: true, message: "Notes updated successfully" };
|
||||
}),
|
||||
|
||||
getRecentlyOrderedProducts: protectedProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
limit: z.number().min(1).max(50).default(20),
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
.query(async ({ input, ctx }): Promise<UserRecentProductsResponse> => {
|
||||
const { limit = 20 } = input || {};
|
||||
const userId = ctx.user.userId;
|
||||
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const recentOrderIds = await getUserRecentlyDeliveredOrderIds(userId, 10, thirtyDaysAgo);
|
||||
|
||||
if (recentOrderIds.length === 0) {
|
||||
return { success: true, products: [] };
|
||||
}
|
||||
|
||||
const productIds = await getUserProductIdsFromOrders(recentOrderIds);
|
||||
|
||||
if (productIds.length === 0) {
|
||||
return { success: true, products: [] };
|
||||
}
|
||||
|
||||
const productsWithUnits = await getUserProductsForRecentOrders(productIds, limit);
|
||||
|
||||
const formattedProducts = await Promise.all(
|
||||
productsWithUnits.map(async (product) => {
|
||||
const nextDeliveryDate = await getNextDeliveryDate(product.id);
|
||||
return {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
shortDescription: product.shortDescription,
|
||||
price: product.price,
|
||||
unit: product.unitShortNotation,
|
||||
incrementStep: product.incrementStep,
|
||||
isOutOfStock: product.isOutOfStock,
|
||||
nextDeliveryDate: nextDeliveryDate
|
||||
? nextDeliveryDate.toISOString()
|
||||
: null,
|
||||
images: scaffoldAssetUrl(
|
||||
(product.images as string[]) || []
|
||||
),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
products: formattedProducts,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-ind
|
|||
import { z } from 'zod'
|
||||
import { claimUploadUrl, extractKeyFromPresignedUrl, scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
import { getProductById as getProductByIdFromCache } from '@/src/stores/product-store'
|
||||
import { getProductById as getProductByIdFromCache, getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
getUserProductDetailById as getUserProductDetailByIdInDb,
|
||||
|
|
@ -164,6 +164,24 @@ export const productRouter = router({
|
|||
return { success: true, review: newReview }
|
||||
}),
|
||||
|
||||
|
||||
getAllProductsSummary: publicProcedure
|
||||
.query(async (): Promise<UserProductDetail[]> => {
|
||||
// Get all products from cache
|
||||
const allCachedProducts = await getAllProductsFromCache();
|
||||
|
||||
// Transform the cached products to match the expected summary format
|
||||
// (with empty deliverySlots and specialDeals arrays for summary view)
|
||||
const transformedProducts: UserProductDetail[] = allCachedProducts.map(product => ({
|
||||
...product,
|
||||
images: product.images || [],
|
||||
deliverySlots: [],
|
||||
specialDeals: [],
|
||||
}))
|
||||
|
||||
return transformedProducts
|
||||
}),
|
||||
|
||||
getOffersPage: publicProcedure
|
||||
.query(async () => {
|
||||
const data = await getOffersAndCombosInDb();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,30 @@
|
|||
import { router, publicProcedure } from "@/src/trpc/trpc-index"
|
||||
import { getAllSlots as getAllSlotsFromCache } from "@/src/stores/slot-store"
|
||||
import { z } from "zod"
|
||||
import { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "@/src/stores/slot-store"
|
||||
import dayjs from 'dayjs'
|
||||
import { getUserActiveSlotsList as getUserActiveSlotsListInDb, getUserProductAvailability as getUserProductAvailabilityInDb } from '@/src/dbService'
|
||||
import type { UserSlotsListResponse, UserSlotsWithProductsResponse } from '@packages/shared'
|
||||
import type { UserSlotData, UserSlotsListResponse, UserSlotsWithProductsResponse } from '@packages/shared'
|
||||
|
||||
// Helper method to get formatted slot data by ID
|
||||
async function getSlotData(slotId: number) {
|
||||
const slot = await getSlotByIdFromCache(slotId);
|
||||
|
||||
if (!slot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentTime = new Date();
|
||||
if (dayjs(slot.freezeTime).isBefore(currentTime)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
deliveryTime: slot.deliveryTime,
|
||||
freezeTime: slot.freezeTime,
|
||||
slotId: slot.id,
|
||||
products: slot.products.filter((product) => !product.isOutOfStock),
|
||||
};
|
||||
}
|
||||
|
||||
export async function scaffoldSlotsWithProducts(): Promise<UserSlotsWithProductsResponse> {
|
||||
const allSlots = await getAllSlotsFromCache();
|
||||
|
|
@ -60,4 +82,15 @@ export const slotsRouter = router({
|
|||
count: slots.length,
|
||||
}
|
||||
}),
|
||||
|
||||
getSlotsWithProducts: publicProcedure.query(async (): Promise<UserSlotsWithProductsResponse> => {
|
||||
const response = await scaffoldSlotsWithProducts();
|
||||
return response;
|
||||
}),
|
||||
|
||||
getSlotById: publicProcedure
|
||||
.input(z.object({ slotId: z.number() }))
|
||||
.query(async ({ input }): Promise<UserSlotData | null> => {
|
||||
return await getSlotData(input.slotId);
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { router, publicProcedure } from '@/src/trpc/trpc-index'
|
||||
import { z } from 'zod'
|
||||
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||
import { ApiError } from '@/src/lib/api-error'
|
||||
import { getTagsByStoreId } from '@/src/stores/product-tag-store'
|
||||
|
|
@ -173,3 +175,21 @@ export async function scaffoldStoreWithProducts(storeId: number): Promise<UserSt
|
|||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export const storesRouter = router({
|
||||
getStores: publicProcedure
|
||||
.query(async (): Promise<UserStoresResponse> => {
|
||||
const response = await scaffoldStores();
|
||||
return response;
|
||||
}),
|
||||
|
||||
getStoreWithProducts: publicProcedure
|
||||
.input(z.object({
|
||||
storeId: z.number(),
|
||||
}))
|
||||
.query(async ({ input }): Promise<UserStoreDetail> => {
|
||||
const { storeId } = input;
|
||||
const response = await scaffoldStoreWithProducts(storeId);
|
||||
return response;
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
28
apps/backend/src/trpc/apis/user-apis/apis/tags.ts
Normal file
28
apps/backend/src/trpc/apis/user-apis/apis/tags.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { router, publicProcedure } from '@/src/trpc/trpc-index';
|
||||
import { z } from 'zod';
|
||||
import { getTagsByStoreId } from '@/src/stores/product-tag-store';
|
||||
import { ApiError } from '@/src/lib/api-error';
|
||||
|
||||
export const tagsRouter = router({
|
||||
getTagsByStore: publicProcedure
|
||||
.input(z.object({
|
||||
storeId: z.number(),
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
const { storeId } = input;
|
||||
|
||||
// Get tags from cache that are related to this store
|
||||
const tags = await getTagsByStoreId(storeId);
|
||||
|
||||
|
||||
return {
|
||||
tags: tags.map(tag => ({
|
||||
id: tag.id,
|
||||
tagName: tag.tagName,
|
||||
tagDescription: tag.tagDescription,
|
||||
imageUrl: tag.imageUrl,
|
||||
productIds: tag.productIds,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { router } from '@/src/trpc/trpc-index';
|
||||
import { addressRouter } from '@/src/trpc/apis/user-apis/apis/address';
|
||||
import { authRouter } from '@/src/trpc/apis/user-apis/apis/auth';
|
||||
import { bannerRouter } from '@/src/trpc/apis/user-apis/apis/banners';
|
||||
import { cartRouter } from '@/src/trpc/apis/user-apis/apis/cart';
|
||||
import { complaintRouter } from '@/src/trpc/apis/user-apis/apis/complaint';
|
||||
import { orderRouter } from '@/src/trpc/apis/user-apis/apis/order';
|
||||
|
|
@ -9,11 +10,14 @@ import { slotsRouter } from '@/src/trpc/apis/user-apis/apis/slots';
|
|||
import { userRouter as userDataRouter } from '@/src/trpc/apis/user-apis/apis/user';
|
||||
import { userCouponRouter } from '@/src/trpc/apis/user-apis/apis/coupon';
|
||||
import { paymentRouter } from '@/src/trpc/apis/user-apis/apis/payments';
|
||||
import { storesRouter } from '@/src/trpc/apis/user-apis/apis/stores';
|
||||
import { fileUploadRouter } from '@/src/trpc/apis/user-apis/apis/file-upload';
|
||||
import { tagsRouter } from '@/src/trpc/apis/user-apis/apis/tags';
|
||||
|
||||
export const userRouter = router({
|
||||
address: addressRouter,
|
||||
auth: authRouter,
|
||||
banner: bannerRouter,
|
||||
cart: cartRouter,
|
||||
complaint: complaintRouter,
|
||||
order: orderRouter,
|
||||
|
|
@ -22,7 +26,9 @@ export const userRouter = router({
|
|||
user: userDataRouter,
|
||||
coupon: userCouponRouter,
|
||||
payment: paymentRouter,
|
||||
stores: storesRouter,
|
||||
fileUpload: fileUploadRouter,
|
||||
tags: tagsRouter,
|
||||
});
|
||||
|
||||
export type UserRouter = typeof userRouter;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.100.0",
|
||||
"@tanstack/react-query": "^5.59.16",
|
||||
"@tanstack/react-router": "^1.92.8",
|
||||
"@tanstack/router-devtools": "^1.92.8",
|
||||
"@trpc/client": "^11.6.0",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import {
|
|||
useMarkDataFetchers,
|
||||
LoadingDialog,
|
||||
MyTouchableOpacity,
|
||||
MyText, SearchBar
|
||||
MyText, SearchBar, useStatusBarStore,
|
||||
colors
|
||||
} from "common-ui";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
|
|
@ -34,8 +35,8 @@ dayjs.extend(relativeTime);
|
|||
|
||||
const { width: screenWidth } = Dimensions.get("window");
|
||||
const itemWidth = screenWidth * 0.45;
|
||||
const heroItemWidth = (screenWidth - 72) / 3;
|
||||
const gridItemWidth = (screenWidth - 48) / 2;
|
||||
const headerColor = colors.secondaryPink;
|
||||
|
||||
const formatTimeRange = (deliveryTime: string) => {
|
||||
const time = dayjs(deliveryTime);
|
||||
|
|
@ -57,15 +58,16 @@ const staticStyles = {
|
|||
slotsListContent: { paddingBottom: 24 },
|
||||
};
|
||||
|
||||
// Light/pastel color pairs for the Explore Products tabs.
|
||||
const TAG_COLORS = [
|
||||
{ pageBg: '#FFF8F9', bg: '#FFF1F2', border: '#FECDD3', text: '#9F1239', dot: '#E11D48' }, // rose
|
||||
{ pageBg: '#FFFCF1', bg: '#FFFBEB', border: '#FDE68A', text: '#92400E', dot: '#D97706' }, // amber
|
||||
{ pageBg: '#F6FEF9', bg: '#F0FDF4', border: '#BBF7D0', text: '#166534', dot: '#16A34A' }, // green
|
||||
{ pageBg: '#F5FAFF', bg: '#EFF6FF', border: '#BFDBFE', text: '#1E3A8A', dot: '#2563EB' }, // blue
|
||||
{ pageBg: '#FAF8FF', bg: '#F5F3FF', border: '#DDD6FE', text: '#5B21B6', dot: '#7C3AED' }, // violet
|
||||
{ pageBg: '#FFF9F2', bg: '#FFF7ED', border: '#FFD6B0', text: '#9A3412', dot: '#EA580C' }, // orange
|
||||
{ pageBg: '#F3FEFF', bg: '#ECFEFF', border: '#A5F3FC', text: '#155E75', dot: '#0891B2' }, // cyan
|
||||
{ pageBg: '#FFF7FB', bg: '#FDF2F8', border: '#FBCFE8', text: '#9D174D', dot: '#DB2777' }, // pink
|
||||
{ bg: '#FFE4E6', border: '#FECDD3', text: '#BE123C' }, // rose
|
||||
{ bg: '#FEF3C7', border: '#FDE68A', text: '#B45309' }, // amber
|
||||
{ bg: '#DCFCE7', border: '#BBF7D0', text: '#15803D' }, // green
|
||||
{ bg: '#DBEAFE', border: '#BFDBFE', text: '#1D4ED8' }, // blue
|
||||
{ bg: '#EDE9FE', border: '#DDD6FE', text: '#6D28D9' }, // violet
|
||||
{ bg: '#FFE4CC', border: '#FFD6B0', text: '#C2410C' }, // orange
|
||||
{ bg: '#CFFAFE', border: '#A5F3FC', text: '#0E7490' }, // cyan
|
||||
{ bg: '#FCE7F3', border: '#FBCFE8', text: '#BE185D' }, // pink
|
||||
];
|
||||
|
||||
const getTagColor = (id: number) => TAG_COLORS[id % TAG_COLORS.length];
|
||||
|
|
@ -215,61 +217,32 @@ interface ExploreTabProps {
|
|||
}
|
||||
|
||||
const ExploreTab = memo(({ tag, isSelected, onPress }: ExploreTabProps) => {
|
||||
const color = getTagColor(tag.id);
|
||||
const productCount = tag.productIds?.length || 0;
|
||||
|
||||
return (
|
||||
<MyTouchableOpacity
|
||||
onPress={onPress}
|
||||
activeOpacity={0.75}
|
||||
style={tw`px-2 pt-2 pb-1`}
|
||||
activeOpacity={0.8}
|
||||
style={[
|
||||
tw`px-4 py-2.5 rounded-full border`,
|
||||
isSelected
|
||||
? { backgroundColor: color.bg, borderColor: color.text }
|
||||
: { backgroundColor: '#FFFFFF', borderColor: color.border },
|
||||
]}
|
||||
>
|
||||
<View style={tw`items-center`}>
|
||||
<MyText
|
||||
style={[
|
||||
tw`text-base tracking-tight`,
|
||||
{
|
||||
color: isSelected ? '#111827' : '#64748B',
|
||||
fontWeight: isSelected ? '700' : '500',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{tag.tagName}
|
||||
</MyText>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 4,
|
||||
borderRadius: 999,
|
||||
marginTop: 8,
|
||||
backgroundColor: isSelected ? '#111827' : 'transparent',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<MyText
|
||||
style={[
|
||||
tw`text-sm font-semibold`,
|
||||
{ color: color.text },
|
||||
]}
|
||||
>
|
||||
{tag.tagName} ({productCount})
|
||||
</MyText>
|
||||
</MyTouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
interface ExploreTabsRowProps {
|
||||
dashboardTags: any[];
|
||||
activeTagId: number | null;
|
||||
onSelectTag: (id: number) => void;
|
||||
}
|
||||
|
||||
const ExploreTabsRow = memo(({ dashboardTags, activeTagId, onSelectTag }: ExploreTabsRowProps) => (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={tw`gap-3 py-1 px-1`}
|
||||
>
|
||||
{dashboardTags.map((tag) => (
|
||||
<ExploreTab
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
isSelected={activeTagId === tag.id}
|
||||
onPress={() => onSelectTag(tag.id)}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
));
|
||||
|
||||
interface ExploreProductItemProps {
|
||||
item: any;
|
||||
onPress: (id: number) => void;
|
||||
|
|
@ -279,15 +252,14 @@ const ExploreProductItem = memo(({ item, onPress }: ExploreProductItemProps) =>
|
|||
const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]);
|
||||
|
||||
return (
|
||||
<View style={tw`mb-3`}>
|
||||
<View style={tw`mr-4`}>
|
||||
<ProductCard
|
||||
item={item}
|
||||
itemWidth={heroItemWidth}
|
||||
itemWidth={itemWidth}
|
||||
onPress={handlePress}
|
||||
showDeliveryInfo={false}
|
||||
useAddToCartDialog={true}
|
||||
miniView={true}
|
||||
variant="hero"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -330,7 +302,6 @@ interface ListHeaderProps {
|
|||
activeTagId: number | null;
|
||||
activeTagProducts: any[];
|
||||
onSelectTag: (id: number) => void;
|
||||
onTabsSectionLayout: (layout: { y: number; height: number }) => void;
|
||||
}
|
||||
|
||||
const ListHeader = memo(({
|
||||
|
|
@ -344,22 +315,20 @@ const ListHeader = memo(({
|
|||
activeTagId,
|
||||
activeTagProducts,
|
||||
onSelectTag,
|
||||
onTabsSectionLayout,
|
||||
}: ListHeaderProps) => {
|
||||
const [showAllActiveProducts, setShowAllActiveProducts] = useState(false);
|
||||
const handleLayout = useCallback((event: any) => {
|
||||
const { y, height } = event.nativeEvent.layout;
|
||||
onGradientLayout(y + height);
|
||||
}, [onGradientLayout]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setShowAllActiveProducts(false);
|
||||
}, [activeTagId]);
|
||||
|
||||
const renderPopularItem = useCallback(({ item }: { item: any }) => (
|
||||
<PopularProductItem item={item} onPress={onProductPress} />
|
||||
), [onProductPress]);
|
||||
|
||||
const renderExploreItem = useCallback(({ item }: { item: any }) => (
|
||||
<ExploreProductItem item={item} onPress={onProductPress} />
|
||||
), [onProductPress]);
|
||||
|
||||
const renderSlotItem = useCallback(({ item }: { item: any }) => (
|
||||
<SlotItem item={item} />
|
||||
), []);
|
||||
|
|
@ -368,87 +337,25 @@ const ListHeader = memo(({
|
|||
tw`absolute left-0 right-0 shadow-lg`,
|
||||
{ height: gradientHeight + 32, zIndex: -1 }
|
||||
], [gradientHeight]);
|
||||
const activeColor = activeTagId == null ? null : getTagColor(activeTagId);
|
||||
const pageTint = activeColor?.pageBg ?? '#FFFFFF';
|
||||
const visibleActiveTagProducts = showAllActiveProducts ? activeTagProducts : activeTagProducts.slice(0, 6);
|
||||
const hasMoreActiveTagProducts = activeTagProducts.length > visibleActiveTagProducts.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<View onLayout={handleLayout} style={{ backgroundColor: pageTint }}>
|
||||
<View onLayout={handleLayout}>
|
||||
<LinearGradient
|
||||
colors={[pageTint, pageTint]}
|
||||
colors={[headerColor, headerColor]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0.5 }}
|
||||
style={gradientStyle}
|
||||
/>
|
||||
|
||||
{dashboardTags.length > 0 && (
|
||||
<View
|
||||
onLayout={(event) => {
|
||||
const { y, height } = event.nativeEvent.layout;
|
||||
onTabsSectionLayout({ y, height });
|
||||
}}
|
||||
style={[
|
||||
tw`mx-4 mb-2 rounded-[28px] px-3 pt-3 pb-2`,
|
||||
{ backgroundColor: pageTint },
|
||||
]}
|
||||
>
|
||||
<ExploreTabsRow
|
||||
dashboardTags={dashboardTags}
|
||||
activeTagId={activeTagId}
|
||||
onSelectTag={onSelectTag}
|
||||
/>
|
||||
{activeTagProducts.length > 0 ? (
|
||||
<View style={[tw`mt-4 relative`, { backgroundColor: pageTint }]}>
|
||||
<View style={tw`flex-row flex-wrap justify-between`}>
|
||||
{visibleActiveTagProducts.map((product: any) => (
|
||||
<ExploreProductItem
|
||||
key={product.id}
|
||||
item={product}
|
||||
onPress={onProductPress}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
{hasMoreActiveTagProducts && (
|
||||
<MyTouchableOpacity
|
||||
style={tw`self-center mt-1 mb-2 px-5 py-2.5 rounded-full bg-gray-900`}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => setShowAllActiveProducts(true)}
|
||||
>
|
||||
<MyText style={tw`text-white text-sm font-semibold`}>Show More</MyText>
|
||||
</MyTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<View style={tw`py-6 items-center`}>
|
||||
<MyText style={tw`text-sm text-gray-500 font-medium`}>
|
||||
No products in this category yet
|
||||
</MyText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={tw`py-4`}>
|
||||
<BannerCarousel />
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
tw`rounded-t-3xl px-4`,
|
||||
{ backgroundColor: pageTint },
|
||||
]}
|
||||
>
|
||||
{storesData?.stores && storesData.stores.length > 0 && (
|
||||
<View style={tw`mt-4 mb-5 rounded-[28px] bg-white/70 border border-white px-3 pt-4 pb-2`}>
|
||||
<View style={tw`px-4 pb-2`}>
|
||||
<View style={tw`flex-row items-center justify-between mb-4 px-1`}>
|
||||
<View>
|
||||
<MyText style={tw`text-xl font-extrabold text-gray-900 tracking-tight`}>
|
||||
<MyText style={tw`text-xl font-extrabold text-white tracking-tight drop-shadow-md text-neutral-800`}>
|
||||
Our Stores
|
||||
</MyText>
|
||||
<MyText style={tw`text-xs font-medium mt-0.5 text-gray-500`}>Fresh from our locations</MyText>
|
||||
<MyText style={tw`text-xs font-medium mt-0.5 text-neutral-800`}>Fresh from our locations</MyText>
|
||||
</View>
|
||||
</View>
|
||||
<View style={tw`pb-2`}>
|
||||
|
|
@ -462,7 +369,13 @@ const ListHeader = memo(({
|
|||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={tw`py-4`}>
|
||||
<BannerCarousel />
|
||||
</View>
|
||||
|
||||
<View style={tw`bg-white rounded-t-3xl px-4`}>
|
||||
<View style={tw`py-2`}>
|
||||
<NextOrderGlimpse />
|
||||
</View>
|
||||
|
|
@ -491,6 +404,55 @@ const ListHeader = memo(({
|
|||
/>
|
||||
</View>
|
||||
|
||||
{dashboardTags.length > 0 && (
|
||||
<View style={tw`mb-4`}>
|
||||
<View style={tw`px-1 mb-2`}>
|
||||
<MyText style={tw`text-2xl font-extrabold text-gray-900 tracking-tight`}>Explore Products</MyText>
|
||||
<MyText style={tw`text-sm text-gray-500 font-medium mt-1`}>Browse by category</MyText>
|
||||
</View>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={tw`gap-2 py-2`}
|
||||
>
|
||||
{dashboardTags.map((tag) => (
|
||||
<ExploreTab
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
isSelected={activeTagId === tag.id}
|
||||
onPress={() => onSelectTag(tag.id)}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
{activeTagProducts.length > 0 ? (
|
||||
<View style={tw`mt-3 relative`}>
|
||||
<MyFlatList
|
||||
data={activeTagProducts}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={staticStyles.popularListContent}
|
||||
renderItem={renderExploreItem}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
<LinearGradient
|
||||
colors={["transparent", "rgba(0,0,0,0.08)"]}
|
||||
start={{ x: 0, y: 0.5 }}
|
||||
end={{ x: 1, y: 0.5 }}
|
||||
style={tw`absolute right-0 top-0 bottom-4 w-12 rounded-l-xl`}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View style={tw`py-6 items-center`}>
|
||||
<MyText style={tw`text-sm text-gray-500 font-medium`}>
|
||||
No products in this category yet
|
||||
</MyText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{sortedSlots.length > 0 && (
|
||||
<View style={tw`mt-2 mb-4`}>
|
||||
<View style={tw`flex-row items-center justify-between px-1 mb-6`}>
|
||||
|
|
@ -535,9 +497,7 @@ export default function Dashboard() {
|
|||
const [displayedProducts, setDisplayedProducts] = useState<any[]>([]);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const [searchBarHeight, setSearchBarHeight] = useState(0);
|
||||
const [tabsSectionLayout, setTabsSectionLayout] = useState({ y: 0, height: 0 });
|
||||
const [showStickyTabs, setShowStickyTabs] = useState(false);
|
||||
const { backgroundColor } = useStatusBarStore();
|
||||
const { getQuickestSlot } = useProductSlotIdentifier();
|
||||
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
|
||||
const refetchProducts = useCentralProductStore((state) => state.refetchProducts);
|
||||
|
|
@ -680,19 +640,6 @@ export default function Dashboard() {
|
|||
router.push("/(drawer)/(tabs)/home/search-results");
|
||||
}, [router]);
|
||||
|
||||
const handleTabsSectionLayout = useCallback((layout: { y: number; height: number }) => {
|
||||
setTabsSectionLayout(layout);
|
||||
}, []);
|
||||
|
||||
const handleListScroll = useCallback((event: any) => {
|
||||
const scrollY = event.nativeEvent.contentOffset.y;
|
||||
const stickyStart = tabsSectionLayout.y;
|
||||
const stickyEnd = tabsSectionLayout.y + tabsSectionLayout.height - 56;
|
||||
const shouldShowStickyTabs = tabsSectionLayout.height > 0 && scrollY > stickyStart && scrollY < stickyEnd;
|
||||
|
||||
setShowStickyTabs((current) => current === shouldShowStickyTabs ? current : shouldShowStickyTabs);
|
||||
}, [tabsSectionLayout]);
|
||||
|
||||
const renderProductItem = useCallback(({ item }: { item: any }) => (
|
||||
<ProductItem item={item} onPress={handleProductPress} />
|
||||
// <Image style={{ width: 150, height: 235 }} source={{ uri: item.images[0]}} />
|
||||
|
|
@ -710,20 +657,13 @@ export default function Dashboard() {
|
|||
activeTagId={activeTagId}
|
||||
activeTagProducts={activeTagProducts}
|
||||
onSelectTag={setSelectedTagId}
|
||||
onTabsSectionLayout={handleTabsSectionLayout}
|
||||
/>
|
||||
), [gradientHeight, handleGradientLayout, storesData, popularProducts, sortedSlots, handleProductPress, dashboardTags, activeTagId, activeTagProducts, handleTabsSectionLayout]);
|
||||
|
||||
const pageTint = activeTagId == null ? '#FFFFFF' : getTagColor(activeTagId).pageBg;
|
||||
const pageTintStyle = useMemo(() => [
|
||||
tw`flex-1`,
|
||||
{ backgroundColor: pageTint }
|
||||
], [pageTint]);
|
||||
), [gradientHeight, handleGradientLayout, storesData, popularProducts, sortedSlots, handleProductPress, dashboardTags, activeTagId, activeTagProducts]);
|
||||
|
||||
const searchBarContainerStyle = useMemo(() => [
|
||||
tw`w-full px-4 pt-4 pb-0`,
|
||||
{ backgroundColor: pageTint }
|
||||
], [pageTint]);
|
||||
tw`w-full px-4 pt-4 pb-2`,
|
||||
{ backgroundColor }
|
||||
], [backgroundColor]);
|
||||
|
||||
const listContentContainerStyle = useMemo(() => [
|
||||
tw`pb-24`,
|
||||
|
|
@ -755,90 +695,55 @@ export default function Dashboard() {
|
|||
displayedProducts.forEach(product => str += `${product.id}-`)
|
||||
// console.log(str)
|
||||
return (
|
||||
<TabLayoutWrapper style={{ backgroundColor: pageTint }}>
|
||||
<View style={pageTintStyle}>
|
||||
<View
|
||||
style={searchBarContainerStyle}
|
||||
onLayout={(event) => setSearchBarHeight(event.nativeEvent.layout.height)}
|
||||
>
|
||||
<SearchBar
|
||||
value={""}
|
||||
onChangeText={() => { }}
|
||||
onPress={handleSearchPress}
|
||||
editable={false}
|
||||
containerStyle={tw`bg-white`}
|
||||
onSubmitEditing={() => {
|
||||
if (inputQuery.trim()) {
|
||||
router.push(`/(drawer)/(tabs)/home/search-results?q=${encodeURIComponent(inputQuery.trim())}`);
|
||||
}
|
||||
}}
|
||||
returnKeyType="search"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<MyFlatList
|
||||
data={displayedProducts}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
numColumns={2}
|
||||
style={{ backgroundColor: pageTint }}
|
||||
onScroll={handleListScroll}
|
||||
scrollEventThrottle={16}
|
||||
contentContainerStyle={listContentContainerStyle}
|
||||
columnWrapperStyle={staticStyles.columnWrapper}
|
||||
renderItem={renderProductItem}
|
||||
ListHeaderComponent={listHeader}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor="#3b82f6"
|
||||
colors={["#3b82f6"]}
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
<View style={tw`items-center py-8`}>
|
||||
<MyText style={tw`text-gray-500`}>No products available</MyText>
|
||||
</View>
|
||||
}
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={10}
|
||||
windowSize={5}
|
||||
initialNumToRender={10}
|
||||
updateCellsBatchingPeriod={50}
|
||||
<TabLayoutWrapper>
|
||||
<View style={searchBarContainerStyle}>
|
||||
<SearchBar
|
||||
value={""}
|
||||
onChangeText={() => { }}
|
||||
onPress={handleSearchPress}
|
||||
editable={false}
|
||||
containerStyle={tw`bg-white`}
|
||||
onSubmitEditing={() => {
|
||||
if (inputQuery.trim()) {
|
||||
router.push(`/(drawer)/(tabs)/home/search-results?q=${encodeURIComponent(inputQuery.trim())}`);
|
||||
}
|
||||
}}
|
||||
returnKeyType="search"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{showStickyTabs && dashboardTags.length > 0 && (
|
||||
<View
|
||||
pointerEvents="box-none"
|
||||
style={[
|
||||
tw`absolute left-0 right-0 z-20 px-4 pb-2`,
|
||||
{
|
||||
top: searchBarHeight,
|
||||
backgroundColor: pageTint,
|
||||
elevation: 8,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
tw`rounded-[28px] px-3 pt-2 pb-1`,
|
||||
{ backgroundColor: pageTint },
|
||||
]}
|
||||
>
|
||||
<ExploreTabsRow
|
||||
dashboardTags={dashboardTags}
|
||||
activeTagId={activeTagId}
|
||||
onSelectTag={setSelectedTagId}
|
||||
/>
|
||||
</View>
|
||||
<MyFlatList
|
||||
data={displayedProducts}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
numColumns={2}
|
||||
contentContainerStyle={listContentContainerStyle}
|
||||
columnWrapperStyle={staticStyles.columnWrapper}
|
||||
renderItem={renderProductItem}
|
||||
ListHeaderComponent={listHeader}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor="#3b82f6"
|
||||
colors={["#3b82f6"]}
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
<View style={tw`items-center py-8`}>
|
||||
<MyText style={tw`text-gray-500`}>No products available</MyText>
|
||||
</View>
|
||||
)}
|
||||
}
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={10}
|
||||
windowSize={5}
|
||||
initialNumToRender={10}
|
||||
updateCellsBatchingPeriod={50}
|
||||
/>
|
||||
|
||||
<LoadingDialog open={isLoadingDialogOpen} message="Adding to cart..." />
|
||||
<AddToCartDialog />
|
||||
<View style={tw`absolute bottom-2 left-4 right-4`}>
|
||||
<FloatingCartBar />
|
||||
</View>
|
||||
<LoadingDialog open={isLoadingDialogOpen} message="Adding to cart..." />
|
||||
<AddToCartDialog />
|
||||
<View style={tw`absolute bottom-2 left-4 right-4`}>
|
||||
<FloatingCartBar />
|
||||
</View>
|
||||
</TabLayoutWrapper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ interface ProductCardProps {
|
|||
onPress?: () => void;
|
||||
showDeliveryInfo?: boolean;
|
||||
miniView?: boolean;
|
||||
variant?: 'default' | 'hero';
|
||||
nullIfNotAvailable?: boolean;
|
||||
containerComp?: React.ComponentType<any> | React.JSXElementConstructor<any>;
|
||||
useAddToCartDialog?: boolean;
|
||||
|
|
@ -43,12 +42,10 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
|||
onPress,
|
||||
showDeliveryInfo = true,
|
||||
miniView = false,
|
||||
variant = 'default',
|
||||
nullIfNotAvailable = false,
|
||||
containerComp: ContainerComp = React.Fragment,
|
||||
useAddToCartDialog = false,
|
||||
}) => {
|
||||
const isHero = variant === 'hero';
|
||||
const imageUri = item.images?.[0]
|
||||
const [imageStatus, setImageStatus] = React.useState<'loading' | 'loaded' | 'error'>('loading')
|
||||
const [imageError, setImageError] = React.useState<string | null>(null)
|
||||
|
|
@ -155,8 +152,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
|||
<ContainerComp>
|
||||
<MyTouchableOpacity
|
||||
style={[
|
||||
tw`bg-white overflow-hidden border border-gray-200`,
|
||||
isHero ? tw`rounded-xl pb-1.5` : tw`rounded-2xl pb-2`,
|
||||
tw`bg-white rounded-2xl overflow-hidden border border-gray-200 pb-2`,
|
||||
{ width: itemWidth },
|
||||
]}
|
||||
onPress={onPress || (() => {/* TODO: Navigate to product detail */})}
|
||||
|
|
@ -166,7 +162,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
|||
<RnImage
|
||||
source={{ uri: imageUri }}
|
||||
// source={{uri: 'https://pub-6bf1fbc4048a4cbaa533ddbb13bf9de6.r2.dev/product-images/1763796113884-0'}}
|
||||
style={{ width: "100%", height: isHero ? itemWidth * 0.82 : itemWidth, resizeMode: "cover" }}
|
||||
style={{ width: "100%", height: itemWidth, resizeMode: "cover" }}
|
||||
onLoadStart={() => {
|
||||
setImageStatus('loading')
|
||||
setImageError(null)
|
||||
|
|
@ -198,36 +194,36 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
|||
</View>
|
||||
)}
|
||||
{miniView && (
|
||||
<View style={isHero ? tw`absolute bottom-1.5 right-1.5` : tw`absolute bottom-2 right-2`}>
|
||||
<View style={tw`absolute bottom-2 right-2`}>
|
||||
{quantity > 0 ? (
|
||||
<MiniQuantifier value={quantity} onChange={handleQuantityChange} step={item.incrementStep} />
|
||||
) : (
|
||||
<MyTouchableOpacity
|
||||
style={isHero ? tw`w-7 h-7 rounded-full bg-white items-center justify-center shadow-md` : tw`w-8 h-8 rounded-full bg-white items-center justify-center shadow-md`}
|
||||
onPress={() => handleQuantityChange(1)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<CartIcon focused={false} size={isHero ? 14 : 16} color="#2E90FA" />
|
||||
</MyTouchableOpacity>
|
||||
)}
|
||||
style={tw`w-8 h-8 rounded-full bg-white items-center justify-center shadow-md`}
|
||||
onPress={() => handleQuantityChange(1)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<CartIcon focused={false} size={16} color="#2E90FA" />
|
||||
</MyTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={isHero ? tw`px-2 pt-2` : tw`px-3 pt-3`}>
|
||||
<MyText style={isHero ? tw`text-gray-900 font-semibold text-xs mb-1 leading-4` : tw`text-gray-900 font-bold text-sm mb-1`} numberOfLines={2}>
|
||||
<View style={tw`px-3 pt-3`}>
|
||||
<MyText style={tw`text-gray-900 font-bold text-sm mb-1`} numberOfLines={2}>
|
||||
{item.name}
|
||||
</MyText>
|
||||
|
||||
<View style={isHero ? tw`flex-row items-baseline mb-1` : tw`flex-row items-baseline mb-2`}>
|
||||
<MyText style={isHero ? tw`text-brand500 font-bold text-sm` : tw`text-brand500 font-bold text-base`}>₹{item.price}</MyText>
|
||||
<View style={tw`flex-row items-baseline mb-2`}>
|
||||
<MyText style={tw`text-brand500 font-bold text-base`}>₹{item.price}</MyText>
|
||||
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
|
||||
<MyText style={isHero ? tw`text-gray-400 text-[10px] ml-1.5 line-through` : tw`text-gray-400 text-xs ml-2 line-through`}>₹{item.marketPrice}</MyText>
|
||||
<MyText style={tw`text-gray-400 text-xs ml-2 line-through`}>₹{item.marketPrice}</MyText>
|
||||
)}
|
||||
</View>
|
||||
<View style={isHero ? tw`flex-row items-center mb-1` : tw`flex-row items-center mb-2`}>
|
||||
<View style={tw`flex-row items-center mb-2`}>
|
||||
{item.productType !== 'combo' && (
|
||||
<MyText style={isHero ? tw`text-gray-500 text-[10px] font-medium` : tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{item.unitNotation}</MyText></MyText>
|
||||
<MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{item.unitNotation}</MyText></MyText>
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import React from 'react';
|
||||
import type { StyleProp, ViewStyle } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { tw } from 'common-ui';
|
||||
|
||||
interface TabLayoutWrapperProps {
|
||||
children: React.ReactNode;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
export default function TabLayoutWrapper({ children, style }: TabLayoutWrapperProps) {
|
||||
export default function TabLayoutWrapper({ children }: TabLayoutWrapperProps) {
|
||||
return (
|
||||
<SafeAreaView edges={['top']} style={[tw`flex-1 bg-white`, style]}>
|
||||
<SafeAreaView edges={['top']} style={tw`flex-1 bg-white`}>
|
||||
{children}
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -202,6 +202,18 @@ export {
|
|||
getActiveBanners as getUserActiveBanners,
|
||||
} from './src/user-apis/banners';
|
||||
|
||||
export {
|
||||
// User Cart
|
||||
getCartItemsWithProducts as getUserCartItemsWithProducts,
|
||||
getProductById as getUserProductById,
|
||||
getCartItemByUserProduct as getUserCartItemByUserProduct,
|
||||
incrementCartItemQuantity as incrementUserCartItemQuantity,
|
||||
insertCartItem as insertUserCartItem,
|
||||
updateCartItemQuantity as updateUserCartItemQuantity,
|
||||
deleteCartItem as deleteUserCartItem,
|
||||
clearUserCart,
|
||||
} from './src/user-apis/cart';
|
||||
|
||||
export {
|
||||
// User Complaint
|
||||
getUserComplaints as getUserComplaints,
|
||||
|
|
@ -296,6 +308,9 @@ export {
|
|||
getOrderBasic as getUserOrderBasic,
|
||||
cancelOrderTransaction as cancelUserOrderTransaction,
|
||||
updateOrderNotes as updateUserOrderNotes,
|
||||
getRecentlyDeliveredOrderIds as getUserRecentlyDeliveredOrderIds,
|
||||
getProductIdsFromOrders as getUserProductIdsFromOrders,
|
||||
getProductsForRecentOrders as getUserProductsForRecentOrders,
|
||||
// Post-order handler helpers
|
||||
getOrdersByIdsWithFullData,
|
||||
getOrderByIdWithFullData,
|
||||
|
|
|
|||
95
packages/db_helper_postgres/src/user-apis/cart.ts
Normal file
95
packages/db_helper_postgres/src/user-apis/cart.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { cartItems, productInfo, units } from '../db/schema'
|
||||
import { and, eq, sql } from 'drizzle-orm'
|
||||
import type { UserCartItem } from '@packages/shared'
|
||||
|
||||
const getStringArray = (value: unknown): string[] => {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.map((item) => String(item))
|
||||
}
|
||||
|
||||
export async function getCartItemsWithProducts(userId: number): Promise<UserCartItem[]> {
|
||||
const cartItemsWithProducts = await db
|
||||
.select({
|
||||
cartId: cartItems.id,
|
||||
productId: productInfo.id,
|
||||
productName: productInfo.name,
|
||||
productPrice: productInfo.price,
|
||||
productImages: productInfo.images,
|
||||
productQuantity: productInfo.productQuantity,
|
||||
isOutOfStock: productInfo.isOutOfStock,
|
||||
unitShortNotation: units.shortNotation,
|
||||
quantity: cartItems.quantity,
|
||||
addedAt: cartItems.addedAt,
|
||||
})
|
||||
.from(cartItems)
|
||||
.innerJoin(productInfo, eq(cartItems.productId, productInfo.id))
|
||||
.innerJoin(units, eq(productInfo.unitId, units.id))
|
||||
.where(eq(cartItems.userId, userId))
|
||||
|
||||
return cartItemsWithProducts.map((item) => ({
|
||||
id: item.cartId,
|
||||
productId: item.productId,
|
||||
quantity: parseFloat(item.quantity),
|
||||
addedAt: item.addedAt,
|
||||
product: {
|
||||
id: item.productId,
|
||||
name: item.productName,
|
||||
price: item.productPrice.toString(),
|
||||
productQuantity: item.productQuantity,
|
||||
unit: item.unitShortNotation,
|
||||
isOutOfStock: item.isOutOfStock,
|
||||
images: getStringArray(item.productImages),
|
||||
},
|
||||
subtotal: parseFloat(item.productPrice.toString()) * parseFloat(item.quantity),
|
||||
}))
|
||||
}
|
||||
|
||||
export async function getProductById(productId: number) {
|
||||
return db.query.productInfo.findFirst({
|
||||
where: eq(productInfo.id, productId),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getCartItemByUserProduct(userId: number, productId: number) {
|
||||
return db.query.cartItems.findFirst({
|
||||
where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)),
|
||||
})
|
||||
}
|
||||
|
||||
export async function incrementCartItemQuantity(itemId: number, quantity: number): Promise<void> {
|
||||
await db.update(cartItems)
|
||||
.set({
|
||||
quantity: sql`${cartItems.quantity} + ${quantity}`,
|
||||
})
|
||||
.where(eq(cartItems.id, itemId))
|
||||
}
|
||||
|
||||
export async function insertCartItem(userId: number, productId: number, quantity: number): Promise<void> {
|
||||
await db.insert(cartItems).values({
|
||||
userId,
|
||||
productId,
|
||||
quantity: quantity.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateCartItemQuantity(userId: number, itemId: number, quantity: number) {
|
||||
const [updatedItem] = await db.update(cartItems)
|
||||
.set({ quantity: quantity.toString() })
|
||||
.where(and(eq(cartItems.id, itemId), eq(cartItems.userId, userId)))
|
||||
.returning({ id: cartItems.id })
|
||||
|
||||
return !!updatedItem
|
||||
}
|
||||
|
||||
export async function deleteCartItem(userId: number, itemId: number): Promise<boolean> {
|
||||
const [deletedItem] = await db.delete(cartItems)
|
||||
.where(and(eq(cartItems.id, itemId), eq(cartItems.userId, userId)))
|
||||
.returning({ id: cartItems.id })
|
||||
|
||||
return !!deletedItem
|
||||
}
|
||||
|
||||
export async function clearUserCart(userId: number): Promise<void> {
|
||||
await db.delete(cartItems).where(eq(cartItems.userId, userId))
|
||||
}
|
||||
|
|
@ -14,10 +14,11 @@ import {
|
|||
userDetails,
|
||||
deliverySlotInfo,
|
||||
} from '../db/schema'
|
||||
import { and, eq, inArray, desc, lte } from 'drizzle-orm'
|
||||
import { and, eq, inArray, desc, gte, lte } from 'drizzle-orm'
|
||||
import type {
|
||||
UserOrderSummary,
|
||||
UserOrderDetail,
|
||||
UserRecentProduct,
|
||||
} from '@packages/shared'
|
||||
|
||||
export interface OrderItemInput {
|
||||
|
|
@ -551,6 +552,77 @@ export async function updateOrderNotes(
|
|||
.where(eq(orders.id, orderId))
|
||||
}
|
||||
|
||||
export async function getRecentlyDeliveredOrderIds(
|
||||
userId: number,
|
||||
limit: number,
|
||||
since: Date
|
||||
): Promise<number[]> {
|
||||
const recentOrders = await db
|
||||
.select({ id: orders.id })
|
||||
.from(orders)
|
||||
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
|
||||
.where(
|
||||
and(
|
||||
eq(orders.userId, userId),
|
||||
eq(orderStatus.isDelivered, true),
|
||||
gte(orders.createdAt, since)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(orders.createdAt))
|
||||
.limit(limit)
|
||||
|
||||
return recentOrders.map((order) => order.id)
|
||||
}
|
||||
|
||||
export async function getProductIdsFromOrders(
|
||||
orderIds: number[]
|
||||
): Promise<number[]> {
|
||||
const orderItemsResult = await db
|
||||
.select({ productId: orderItems.productId })
|
||||
.from(orderItems)
|
||||
.where(inArray(orderItems.orderId, orderIds))
|
||||
|
||||
return [...new Set(orderItemsResult.map((item) => item.productId))]
|
||||
}
|
||||
|
||||
export interface RecentProductData {
|
||||
id: number
|
||||
name: string
|
||||
shortDescription: string | null
|
||||
price: string
|
||||
images: unknown
|
||||
isOutOfStock: boolean
|
||||
unitShortNotation: string
|
||||
incrementStep: number
|
||||
}
|
||||
|
||||
export async function getProductsForRecentOrders(
|
||||
productIds: number[],
|
||||
limit: number
|
||||
): Promise<RecentProductData[]> {
|
||||
return db
|
||||
.select({
|
||||
id: productInfo.id,
|
||||
name: productInfo.name,
|
||||
shortDescription: productInfo.shortDescription,
|
||||
price: productInfo.price,
|
||||
images: productInfo.images,
|
||||
isOutOfStock: productInfo.isOutOfStock,
|
||||
unitShortNotation: units.shortNotation,
|
||||
incrementStep: productInfo.incrementStep,
|
||||
})
|
||||
.from(productInfo)
|
||||
.innerJoin(units, eq(productInfo.unitId, units.id))
|
||||
.where(
|
||||
and(
|
||||
inArray(productInfo.id, productIds),
|
||||
eq(productInfo.isSuspended, false)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(productInfo.createdAt))
|
||||
.limit(limit)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Post-Order Handler Helpers (for Telegram notifications)
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -201,6 +201,18 @@ export {
|
|||
getActiveBanners as getUserActiveBanners,
|
||||
} from './src/user-apis/banners'
|
||||
|
||||
export {
|
||||
// User Cart
|
||||
getCartItemsWithProducts as getUserCartItemsWithProducts,
|
||||
getProductById as getUserProductById,
|
||||
getCartItemByUserProduct as getUserCartItemByUserProduct,
|
||||
incrementCartItemQuantity as incrementUserCartItemQuantity,
|
||||
insertCartItem as insertUserCartItem,
|
||||
updateCartItemQuantity as updateUserCartItemQuantity,
|
||||
deleteCartItem as deleteUserCartItem,
|
||||
clearUserCart,
|
||||
} from './src/user-apis/cart'
|
||||
|
||||
export {
|
||||
// User Complaint
|
||||
getUserComplaints as getUserComplaints,
|
||||
|
|
@ -300,6 +312,9 @@ export {
|
|||
getOrderBasic as getUserOrderBasic,
|
||||
cancelOrderTransaction as cancelUserOrderTransaction,
|
||||
updateOrderNotes as updateUserOrderNotes,
|
||||
getRecentlyDeliveredOrderIds as getUserRecentlyDeliveredOrderIds,
|
||||
getSkuIdsFromOrders as getUserProductIdsFromOrders,
|
||||
getProductsForRecentOrders as getUserProductsForRecentOrders,
|
||||
// Post-order handler helpers
|
||||
getOrdersByIdsWithFullData,
|
||||
getOrderByIdWithFullData,
|
||||
|
|
|
|||
98
packages/db_helper_sqlite/src/user-apis/cart.ts
Normal file
98
packages/db_helper_sqlite/src/user-apis/cart.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { cartItems, productSkus } from '../db/schema'
|
||||
import { and, eq, sql } from 'drizzle-orm'
|
||||
import type { UserCartItem } from '@packages/shared'
|
||||
import { composeSkuName, composeUnitNotation } from '../lib/sku-features'
|
||||
|
||||
const getStringArray = (value: unknown): string[] => {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.map((item) => String(item))
|
||||
}
|
||||
|
||||
export async function getCartItemsWithProducts(userId: number): Promise<UserCartItem[]> {
|
||||
const cartItemsWithProducts = await db.query.cartItems.findMany({
|
||||
where: eq(cartItems.userId, userId),
|
||||
with: {
|
||||
sku: {
|
||||
with: {
|
||||
product: true,
|
||||
features: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return cartItemsWithProducts
|
||||
.filter((item) => item.sku && !item.sku.isSuspended)
|
||||
.map((item) => {
|
||||
const sku = item.sku
|
||||
const features = sku?.features || []
|
||||
const priceValue = sku?.price ?? '0'
|
||||
const quantityValue = item.quantity ?? '0'
|
||||
return {
|
||||
id: item.id,
|
||||
skuId: item.skuId,
|
||||
quantity: parseFloat(quantityValue),
|
||||
addedAt: item.addedAt,
|
||||
product: {
|
||||
id: sku?.id ?? 0,
|
||||
name: composeSkuName(sku?.product?.name ?? 'Unknown', features),
|
||||
price: String(priceValue),
|
||||
productQuantity: 1,
|
||||
unit: composeUnitNotation(features),
|
||||
isOutOfStock: sku?.isOutOfStock ?? false,
|
||||
images: getStringArray(sku?.images),
|
||||
},
|
||||
subtotal: parseFloat(String(priceValue)) * parseFloat(quantityValue),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function getProductById(skuId: number) {
|
||||
return db.query.productSkus.findFirst({
|
||||
where: and(eq(productSkus.id, skuId), eq(productSkus.isSuspended, false)),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getCartItemByUserProduct(userId: number, skuId: number) {
|
||||
return db.query.cartItems.findFirst({
|
||||
where: and(eq(cartItems.userId, userId), eq(cartItems.skuId, skuId)),
|
||||
})
|
||||
}
|
||||
|
||||
export async function incrementCartItemQuantity(itemId: number, quantity: number): Promise<void> {
|
||||
await db.update(cartItems)
|
||||
.set({
|
||||
quantity: sql`${cartItems.quantity} + ${quantity}`,
|
||||
})
|
||||
.where(eq(cartItems.id, itemId))
|
||||
}
|
||||
|
||||
export async function insertCartItem(userId: number, skuId: number, quantity: number): Promise<void> {
|
||||
await db.insert(cartItems).values({
|
||||
userId,
|
||||
skuId,
|
||||
quantity: quantity.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateCartItemQuantity(userId: number, itemId: number, quantity: number) {
|
||||
const [updatedItem] = await db.update(cartItems)
|
||||
.set({ quantity: quantity.toString() })
|
||||
.where(and(eq(cartItems.id, itemId), eq(cartItems.userId, userId)))
|
||||
.returning({ id: cartItems.id })
|
||||
|
||||
return !!updatedItem
|
||||
}
|
||||
|
||||
export async function deleteCartItem(userId: number, itemId: number): Promise<boolean> {
|
||||
const [deletedItem] = await db.delete(cartItems)
|
||||
.where(and(eq(cartItems.id, itemId), eq(cartItems.userId, userId)))
|
||||
.returning({ id: cartItems.id })
|
||||
|
||||
return !!deletedItem
|
||||
}
|
||||
|
||||
export async function clearUserCart(userId: number): Promise<void> {
|
||||
await db.delete(cartItems).where(eq(cartItems.userId, userId))
|
||||
}
|
||||
|
|
@ -15,10 +15,11 @@ import {
|
|||
userDetails,
|
||||
deliverySlotInfo,
|
||||
} from '../db/schema'
|
||||
import { and, eq, inArray, desc, sql } from 'drizzle-orm'
|
||||
import { and, eq, inArray, desc, gte, sql } from 'drizzle-orm'
|
||||
import type {
|
||||
UserOrderSummary,
|
||||
UserOrderDetail,
|
||||
UserRecentProduct,
|
||||
} from '@packages/shared'
|
||||
import { coerceDate } from '../lib/date'
|
||||
import { runBatched } from '../lib/run-batched'
|
||||
|
|
@ -610,6 +611,93 @@ export async function updateOrderNotes(
|
|||
.where(eq(orders.id, orderId))
|
||||
}
|
||||
|
||||
export async function getRecentlyDeliveredOrderIds(
|
||||
userId: number,
|
||||
limit: number,
|
||||
since: Date
|
||||
): Promise<number[]> {
|
||||
const recentOrders = await db
|
||||
.select({ id: orders.id })
|
||||
.from(orders)
|
||||
.innerJoin(orderStatus, eq(orders.id, orderStatus.orderId))
|
||||
.where(
|
||||
and(
|
||||
eq(orders.userId, userId),
|
||||
eq(orderStatus.isDelivered, true),
|
||||
gte(orders.createdAt, since)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(orders.createdAt))
|
||||
.limit(limit)
|
||||
|
||||
return recentOrders.map((order) => order.id)
|
||||
}
|
||||
|
||||
export async function getSkuIdsFromOrders(
|
||||
orderIds: number[]
|
||||
): Promise<number[]> {
|
||||
if (orderIds.length === 0) return []
|
||||
|
||||
const skuChunks = await runBatched(db, orderIds, 10, async (tx, chunk) => {
|
||||
return tx
|
||||
.select({ skuId: orderItems.skuId })
|
||||
.from(orderItems)
|
||||
.where(inArray(orderItems.orderId, chunk))
|
||||
})
|
||||
|
||||
return [...new Set(skuChunks.flat().map((item) => item.skuId))]
|
||||
}
|
||||
|
||||
export interface RecentProductData {
|
||||
id: number
|
||||
name: string
|
||||
shortDescription: string | null
|
||||
price: string
|
||||
images: unknown
|
||||
isOutOfStock: boolean
|
||||
unitShortNotation: string
|
||||
incrementStep: number
|
||||
}
|
||||
|
||||
export async function getProductsForRecentOrders(
|
||||
productIds: number[],
|
||||
limit: number
|
||||
): Promise<RecentProductData[]> {
|
||||
if (productIds.length === 0) return []
|
||||
|
||||
const skuChunks = await runBatched(db, productIds, 10, async (tx, chunk) => {
|
||||
return tx.query.productSkus.findMany({
|
||||
where: and(
|
||||
inArray(productSkus.id, chunk),
|
||||
eq(productSkus.isSuspended, false)
|
||||
),
|
||||
with: {
|
||||
product: true,
|
||||
features: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const skus = skuChunks
|
||||
.flat()
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||
.slice(0, limit)
|
||||
|
||||
return skus.map((sku) => {
|
||||
const features = sku.features || []
|
||||
return {
|
||||
id: sku.id,
|
||||
name: composeSkuName(sku.product?.name ?? 'Unknown', features),
|
||||
shortDescription: sku.product?.shortDescription ?? null,
|
||||
price: String(sku.price ?? '0'),
|
||||
images: sku.images,
|
||||
isOutOfStock: sku.isOutOfStock,
|
||||
unitShortNotation: composeUnitNotation(features),
|
||||
incrementStep: sku.product?.incrementStep ?? 1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Post-Order Handler Helpers (for Telegram notifications)
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -133,6 +133,32 @@ export interface UserBannersResponse {
|
|||
banners: UserBanner[];
|
||||
}
|
||||
|
||||
export interface UserCartProduct {
|
||||
id: number;
|
||||
name: string;
|
||||
price: string;
|
||||
productQuantity: number;
|
||||
unit: string;
|
||||
isOutOfStock: boolean;
|
||||
images: string[];
|
||||
}
|
||||
|
||||
export interface UserCartItem {
|
||||
id: number;
|
||||
skuId: number;
|
||||
quantity: number;
|
||||
addedAt: Date;
|
||||
product: UserCartProduct;
|
||||
subtotal: number;
|
||||
}
|
||||
|
||||
export interface UserCartResponse {
|
||||
items: UserCartItem[];
|
||||
totalItems: number;
|
||||
totalAmount: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface UserComplaint {
|
||||
id: number;
|
||||
complaintBody: string;
|
||||
|
|
@ -341,6 +367,13 @@ export interface UserSlotWithProducts {
|
|||
products: UserSlotProduct[];
|
||||
}
|
||||
|
||||
export interface UserSlotData {
|
||||
slotId: number;
|
||||
deliveryTime: Date;
|
||||
freezeTime: Date;
|
||||
products: UserSlotProduct[];
|
||||
}
|
||||
|
||||
export interface UserSlotAvailability {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
@ -431,6 +464,16 @@ export interface UserPasswordUpdateResponse {
|
|||
message: string;
|
||||
}
|
||||
|
||||
export interface UserProfileResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
mobile: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserDeleteAccountResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
|
|
@ -595,3 +638,20 @@ export interface UserUpdateNotesResponse {
|
|||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface UserRecentProduct {
|
||||
id: number;
|
||||
name: string;
|
||||
shortDescription: string | null;
|
||||
price: string;
|
||||
images: string[];
|
||||
isOutOfStock: boolean;
|
||||
unit: string;
|
||||
incrementStep: number;
|
||||
nextDeliveryDate: string | null;
|
||||
}
|
||||
|
||||
export interface UserRecentProductsResponse {
|
||||
success: boolean;
|
||||
products: UserRecentProduct[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue