Compare commits

..

5 commits

Author SHA1 Message Date
shafi54
5ed889a34f Merge branch 'main' of https://git.technocracy.ovh/shafi/freshyo 2026-03-23 03:23:15 +05:30
shafi54
3ddc939a48 enh 2026-03-23 03:22:20 +05:30
shafi54
24252b717b enh 2026-03-22 21:43:44 +05:30
shafi54
78305e1670 enh 2026-03-22 21:28:32 +05:30
shafi54
1a3fe7826f enh 2026-03-15 22:38:17 +05:30
323 changed files with 32300 additions and 211784 deletions

View file

@ -1,8 +0,0 @@
**/node_modules
**/dist
apps/users-ui/app
apps/users-ui/src
apps/admin-ui/app
apps/users-ui/src
**/package-lock.json

View file

@ -1,4 +0,0 @@
- trpc.user.tags.getTagsByStore — apps/backend/src/trpc/apis/user-apis/apis/tags.ts
- trpc.common.product.getAllProductsSummary — apps/backend/src/trpc/apis/common-apis/common.ts
- remove slots from products cache
- remove redundant product details like name, description etc from the slots api

View file

@ -1,36 +1,32 @@
# Optimized Dockerfile for backend and fallback-ui services (project root)
# 1. ---- Base Bun image
FROM oven/bun:1.3.10 AS base
# 1. ---- Base Node image
FROM node:20-slim AS base
WORKDIR /app
# 2. ---- Pruner ----
FROM base AS pruner
WORKDIR /app
# Copy config files first for better caching
COPY package.json turbo.json ./
COPY package.json package-lock.json turbo.json ./
COPY apps/backend/package.json ./apps/backend/
COPY apps/fallback-ui/package.json ./apps/fallback-ui/
COPY packages/shared/ ./packages/shared
COPY packages/ui/package.json ./packages/ui/
RUN bun install -g turbo
RUN npm install -g turbo
COPY . .
RUN turbo prune --scope=backend --scope=fallback-ui --scope=@packages/shared --docker
# RUN find . -path "./node_modules" -prune -o -print
RUN turbo prune --scope=backend --scope=fallback-ui --scope=common-ui --docker
# 3. ---- Builder ----
FROM base AS builder
WORKDIR /app
# Copy package files first to cache bun install
# Copy package files first to cache npm install
COPY --from=pruner /app/out/json/ .
#COPY --from=pruner /app/out/bun.lock ./bun.lock
#RUN cat ./bun.lock
COPY --from=pruner /app/out/package-lock.json ./package-lock.json
COPY --from=pruner /app/turbo.json .
RUN bun install
RUN npm ci
# Copy source code after dependencies are installed
COPY --from=pruner /app/out/full/ .
RUN bunx turbo run build --filter=fallback-ui... --filter=backend...
RUN find . -path "./node_modules" -prune -o -print
RUN npx turbo run build --filter=fallback-ui... --filter=backend...
# 4. ---- Runner ----
FROM base AS runner
@ -38,15 +34,12 @@ WORKDIR /app
ENV NODE_ENV=production
# Copy package files and install production deps
COPY --from=pruner /app/out/json/ .
#COPY --from=pruner /app/out/bun.lock ./bun.lock
RUN bun install --production
COPY --from=pruner /app/out/package-lock.json ./package-lock.json
RUN npm ci --production --omit=dev
# Copy built applications
COPY --from=builder /app/apps/backend/dist ./apps/backend/dist
COPY --from=builder /app/apps/fallback-ui/dist ./apps/fallback-ui/dist
COPY --from=builder /app/packages/shared ./packages/shared
# RUN ls -R
RUN find . -path "./node_modules" -prune -o -print
EXPOSE 4000
CMD ["bun", "apps/backend/dist/apps/backend/index.js"]
RUN npm i -g bun
CMD ["bun", "apps/backend/dist/index.js"]
# CMD ["node", "apps/backend/dist/index.js"]

File diff suppressed because one or more lines are too long

View file

@ -227,6 +227,7 @@ export default function Layout() {
<Drawer.Screen name="slots" options={{ title: "Slots" }} />
<Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} />
<Drawer.Screen name="stores" options={{ title: "Stores" }} />
<Drawer.Screen name="address-management" options={{ title: "Address Management" }} />
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
<Drawer.Screen name="user-management" options={{ title: "User Management" }} />

View file

@ -0,0 +1,108 @@
import React, { useState } from 'react'
import { View, Text, TouchableOpacity, ScrollView } from 'react-native'
import { BottomDialog , tw } from 'common-ui'
import { trpc } from '@/src/trpc-client'
import AddressZoneForm from '@/components/AddressZoneForm'
import AddressPlaceForm from '@/components/AddressPlaceForm'
import MaterialIcons from '@expo/vector-icons/MaterialIcons'
const AddressManagement: React.FC = () => {
const [dialogOpen, setDialogOpen] = useState(false)
const [dialogType, setDialogType] = useState<'zone' | 'place' | null>(null)
const [expandedZones, setExpandedZones] = useState<Set<number>>(new Set())
const { data: zones, refetch: refetchZones } = trpc.admin.address.getZones.useQuery()
const { data: areas, refetch: refetchAreas } = trpc.admin.address.getAreas.useQuery()
const createZone = trpc.admin.address.createZone.useMutation({
onSuccess: () => {
refetchZones()
setDialogOpen(false)
},
})
const createArea = trpc.admin.address.createArea.useMutation({
onSuccess: () => {
refetchAreas()
setDialogOpen(false)
},
})
const handleAddZone = () => {
setDialogType('zone')
setDialogOpen(true)
}
const handleAddPlace = () => {
setDialogType('place')
setDialogOpen(true)
}
const toggleZone = (zoneId: number) => {
setExpandedZones(prev => {
const newSet = new Set(prev)
if (newSet.has(zoneId)) {
newSet.delete(zoneId)
} else {
newSet.add(zoneId)
}
return newSet
})
}
const groupedAreas = areas?.reduce((acc, area) => {
if (area.zoneId) {
if (!acc[area.zoneId]) acc[area.zoneId] = []
acc[area.zoneId].push(area)
}
return acc
}, {} as Record<number, typeof areas[0][]>) || {}
const unzonedAreas = areas?.filter(a => !a.zoneId) || []
return (
<View style={tw`flex-1 bg-white`}>
<View style={tw`flex-row justify-between p-4`}>
<TouchableOpacity style={tw`bg-blue1 px-4 py-2 rounded`} onPress={handleAddZone}>
<Text style={tw`text-white`}>Add Zone</Text>
</TouchableOpacity>
<TouchableOpacity style={tw`bg-green1 px-4 py-2 rounded`} onPress={handleAddPlace}>
<Text style={tw`text-white`}>Add Place</Text>
</TouchableOpacity>
</View>
<ScrollView style={tw`flex-1 p-4`}>
{zones?.map(zone => (
<View key={zone.id} style={tw`mb-4 border border-gray-300 rounded`}>
<TouchableOpacity style={tw`flex-row items-center p-3 bg-gray-100`} onPress={() => toggleZone(zone.id)}>
<Text style={tw`flex-1 text-lg font-semibold`}>{zone.zoneName}</Text>
<MaterialIcons name={expandedZones.has(zone.id) ? 'expand-less' : 'expand-more'} size={24} />
</TouchableOpacity>
{expandedZones.has(zone.id) && (
<View style={tw`p-3`}>
{groupedAreas[zone.id]?.map(area => (
<Text key={area.id} style={tw`text-base mb-1`}>- {area.placeName}</Text>
)) || <Text style={tw`text-gray-500`}>No places in this zone</Text>}
</View>
)}
</View>
))}
<View style={tw`mt-6`}>
<Text style={tw`text-xl font-bold mb-2`}>Unzoned Places</Text>
{unzonedAreas.map(area => (
<Text key={area.id} style={tw`text-base mb-1`}>- {area.placeName}</Text>
))}
{unzonedAreas.length === 0 && <Text style={tw`text-gray-500`}>No unzoned places</Text>}
</View>
</ScrollView>
<BottomDialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
{dialogType === 'zone' && <AddressZoneForm onSubmit={createZone.mutate} onClose={() => setDialogOpen(false)} />}
{dialogType === 'place' && <AddressPlaceForm onSubmit={createArea.mutate} onClose={() => setDialogOpen(false)} />}
</BottomDialog>
</View>
)
}
export default AddressManagement

View file

@ -74,7 +74,7 @@ export default function Dashboard() {
const menuItems: MenuItem[] = [
{
title: 'Manage Orderss',
title: 'Manage Orders',
icon: 'shopping-bag',
description: 'View and manage customer orders',
route: '/(drawer)/manage-orders',
@ -175,6 +175,15 @@ export default function Dashboard() {
category: 'marketing',
iconColor: '#F97316',
iconBg: '#FFEDD5',
},
{
title: 'Address Management',
icon: 'location-on',
description: 'Manage service areas',
route: '/(drawer)/address-management',
category: 'settings',
iconColor: '#EAB308',
iconBg: '#FEF9C3',
},
{
title: 'App Constants',

View file

@ -63,8 +63,7 @@ export default function OrderDetails() {
onSuccess: (result) => {
Alert.alert(
"Success",
`Refund initiated successfully!\n\nAmount: `
// `Refund initiated successfully!\n\nAmount: ₹${result.amount}\nStatus: ${result.status}`
`Refund initiated successfully!\n\nAmount: ₹${result.amount}\nStatus: ${result.status}`
);
setInitiateRefundDialogOpen(false);
},

View file

@ -0,0 +1,197 @@
import React from 'react';
import { View, ScrollView, Dimensions } from 'react-native';
import { Image } from 'expo-image';
import { MyText, tw } from 'common-ui';
import { trpc } from '../src/trpc-client';
interface FullOrderViewProps {
orderId: number;
}
export const FullOrderView: React.FC<FullOrderViewProps> = ({ orderId }) => {
const { data: order, isLoading, error } = trpc.admin.order.getFullOrder.useQuery({ orderId });
if (isLoading) {
return (
<View style={tw`p-6`}>
<MyText style={tw`text-center text-gray-600`}>Loading order details...</MyText>
</View>
);
}
if (error || !order) {
return (
<View style={tw`p-6`}>
<MyText style={tw`text-center text-red-600`}>Failed to load order details</MyText>
</View>
);
}
const totalAmount = order.items.reduce((sum, item) => sum + item.amount, 0);
return (
<ScrollView
style={[tw`flex-1`, { maxHeight: Dimensions.get('window').height * 0.8 }]}
showsVerticalScrollIndicator={false}
>
<View style={tw`p-6`}>
<MyText style={tw`text-2xl font-bold text-gray-800 mb-6`}>Order #{order.readableId}</MyText>
{/* Customer Information */}
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Customer Details</MyText>
<View style={tw`space-y-2`}>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Name:</MyText>
<MyText style={tw`font-medium`}>{order.customerName}</MyText>
</View>
{order.customerEmail && (
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Email:</MyText>
<MyText style={tw`font-medium`}>{order.customerEmail}</MyText>
</View>
)}
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Mobile:</MyText>
<MyText style={tw`font-medium`}>{order.customerMobile}</MyText>
</View>
</View>
</View>
{/* Delivery Address */}
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Delivery Address</MyText>
<View style={tw`space-y-1`}>
<MyText style={tw`text-gray-800`}>{order.address.line1}</MyText>
{order.address.line2 && <MyText style={tw`text-gray-800`}>{order.address.line2}</MyText>}
<MyText style={tw`text-gray-800`}>
{order.address.city}, {order.address.state} - {order.address.pincode}
</MyText>
<MyText style={tw`text-gray-800`}>Phone: {order.address.phone}</MyText>
</View>
</View>
{/* Order Details */}
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Order Details</MyText>
<View style={tw`space-y-2`}>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Order Date:</MyText>
<MyText style={tw`font-medium`}>
{new Date(order.createdAt).toLocaleDateString('en-IN', {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Payment Method:</MyText>
<MyText style={tw`font-medium`}>
{order.isCod ? 'Cash on Delivery' : 'Online Payment'}
</MyText>
</View>
{order.slotInfo && (
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Delivery Slot:</MyText>
<MyText style={tw`font-medium`}>
{new Date(order.slotInfo.time).toLocaleDateString('en-IN', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
})}
</MyText>
</View>
)}
</View>
</View>
{/* Items */}
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Items ({order.items.length})</MyText>
{order.items.map((item, index) => (
<View key={item.id} style={tw`flex-row items-center py-3 ${index !== order.items.length - 1 ? 'border-b border-gray-100' : ''}`}>
<View style={tw`flex-1`}>
<MyText style={tw`font-medium text-gray-800`} numberOfLines={2}>
{item.productName}
</MyText>
<MyText style={tw`text-sm text-gray-600`}>
Qty: {item.quantity} {item.unit} × {parseFloat(item.price.toString()).toFixed(2)}
</MyText>
</View>
<MyText style={tw`font-semibold text-gray-800`}>{item.amount.toFixed(2)}</MyText>
</View>
))}
</View>
{/* Payment Information */}
{(order.payment || order.paymentInfo) && (
<View style={tw`bg-white rounded-xl p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-3`}>Payment Information</MyText>
{order.payment && (
<View style={tw`space-y-2 mb-3`}>
<MyText style={tw`text-sm font-medium text-gray-700`}>Payment Details:</MyText>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Status:</MyText>
<MyText style={tw`font-medium capitalize`}>{order.payment.status}</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Gateway:</MyText>
<MyText style={tw`font-medium`}>{order.payment.gateway}</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Order ID:</MyText>
<MyText style={tw`font-medium`}>{order.payment.merchantOrderId}</MyText>
</View>
</View>
)}
{order.paymentInfo && (
<View style={tw`space-y-2`}>
<MyText style={tw`text-sm font-medium text-gray-700`}>Payment Info:</MyText>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Status:</MyText>
<MyText style={tw`font-medium capitalize`}>{order.paymentInfo.status}</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Gateway:</MyText>
<MyText style={tw`font-medium`}>{order.paymentInfo.gateway}</MyText>
</View>
<View style={tw`flex-row justify-between`}>
<MyText style={tw`text-gray-600`}>Order ID:</MyText>
<MyText style={tw`font-medium`}>{order.paymentInfo.merchantOrderId}</MyText>
</View>
</View>
)}
</View>
)}
{/* User Notes */}
{order.userNotes && (
<View style={tw`bg-blue-50 rounded-xl p-4 mb-4`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-2`}>Customer Notes</MyText>
<MyText style={tw`text-gray-700`}>{order.userNotes}</MyText>
</View>
)}
{/* Admin Notes */}
{order.adminNotes && (
<View style={tw`bg-yellow-50 rounded-xl p-4 mb-4`}>
<MyText style={tw`text-lg font-semibold text-gray-800 mb-2`}>Admin Notes</MyText>
<MyText style={tw`text-gray-700`}>{order.adminNotes}</MyText>
</View>
)}
{/* Total */}
<View style={tw`bg-blue-50 rounded-xl p-4`}>
<View style={tw`flex-row justify-between items-center`}>
<MyText style={tw`text-xl font-bold text-gray-800`}>Total Amount</MyText>
<MyText style={tw`text-2xl font-bold text-blue-600`}>{parseFloat(order.totalAmount.toString()).toFixed(2)}</MyText>
</View>
</View>
</View>
</ScrollView>
);
};

Binary file not shown.

View file

@ -1,6 +1,6 @@
ENV_MODE=PROD
DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy
# DATABASE_URL=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner
# DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy
DATABASE_URL=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner
PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
PHONE_PE_CLIENT_VERSION=1
@ -17,14 +17,10 @@ S3_REGION=apac
S3_ACCESS_KEY_ID=8fab47503efb9547b50e4fb317e35cc7
S3_SECRET_ACCESS_KEY=47c2eb5636843cf568dda7ad0959a3e42071303f26dbdff94bd45a3c33dcd950
S3_URL=https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com
S3_BUCKET_NAME=meatfarmer-dev
S3_BUCKET_NAME=meatfarmer
EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK-
JWT_SECRET=my_meatfarmer_jwt_secret_key
ASSETS_DOMAIN=https://assets2.freshyo.in/
API_CACHE_KEY=api-cache-dev
# CLOUDFLARE_API_TOKEN=I8Vp4E9TX58E8qEDeH0nTFDS2d2zXNYiXvbs4Ckj
CLOUDFLARE_API_TOKEN=N7jAg5X-RUj_fVfMW6zbfJ8qIYc81TSIKKlbZ6oh
CLOUDFLARE_ZONE_ID=edefbf750bfc3ff26ccd11e8e28dc8d7
ASSETS_DOMAIN=https://assets.freshyo.in/
# REDIS_URL=redis://default:redis_shafi_password@5.223.55.14:6379
REDIS_URL=redis://default:redis_shafi_password@57.128.212.174:6379
APP_URL=http://localhost:4000

File diff suppressed because one or more lines are too long

View file

@ -180,6 +180,6 @@ app.use((err: any, req: express.Request, res: express.Response, next: express.Ne
res.status(status).json({ message });
});
app.listen(4000, () => {
app.listen(4000, '::', () => {
console.log("Server is running on http://localhost:4000/api/mobile/");
});

View file

@ -5,8 +5,7 @@ import { eq } from "drizzle-orm";
import { ApiError } from "@/src/lib/api-error";
import { imageUploadS3, generateSignedUrlFromS3Url } from "@/src/lib/s3-client";
import { deleteS3Image } from "@/src/lib/delete-image";
import { scheduleStoreInitialization } from '@/src/stores/store-initializer';
import { initializeAllStores } from '@/src/stores/store-initializer';
/**
* Create a new product tag
@ -59,10 +58,9 @@ export const createTag = async (req: Request, res: Response) => {
.returning();
// Reinitialize stores to reflect changes in cache
scheduleStoreInitialization()
await initializeAllStores();
// Send response first
res.status(201).json({
return res.status(201).json({
tag: newTag,
message: "Tag created successfully",
});
@ -95,7 +93,7 @@ export const getAllTags = async (req: Request, res: Response) => {
* Get a single product tag by ID
*/
export const getTagById = async (req: Request, res: Response) => {
const id = req.params.id as string
const { id } = req.params;
const tag = await db.query.productTagInfo.findFirst({
where: eq(productTagInfo.id, parseInt(id)),
@ -121,7 +119,7 @@ export const getTagById = async (req: Request, res: Response) => {
* Update a product tag
*/
export const updateTag = async (req: Request, res: Response) => {
const id = req.params.id as string
const { id } = req.params;
const { tagName, tagDescription, isDashboardTag, relatedStores } = req.body;
// Get the current tag to check for existing image
@ -179,10 +177,9 @@ export const updateTag = async (req: Request, res: Response) => {
.returning();
// Reinitialize stores to reflect changes in cache
scheduleStoreInitialization()
await initializeAllStores();
// Send response first
res.status(200).json({
return res.status(200).json({
tag: updatedTag,
message: "Tag updated successfully",
});
@ -192,7 +189,7 @@ export const updateTag = async (req: Request, res: Response) => {
* Delete a product tag
*/
export const deleteTag = async (req: Request, res: Response) => {
const id = req.params.id as string
const { id } = req.params;
// Check if tag exists
const tag = await db.query.productTagInfo.findFirst({
@ -217,10 +214,9 @@ export const deleteTag = async (req: Request, res: Response) => {
await db.delete(productTagInfo).where(eq(productTagInfo.id, parseInt(id)));
// Reinitialize stores to reflect changes in cache
scheduleStoreInitialization()
await initializeAllStores();
// Send response first
res.status(200).json({
return res.status(200).json({
message: "Tag deleted successfully",
});
};

View file

@ -6,8 +6,7 @@ import { ApiError } from "@/src/lib/api-error";
import { imageUploadS3, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client";
import { deleteS3Image } from "@/src/lib/delete-image";
import type { SpecialDeal } from "@/src/db/types";
import { scheduleStoreInitialization } from '@/src/stores/store-initializer';
import { initializeAllStores } from '@/src/stores/store-initializer';
type CreateDeal = {
quantity: number;
@ -109,10 +108,9 @@ export const createProduct = async (req: Request, res: Response) => {
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
// Send response first
res.status(201).json({
return res.status(201).json({
product: newProduct,
deals: createdDeals,
message: "Product created successfully",
@ -123,7 +121,7 @@ export const createProduct = async (req: Request, res: Response) => {
* Update a product
*/
export const updateProduct = async (req: Request, res: Response) => {
const id = req.params.id as string
const { id } = req.params;
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals:dealsRaw, imagesToDelete:imagesToDeleteRaw, tagIds } = req.body;
@ -296,10 +294,9 @@ export const updateProduct = async (req: Request, res: Response) => {
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
// Send response first
res.status(200).json({
return res.status(200).json({
product: updatedProduct,
message: "Product updated successfully",
});

View file

@ -1,235 +0,0 @@
// Database Service - Central export for all database-related imports
// This file re-exports everything from postgresService to provide a clean abstraction layer
// Implementation is the responsibility of postgresService package
import { getOrderDetails as getOrderDetailsFromDb } from 'postgresService'
import type { AdminOrderDetails } from '@packages/shared'
// Re-export database connection
export { db } from 'postgresService'
// Re-export all schema exports
export * from 'postgresService'
// Re-export methods from postgresService (implementation lives there)
export {
// Banner methods
getBanners,
getBannerById,
createBanner,
updateBanner,
deleteBanner,
// Complaint methods
getComplaints,
resolveComplaint,
// Constants methods
getAllConstants,
upsertConstants,
// Coupon methods
getAllCoupons,
getCouponById,
invalidateCoupon,
validateCoupon,
getReservedCoupons,
getUsersForCoupon,
createCouponWithRelations,
updateCouponWithRelations,
generateCancellationCoupon,
createReservedCouponWithProducts,
createCouponForUser,
checkUsersExist,
checkCouponExists,
checkReservedCouponExists,
getOrderWithUser,
// Store methods
getAllStores,
getStoreById,
createStore,
updateStore,
deleteStore,
// Staff-user methods
getStaffUserByName,
getAllStaff,
getAllUsers,
getUserWithDetails,
updateUserSuspensionStatus,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
getAllRoles,
// User methods
createUserByMobile,
getUserByMobile,
getUnresolvedComplaintsCount,
getAllUsersWithFilters,
getOrderCountsByUserIds,
getLastOrdersByUserIds,
getSuspensionStatusesByUserIds,
getUserBasicInfo,
getUserSuspensionStatus,
getUserOrders,
getOrderStatusesByOrderIds,
getItemCountsByOrderIds,
upsertUserSuspension,
searchUsers,
getAllNotifCreds,
getAllUnloggedTokens,
getNotifTokensByUserIds,
getUserIncidentsWithRelations,
createUserIncident,
// Vendor-snippets methods
checkVendorSnippetExists,
getVendorSnippetById,
getVendorSnippetByCode,
getAllVendorSnippets,
createVendorSnippet,
updateVendorSnippet,
deleteVendorSnippet,
getProductsByIds,
getVendorSlotById,
getVendorOrdersBySlotId,
getOrderItemsByOrderIds,
getOrderStatusByOrderIds,
updateVendorOrderItemPackaging,
getVendorOrders,
// Product methods
getAllProducts,
getProductById,
deleteProduct,
createProduct,
updateProduct,
toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,
getAllUnits,
getAllProductTags,
getProductReviews,
respondToReview,
getAllProductGroups,
createProductGroup,
updateProductGroup,
deleteProductGroup,
addProductToGroup,
removeProductFromGroup,
updateProductPrices,
// Slots methods
getActiveSlotsWithProducts,
getActiveSlots,
getSlotsAfterDate,
getSlotByIdWithRelations,
createSlotWithRelations,
updateSlotWithRelations,
deleteSlotById,
updateSlotCapacity,
getSlotDeliverySequence,
updateSlotDeliverySequence,
// Order methods
updateOrderNotes,
updateOrderPackaged,
updateOrderDelivered,
updateOrderItemPackaging,
removeDeliveryCharge,
getSlotOrders,
updateAddressCoords,
getAllOrders,
rebalanceSlots,
cancelOrder,
deleteOrderById,
} from 'postgresService'
export async function getOrderDetails(orderId: number): Promise<AdminOrderDetails | null> {
return getOrderDetailsFromDb(orderId)
}
// Re-export all types from shared package
export type {
// Admin types
Banner,
Complaint,
ComplaintWithUser,
Constant,
ConstantUpdateResult,
Coupon,
CouponValidationResult,
UserMiniInfo,
Store,
StaffUser,
StaffRole,
AdminOrderRow,
AdminOrderDetails,
AdminOrderUpdateResult,
AdminOrderItemPackagingResult,
AdminOrderMessageResult,
AdminOrderBasicResult,
AdminGetSlotOrdersResult,
AdminGetAllOrdersResult,
AdminGetAllOrdersResultWithUserId,
AdminRebalanceSlotsResult,
AdminCancelOrderResult,
AdminUnit,
AdminProduct,
AdminProductWithRelations,
AdminProductWithDetails,
AdminProductTagInfo,
AdminProductTagWithProducts,
AdminProductListResponse,
AdminProductResponse,
AdminDeleteProductResult,
AdminToggleOutOfStockResult,
AdminUpdateSlotProductsResult,
AdminSlotProductIdsResult,
AdminSlotsProductIdsResult,
AdminProductReview,
AdminProductReviewWithSignedUrls,
AdminProductReviewsResult,
AdminProductReviewResponse,
AdminProductGroup,
AdminProductGroupsResult,
AdminProductGroupResponse,
AdminProductGroupInfo,
AdminUpdateProductPricesResult,
AdminDeliverySlot,
AdminSlotProductSummary,
AdminSlotWithProducts,
AdminSlotWithProductsAndSnippets,
AdminSlotWithProductsAndSnippetsBase,
AdminSlotsResult,
AdminSlotsListResult,
AdminSlotResult,
AdminSlotCreateResult,
AdminSlotUpdateResult,
AdminSlotDeleteResult,
AdminDeliverySequence,
AdminDeliverySequenceResult,
AdminUpdateDeliverySequenceResult,
AdminUpdateSlotCapacityResult,
AdminVendorSnippet,
AdminVendorSnippetWithAccess,
AdminVendorSnippetWithSlot,
AdminVendorSnippetProduct,
AdminVendorSnippetWithProducts,
AdminVendorSnippetCreateInput,
AdminVendorSnippetUpdateInput,
AdminVendorSnippetDeleteResult,
AdminVendorSnippetOrderProduct,
AdminVendorSnippetOrderSummary,
AdminVendorSnippetOrdersResult,
AdminVendorSnippetOrdersWithSlotResult,
AdminVendorOrderSummary,
AdminUpcomingSlotsResult,
AdminVendorUpdatePackagingResult,
} from '@packages/shared';
export type {
// User types
User,
UserDetails,
Address,
Product,
CartItem,
Order,
OrderItem,
Payment,
} from '@packages/shared';

View file

@ -2,6 +2,7 @@ import * as cron from 'node-cron';
import { db } from '@/src/db/db_index'
import { payments, orders, deliverySlotInfo, refunds } from '@/src/db/schema'
import { eq, and, gt, isNotNull } from 'drizzle-orm';
import { RazorpayPaymentService } from '@/src/lib/payments-utils'
interface PendingPaymentRecord {
payment: typeof payments.$inferSelect;
@ -19,34 +20,34 @@ export const createPaymentNotification = (record: PendingPaymentRecord) => {
export const checkRefundStatuses = async () => {
try {
// const initiatedRefunds = await db
// .select()
// .from(refunds)
// .where(and(
// eq(refunds.refundStatus, 'initiated'),
// isNotNull(refunds.merchantRefundId)
// ));
//
// // Process refunds concurrently using Promise.allSettled
// const promises = initiatedRefunds.map(async (refund) => {
// if (!refund.merchantRefundId) return;
//
// try {
// const razorpayRefund = await RazorpayPaymentService.fetchRefund(refund.merchantRefundId);
//
// if (razorpayRefund.status === 'processed') {
// await db
// .update(refunds)
// .set({ refundStatus: 'success', refundProcessedAt: new Date() })
// .where(eq(refunds.id, refund.id));
// }
// } catch (error) {
// console.error(`Error checking refund ${refund.id}:`, error);
// }
// });
//
// // Wait for all promises to complete
// await Promise.allSettled(promises);
const initiatedRefunds = await db
.select()
.from(refunds)
.where(and(
eq(refunds.refundStatus, 'initiated'),
isNotNull(refunds.merchantRefundId)
));
// Process refunds concurrently using Promise.allSettled
const promises = initiatedRefunds.map(async (refund) => {
if (!refund.merchantRefundId) return;
try {
const razorpayRefund = await RazorpayPaymentService.fetchRefund(refund.merchantRefundId);
if (razorpayRefund.status === 'processed') {
await db
.update(refunds)
.set({ refundStatus: 'success', refundProcessedAt: new Date() })
.where(eq(refunds.id, refund.id));
}
} catch (error) {
console.error(`Error checking refund ${refund.id}:`, error);
}
});
// Wait for all promises to complete
await Promise.allSettled(promises);
} catch (error) {
console.error('Error in checkRefundStatuses:', error);
}

View file

@ -1,376 +0,0 @@
import axios from 'axios'
import { scaffoldProducts } from '@/src/trpc/apis/common-apis/common'
import { scaffoldEssentialConsts } from '@/src/trpc/apis/common-apis/common-trpc-index'
import { scaffoldStores } from '@/src/trpc/apis/user-apis/apis/stores'
import { scaffoldSlotsWithProducts } from '@/src/trpc/apis/user-apis/apis/slots'
import { scaffoldBanners } from '@/src/trpc/apis/user-apis/apis/banners'
import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores'
import { storeInfo } from '@/src/db/schema'
import { db } from '@/src/db/db_index'
import { imageUploadS3 } from '@/src/lib/s3-client'
import { apiCacheKey, cloudflareApiToken, cloudflareZoneId, assetsDomain } from '@/src/lib/env-exporter'
import { CACHE_FILENAMES } from '@packages/shared'
import { retryWithExponentialBackoff } from '@/src/lib/retry'
function constructCacheUrl(path: string): string {
return `${assetsDomain}${apiCacheKey}/${path}`
}
export async function createProductsFile(): Promise<string> {
// Get products data from the API method
const productsData = await scaffoldProducts()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(productsData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.products}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.products)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createEssentialConstsFile(): Promise<string> {
// Get essential consts data from the API method
const essentialConstsData = await scaffoldEssentialConsts()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(essentialConstsData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.essentialConsts}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.essentialConsts)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createStoresFile(): Promise<string> {
// Get stores data from the API method
const storesData = await scaffoldStores()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(storesData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.stores}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.stores)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createSlotsFile(): Promise<string> {
// Get slots data from the API method
const slotsData = await scaffoldSlotsWithProducts()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(slotsData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.slots}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.slots)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createBannersFile(): Promise<string> {
// Get banners data from the API method
const bannersData = await scaffoldBanners()
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(bannersData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.banners}`)
// Purge cache with retry
const url = constructCacheUrl(CACHE_FILENAMES.banners)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createStoreFile(storeId: number): Promise<string> {
// Get store data from the API method
const storeData = await scaffoldStoreWithProducts(storeId)
// Convert to JSON string with pretty formatting
const jsonContent = JSON.stringify(storeData, null, 2)
// Convert to Buffer for S3 upload
const buffer = Buffer.from(jsonContent, 'utf-8')
// Upload to S3 at the specified path using apiCacheKey
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/stores/${storeId}.json`)
// Purge cache with retry
const url = constructCacheUrl(`stores/${storeId}.json`)
try {
await retryWithExponentialBackoff(() => clearUrlCache([url]))
console.log(`Cache purged for ${url}`)
} catch (error) {
console.error(`Failed to purge cache for ${url} after 3 retries:`, error)
}
return s3Key
}
export async function createAllStoresFiles(): Promise<string[]> {
// Fetch all store IDs from database
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
// Create cache files for all stores and collect URLs
const results: string[] = []
const urls: string[] = []
for (const store of stores) {
const s3Key = await createStoreFile(store.id)
results.push(s3Key)
urls.push(constructCacheUrl(`stores/${store.id}.json`))
}
console.log(`Created ${results.length} store cache files`)
// Purge all store caches in one batch with retry
try {
await retryWithExponentialBackoff(() => clearUrlCache(urls))
console.log(`Cache purged for ${urls.length} store files`)
} catch (error) {
console.error(`Failed to purge cache for store files after 3 retries. URLs: ${urls.join(', ')}`, error)
}
return results
}
export interface CreateAllCacheFilesResult {
products: string
essentialConsts: string
stores: string
slots: string
banners: string
individualStores: string[]
}
export async function createAllCacheFiles(): Promise<CreateAllCacheFilesResult> {
console.log('Starting creation of all cache files...')
// Create all global cache files in parallel
const [
productsKey,
essentialConstsKey,
storesKey,
slotsKey,
bannersKey,
individualStoreKeys,
] = await Promise.all([
createProductsFileInternal(),
createEssentialConstsFileInternal(),
createStoresFileInternal(),
createSlotsFileInternal(),
createBannersFileInternal(),
createAllStoresFilesInternal(),
])
// Collect all URLs for batch cache purge
const urls = [
constructCacheUrl(CACHE_FILENAMES.products),
constructCacheUrl(CACHE_FILENAMES.essentialConsts),
constructCacheUrl(CACHE_FILENAMES.stores),
constructCacheUrl(CACHE_FILENAMES.slots),
constructCacheUrl(CACHE_FILENAMES.banners),
...individualStoreKeys.map((_, index) => constructCacheUrl(`stores/${index + 1}.json`)),
]
// Purge all caches in one batch with retry
try {
await retryWithExponentialBackoff(() => clearUrlCache(urls))
console.log(`Cache purged for all ${urls.length} files`)
} catch (error) {
console.error(`Failed to purge cache for all files after 3 retries`, error)
}
console.log('All cache files created successfully')
return {
products: productsKey,
essentialConsts: essentialConstsKey,
stores: storesKey,
slots: slotsKey,
banners: bannersKey,
individualStores: individualStoreKeys,
}
}
// Internal versions that skip cache purging (for batch operations)
async function createProductsFileInternal(): Promise<string> {
const productsData = await scaffoldProducts()
const jsonContent = JSON.stringify(productsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.products}`)
}
async function createEssentialConstsFileInternal(): Promise<string> {
const essentialConstsData = await scaffoldEssentialConsts()
const jsonContent = JSON.stringify(essentialConstsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.essentialConsts}`)
}
async function createStoresFileInternal(): Promise<string> {
const storesData = await scaffoldStores()
const jsonContent = JSON.stringify(storesData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.stores}`)
}
async function createSlotsFileInternal(): Promise<string> {
const slotsData = await scaffoldSlotsWithProducts()
const jsonContent = JSON.stringify(slotsData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.slots}`)
}
async function createBannersFileInternal(): Promise<string> {
const bannersData = await scaffoldBanners()
const jsonContent = JSON.stringify(bannersData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
return await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.banners}`)
}
async function createAllStoresFilesInternal(): Promise<string[]> {
const stores = await db.select({ id: storeInfo.id }).from(storeInfo)
const results: string[] = []
for (const store of stores) {
const storeData = await scaffoldStoreWithProducts(store.id)
const jsonContent = JSON.stringify(storeData, null, 2)
const buffer = Buffer.from(jsonContent, 'utf-8')
const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/stores/${store.id}.json`)
results.push(s3Key)
}
console.log(`Created ${results.length} store cache files`)
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) {
console.log(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] }
}
}

View file

@ -17,12 +17,6 @@ export const s3Region = process.env.S3_REGION as string
export const assetsDomain = process.env.ASSETS_DOMAIN as string;
export const apiCacheKey = process.env.API_CACHE_KEY as string;
export const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN as string;
export const cloudflareZoneId = process.env.CLOUDFLARE_ZONE_ID as string;
export const s3Url = process.env.S3_URL as string
export const redisUrl = process.env.REDIS_URL as string

View file

@ -3,7 +3,6 @@ import { initializeAllStores } from '@/src/stores/store-initializer'
import { initializeUserNegativityStore } from '@/src/stores/user-negativity-store'
import { startOrderHandler, startCancellationHandler, publishOrder } from '@/src/lib/post-order-handler'
import { deleteOrders } from '@/src/lib/delete-orders'
import { createAllCacheFiles } from '@/src/lib/cloud_cache'
/**
* Initialize all application services
@ -26,10 +25,6 @@ export const initFunc = async (): Promise<void> => {
startCancellationHandler(),
]);
// Create all cache files after stores are initialized
await createAllCacheFiles();
console.log('Cache files created successfully');
console.log('Application initialization completed successfully');
} catch (error) {
console.error('Application initialization failed:', error);

View file

@ -1,4 +1,4 @@
// import Razorpay from "razorpay";
import Razorpay from "razorpay";
import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter"
import { db } from "@/src/db/db_index"
import { payments } from "@/src/db/schema"
@ -6,54 +6,54 @@ import { payments } from "@/src/db/schema"
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];
export class RazorpayPaymentService {
// private static instance = new Razorpay({
// key_id: razorpayId,
// key_secret: razorpaySecret,
// });
//
private static instance = new Razorpay({
key_id: razorpayId,
key_secret: razorpaySecret,
});
static async createOrder(orderId: number, amount: string) {
// Create Razorpay order
// const razorpayOrder = await this.instance.orders.create({
// amount: parseFloat(amount) * 100, // Convert to paisa
// currency: 'INR',
// receipt: `order_${orderId}`,
// notes: {
// customerOrderId: orderId.toString(),
// },
// });
//
// return razorpayOrder;
const razorpayOrder = await this.instance.orders.create({
amount: parseFloat(amount) * 100, // Convert to paisa
currency: 'INR',
receipt: `order_${orderId}`,
notes: {
customerOrderId: orderId.toString(),
},
});
return razorpayOrder;
}
static async insertPaymentRecord(orderId: number, razorpayOrder: any, tx?: Tx) {
// Use transaction if provided, otherwise use db
// const dbInstance = tx || db;
//
// // Insert payment record
// const [payment] = await dbInstance
// .insert(payments)
// .values({
// status: 'pending',
// gateway: 'razorpay',
// orderId,
// token: orderId.toString(),
// merchantOrderId: razorpayOrder.id,
// payload: razorpayOrder,
// })
// .returning();
//
// return payment;
const dbInstance = tx || db;
// Insert payment record
const [payment] = await dbInstance
.insert(payments)
.values({
status: 'pending',
gateway: 'razorpay',
orderId,
token: orderId.toString(),
merchantOrderId: razorpayOrder.id,
payload: razorpayOrder,
})
.returning();
return payment;
}
static async initiateRefund(paymentId: string, amount: number) {
// const refund = await this.instance.payments.refund(paymentId, {
// amount,
// });
// return refund;
const refund = await this.instance.payments.refund(paymentId, {
amount,
});
return refund;
}
static async fetchRefund(refundId: string) {
// const refund = await this.instance.refunds.fetch(refundId);
// return refund;
const refund = await this.instance.refunds.fetch(refundId);
return refund;
}
}

View file

@ -35,7 +35,10 @@ const formatOrderMessageWithFullData = (ordersData: any[]): string => {
message += '📦 <b>Items:</b>\n';
order.orderItems?.forEach((item: any) => {
message += `${item.product?.name || 'Unknown'} x${item.quantity}\n`;
const productQuantity = item.product?.productQuantity ?? 1
const unitNotation = item.product?.unit?.shortNotation || ''
const quantityWithUnit = unitNotation ? `${productQuantity}${unitNotation}` : `${productQuantity}`
message += `${item.product?.name || 'Unknown'} ${quantityWithUnit} x${item.quantity}\n`;
});
message += `\n💰 <b>Total:</b> ₹${order.totalAmount}\n`;
@ -72,7 +75,12 @@ const formatCancellationMessage = (orderData: any, cancellationData: Cancellatio
📞 <b>Phone:</b> ${orderData.address?.phone || 'N/A'}
📦 <b>Items:</b>
${orderData.orderItems?.map((item: any) => `${item.product?.name || 'Unknown'} x${item.quantity}`).join('\n') || ' N/A'}
${orderData.orderItems?.map((item: any) => {
const productQuantity = item.product?.productQuantity ?? 1
const unitNotation = item.product?.unit?.shortNotation || ''
const quantityWithUnit = unitNotation ? `${productQuantity}${unitNotation}` : `${productQuantity}`
return `${item.product?.name || 'Unknown'} ${quantityWithUnit} x${item.quantity}`
}).join('\n') || ' N/A'}
💰 <b>Total:</b> ${orderData.totalAmount}
💳 <b>Refund:</b> ${orderData.refundStatus === 'na' ? 'N/A (COD)' : orderData.refundStatus || 'Pending'}
@ -102,7 +110,7 @@ export const startOrderHandler = async (): Promise<void> => {
where: inArray(orders.id, orderIds),
with: {
address: true,
orderItems: { with: { product: true } },
orderItems: { with: { product: { with: { unit: true } } } },
slot: true,
},
});
@ -147,7 +155,7 @@ export const startCancellationHandler = async (): Promise<void> => {
where: eq(orders.id, cancellationData.orderId),
with: {
address: true,
orderItems: { with: { product: true } },
orderItems: { with: { product: { with: { unit: true } } } },
refunds: true,
},
});

View file

@ -1,23 +0,0 @@
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
}

View file

@ -4,10 +4,6 @@ import { initializeProducts } from '@/src/stores/product-store'
import { initializeProductTagStore } from '@/src/stores/product-tag-store'
import { initializeSlotStore } from '@/src/stores/slot-store'
import { initializeBannerStore } from '@/src/stores/banner-store'
import { createAllCacheFiles } from '@/src/lib/cloud_cache'
const STORE_INIT_DELAY_MS = 3 * 60 * 1000
let storeInitializationTimeout: NodeJS.Timeout | null = null
/**
* Initialize all application stores
@ -33,27 +29,8 @@ export const initializeAllStores = async (): Promise<void> => {
]);
console.log('All application stores initialized successfully');
// Regenerate all cache files (fire-and-forget)
createAllCacheFiles().catch(error => {
console.error('Failed to regenerate cache files during store initialization:', error)
})
} catch (error) {
console.error('Application stores initialization failed:', error);
throw error;
}
};
export const scheduleStoreInitialization = (): void => {
if (storeInitializationTimeout) {
clearTimeout(storeInitializationTimeout)
storeInitializationTimeout = null
}
storeInitializationTimeout = setTimeout(() => {
storeInitializationTimeout = null
initializeAllStores().catch(error => {
console.error('Scheduled store initialization failed:', error)
})
}, STORE_INIT_DELAY_MS)
}

View file

@ -0,0 +1,32 @@
import { z } from 'zod';
import { addressZones, addressAreas } from '@/src/db/schema'
import { eq, desc } from 'drizzle-orm';
import { db } from '@/src/db/db_index'
import { router,protectedProcedure } from '@/src/trpc/trpc-index'
const addressRouter = router({
getZones: protectedProcedure.query(async () => {
const zones = await db.select().from(addressZones).orderBy(desc(addressZones.addedAt));
return zones
}),
getAreas: protectedProcedure.query(async () => {
const areas = await db.select().from(addressAreas).orderBy(desc(addressAreas.createdAt));
return areas
}),
createZone: protectedProcedure.input(z.object({ zoneName: z.string().min(1) })).mutation(async ({ input }) => {
const zone = await db.insert(addressZones).values({ zoneName: input.zoneName }).returning();
return {zone: zone};
}),
createArea: protectedProcedure.input(z.object({ placeName: z.string().min(1), zoneId: z.number().nullable() })).mutation(async ({ input }) => {
const area = await db.insert(addressAreas).values({ placeName: input.placeName, zoneId: input.zoneId }).returning();
return {area};
}),
// TODO: Add update and delete mutations if needed
});
export default addressRouter;

View file

@ -2,6 +2,7 @@
import { router } from '@/src/trpc/trpc-index'
import { complaintRouter } from '@/src/trpc/apis/admin-apis/apis/complaint'
import { couponRouter } from '@/src/trpc/apis/admin-apis/apis/coupon'
import { cancelledOrdersRouter } from '@/src/trpc/apis/admin-apis/apis/cancelled-orders'
import { orderRouter } from '@/src/trpc/apis/admin-apis/apis/order'
import { vendorSnippetsRouter } from '@/src/trpc/apis/admin-apis/apis/vendor-snippets'
import { slotsRouter } from '@/src/trpc/apis/admin-apis/apis/slots'
@ -9,6 +10,7 @@ import { productRouter } from '@/src/trpc/apis/admin-apis/apis/product'
import { staffUserRouter } from '@/src/trpc/apis/admin-apis/apis/staff-user'
import { storeRouter } from '@/src/trpc/apis/admin-apis/apis/store'
import { adminPaymentsRouter } from '@/src/trpc/apis/admin-apis/apis/payments'
import addressRouter from '@/src/trpc/apis/admin-apis/apis/address'
import { bannerRouter } from '@/src/trpc/apis/admin-apis/apis/banner'
import { userRouter } from '@/src/trpc/apis/admin-apis/apis/user'
import { constRouter } from '@/src/trpc/apis/admin-apis/apis/const'
@ -16,6 +18,7 @@ import { constRouter } from '@/src/trpc/apis/admin-apis/apis/const'
export const adminRouter = router({
complaint: complaintRouter,
coupon: couponRouter,
cancelledOrders: cancelledOrdersRouter,
order: orderRouter,
vendorSnippets: vendorSnippetsRouter,
slots: slotsRouter,
@ -23,6 +26,7 @@ export const adminRouter = router({
staffUser: staffUserRouter,
store: storeRouter,
payments: adminPaymentsRouter,
address: addressRouter,
banner: bannerRouter,
user: userRouter,
const: constRouter,

View file

@ -1,34 +1,22 @@
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { homeBanners } from '@/src/db/schema'
import { eq, and, desc, sql } from 'drizzle-orm';
import { protectedProcedure, router } from '@/src/trpc/trpc-index'
import { extractKeyFromPresignedUrl, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { ApiError } from '@/src/lib/api-error';
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import {
getBanners as getBannersFromDb,
getBannerById as getBannerByIdFromDb,
createBanner as createBannerInDb,
updateBanner as updateBannerInDb,
deleteBanner as deleteBannerFromDb,
} from '@/src/dbService'
import type { Banner } from '@packages/shared'
import { initializeAllStores } from '@/src/stores/store-initializer'
export const bannerRouter = router({
// Get all banners
getBanners: protectedProcedure
.query(async (): Promise<{ banners: Banner[] }> => {
.query(async () => {
try {
// Using dbService helper (new implementation)
const banners = await getBannersFromDb();
// Old implementation - direct DB query:
// const banners = await db.query.homeBanners.findMany({
// orderBy: desc(homeBanners.createdAt), // Order by creation date instead
const banners = await db.query.homeBanners.findMany({
orderBy: desc(homeBanners.createdAt), // Order by creation date instead
// Removed product relationship since we now use productIds array
// });
});
// Convert S3 keys to signed URLs for client
const bannersWithSignedUrls = await Promise.all(
@ -66,17 +54,11 @@ export const bannerRouter = router({
// Get single banner by ID
getBanner: protectedProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }): Promise<Banner | null> => {
// Using dbService helper (new implementation)
const banner = await getBannerByIdFromDb(input.id);
/*
// Old implementation - direct DB query:
.query(async ({ input }) => {
const banner = await db.query.homeBanners.findFirst({
where: eq(homeBanners.id, input.id),
// Removed product relationship since we now use productIds array
});
*/
if (banner) {
try {
@ -108,22 +90,8 @@ export const bannerRouter = router({
redirectUrl: z.string().url().optional(),
// serialNum removed completely
}))
.mutation(async ({ input }): Promise<Banner> => {
.mutation(async ({ input }) => {
try {
// Using dbService helper (new implementation)
const imageUrl = extractKeyFromPresignedUrl(input.imageUrl)
const banner = await createBannerInDb({
name: input.name,
imageUrl: imageUrl,
description: input.description ?? null,
productIds: input.productIds || [],
redirectUrl: input.redirectUrl ?? null,
serialNum: 999, // Default value, not used
isActive: false, // Default to inactive
});
/*
// Old implementation - direct DB query:
const imageUrl = extractKeyFromPresignedUrl(input.imageUrl)
const [banner] = await db.insert(homeBanners).values({
name: input.name,
@ -134,10 +102,9 @@ export const bannerRouter = router({
serialNum: 999, // Default value, not used
isActive: false, // Default to inactive
}).returning();
*/
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return banner;
} catch (error) {
@ -158,28 +125,9 @@ export const bannerRouter = router({
serialNum: z.number().nullable().optional(),
isActive: z.boolean().optional(),
}))
.mutation(async ({ input }): Promise<Banner> => {
.mutation(async ({ input }) => {
try {
// Using dbService helper (new implementation)
const { id, ...updateData } = input;
// Extract S3 key from presigned URL if imageUrl is provided
const processedData = {
...updateData,
...(updateData.imageUrl && {
imageUrl: extractKeyFromPresignedUrl(updateData.imageUrl)
}),
};
// Handle serialNum null case
if ('serialNum' in processedData && processedData.serialNum === null) {
processedData.serialNum = null;
}
const banner = await updateBannerInDb(id, processedData);
/*
// Old implementation - direct DB query:
const { id, ...updateData } = input;
const incomingProductIds = input.productIds;
// Extract S3 key from presigned URL if imageUrl is provided
@ -201,10 +149,9 @@ export const bannerRouter = router({
.set({ ...finalData, lastUpdated: new Date(), })
.where(eq(homeBanners.id, id))
.returning();
*/
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return banner;
} catch (error) {
@ -216,17 +163,11 @@ export const bannerRouter = router({
// Delete banner
deleteBanner: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }): Promise<{ success: true }> => {
// Using dbService helper (new implementation)
await deleteBannerFromDb(input.id);
/*
// Old implementation - direct DB query:
.mutation(async ({ input }) => {
await db.delete(homeBanners).where(eq(homeBanners.id, input.id));
*/
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return { success: true };
}),

View file

@ -0,0 +1,179 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { orders, orderStatus, users, addresses, orderItems, productInfo, units, refunds } from '@/src/db/schema'
import { eq, desc } from 'drizzle-orm';
const updateCancellationReviewSchema = z.object({
orderId: z.number(),
cancellationReviewed: z.boolean(),
adminNotes: z.string().optional(),
});
const updateRefundSchema = z.object({
orderId: z.number(),
isRefundDone: z.boolean(),
});
export const cancelledOrdersRouter = router({
getAll: protectedProcedure
.query(async () => {
// First get cancelled order statuses with order details
const cancelledOrderStatuses = await db.query.orderStatus.findMany({
where: eq(orderStatus.isCancelled, true),
with: {
order: {
with: {
user: true,
address: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
refunds: true,
},
},
},
orderBy: [desc(orderStatus.orderTime)],
});
const filteredStatuses = cancelledOrderStatuses.filter(status => {
return status.order.isCod || status.paymentStatus === 'success';
});
return filteredStatuses.map(status => {
const refund = status.order.refunds[0];
return {
id: status.order.id,
readableId: status.order.id,
customerName: `${status.order.user.name}`,
address: `${status.order.address.addressLine1}, ${status.order.address.city}`,
totalAmount: status.order.totalAmount,
cancellationReviewed: status.cancellationReviewed || false,
isRefundDone: refund?.refundStatus === 'processed' || false,
adminNotes: status.order.adminNotes,
cancelReason: status.cancelReason,
paymentMode: status.order.isCod ? 'COD' : 'Online',
paymentStatus: status.paymentStatus || 'pending',
items: status.order.orderItems.map(item => ({
name: item.product.name,
quantity: item.quantity,
price: item.price,
unit: item.product.unit?.shortNotation,
amount: parseFloat(item.price.toString()) * parseFloat(item.quantity || '0'),
})),
createdAt: status.order.createdAt,
};
});
}),
updateReview: protectedProcedure
.input(updateCancellationReviewSchema)
.mutation(async ({ input }) => {
const { orderId, cancellationReviewed, adminNotes } = input;
const result = await db.update(orderStatus)
.set({
cancellationReviewed,
cancellationAdminNotes: adminNotes || null,
cancellationReviewedAt: new Date(),
})
.where(eq(orderStatus.orderId, orderId))
.returning();
if (result.length === 0) {
throw new Error("Cancellation record not found");
}
return result[0];
}),
getById: protectedProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
const { id } = input;
// Get cancelled order with full details
const cancelledOrderStatus = await db.query.orderStatus.findFirst({
where: eq(orderStatus.id, id),
with: {
order: {
with: {
user: true,
address: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
},
},
},
});
if (!cancelledOrderStatus || !cancelledOrderStatus.isCancelled) {
throw new Error("Cancelled order not found");
}
// Get refund details separately
const refund = await db.query.refunds.findFirst({
where: eq(refunds.orderId, cancelledOrderStatus.orderId),
});
const order = cancelledOrderStatus.order;
// Format the response similar to the getAll method
const formattedOrder = {
id: order.id,
readableId: order.id,
customerName: order.user.name,
address: `${order.address.addressLine1}${order.address.addressLine2 ? ', ' + order.address.addressLine2 : ''}, ${order.address.city}, ${order.address.state} ${order.address.pincode}`,
totalAmount: order.totalAmount,
cancellationReviewed: cancelledOrderStatus.cancellationReviewed || false,
isRefundDone: refund?.refundStatus === 'processed' || false,
adminNotes: cancelledOrderStatus.cancellationAdminNotes || null,
cancelReason: cancelledOrderStatus.cancelReason || null,
items: order.orderItems.map((item: any) => ({
name: item.product.name,
quantity: item.quantity,
price: parseFloat(item.price.toString()),
unit: item.product.unit?.shortNotation || 'unit',
amount: parseFloat(item.price.toString()) * parseFloat(item.quantity),
image: item.product.images?.[0] || null,
})),
createdAt: order.createdAt.toISOString(),
};
return { order: formattedOrder };
}),
updateRefund: protectedProcedure
.input(updateRefundSchema)
.mutation(async ({ input }) => {
const { orderId, isRefundDone } = input;
const refundStatus = isRefundDone ? 'processed' : 'none';
const result = await db.update(refunds)
.set({
refundStatus,
refundProcessedAt: isRefundDone ? new Date() : null,
})
.where(eq(refunds.orderId, orderId))
.returning();
if (result.length === 0) {
throw new Error("Cancellation record not found");
}
return result[0];
}),
});

View file

@ -1,8 +1,9 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { complaints, users } from '@/src/db/schema'
import { eq, desc, lt, and } from 'drizzle-orm';
import { generateSignedUrlsFromS3Urls } from '@/src/lib/s3-client'
import { getComplaints as getComplaintsFromDb, resolveComplaint as resolveComplaintInDb } from '@/src/dbService'
import type { ComplaintWithUser } from '@packages/shared'
export const complaintRouter = router({
getAll: protectedProcedure
@ -10,27 +11,7 @@ export const complaintRouter = router({
cursor: z.number().optional(),
limit: z.number().default(20),
}))
.query(async ({ input }): Promise<{
complaints: Array<{
id: number;
text: string;
userId: number;
userName: string | null;
userMobile: string | null;
orderId: number | null;
status: string;
createdAt: Date;
images: string[];
}>;
nextCursor?: number;
}> => {
const { cursor, limit } = input;
// Using dbService helper (new implementation)
const { complaints: complaintsData, hasMore } = await getComplaintsFromDb(cursor, limit);
/*
// Old implementation - direct DB query:
.query(async ({ input }) => {
const { cursor, limit } = input;
let whereCondition = cursor
@ -56,13 +37,10 @@ export const complaintRouter = router({
.limit(limit + 1);
const hasMore = complaintsData.length > limit;
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData;
*/
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData;
const complaintsWithSignedImages = await Promise.all(
complaintsToReturn.map(async (c: ComplaintWithUser) => {
complaintsToReturn.map(async (c) => {
const signedImages = c.images
? await generateSignedUrlsFromS3Urls(c.images as string[])
: [];
@ -91,17 +69,11 @@ export const complaintRouter = router({
resolve: protectedProcedure
.input(z.object({ id: z.string(), response: z.string().optional() }))
.mutation(async ({ input }): Promise<{ message: string }> => {
// Using dbService helper (new implementation)
await resolveComplaintInDb(parseInt(input.id), input.response);
/*
// Old implementation - direct DB query:
.mutation(async ({ input }) => {
await db
.update(complaints)
.set({ isResolved: true, response: input.response })
.where(eq(complaints.id, parseInt(input.id)));
*/
return { message: 'Complaint resolved successfully' };
}),

View file

@ -1,27 +1,22 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { keyValStore } from '@/src/db/schema'
import { computeConstants } from '@/src/lib/const-store'
import { CONST_KEYS } from '@/src/lib/const-keys'
import { getAllConstants as getAllConstantsFromDb, upsertConstants as upsertConstantsInDb } from '@/src/dbService'
import type { Constant, ConstantUpdateResult } from '@packages/shared'
export const constRouter = router({
getConstants: protectedProcedure
.query(async (): Promise<Constant[]> => {
// Using dbService helper (new implementation)
const constants = await getAllConstantsFromDb();
.query(async () => {
/*
// Old implementation - direct DB query:
const constants = await db.select().from(keyValStore);
const resp = constants.map(c => ({
key: c.key,
value: c.value,
}));
*/
return constants;
return resp;
}),
updateConstants: protectedProcedure
@ -31,7 +26,7 @@ export const constRouter = router({
value: z.any(),
})),
}))
.mutation(async ({ input }): Promise<ConstantUpdateResult> => {
.mutation(async ({ input }) => {
const { constants } = input;
const validKeys = Object.values(CONST_KEYS) as string[];
@ -43,11 +38,6 @@ export const constRouter = router({
throw new Error(`Invalid constant keys: ${invalidKeys.join(', ')}`);
}
// Using dbService helper (new implementation)
await upsertConstantsInDb(constants);
/*
// Old implementation - direct DB query:
await db.transaction(async (tx) => {
for (const { key, value } of constants) {
await tx.insert(keyValStore)
@ -58,7 +48,6 @@ export const constRouter = router({
});
}
});
*/
// Refresh all constants in Redis after database update
await computeConstants();

View file

@ -1,26 +1,9 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { coupons, users, staffUsers, orders, couponApplicableUsers, couponApplicableProducts, orderStatus, reservedCoupons } from '@/src/db/schema'
import { eq, and, like, or, inArray, lt } from 'drizzle-orm';
import dayjs from 'dayjs';
import {
// Batch 1 - Non-transaction methods
getAllCoupons as getAllCouponsFromDb,
getCouponById as getCouponByIdFromDb,
invalidateCoupon as invalidateCouponInDb,
validateCoupon as validateCouponInDb,
getReservedCoupons as getReservedCouponsFromDb,
getUsersForCoupon as getUsersForCouponFromDb,
// Batch 2 - Transaction methods
createCouponWithRelations,
updateCouponWithRelations,
generateCancellationCoupon,
createReservedCouponWithProducts,
createCouponForUser,
checkUsersExist,
checkCouponExists,
checkReservedCouponExists,
getOrderWithUser,
} from '@/src/dbService'
import type { Coupon, CouponValidationResult, UserMiniInfo } from '@packages/shared'
const createCouponBodySchema = z.object({
couponCode: z.string().optional(),
@ -48,7 +31,7 @@ const validateCouponBodySchema = z.object({
export const couponRouter = router({
create: protectedProcedure
.input(createCouponBodySchema)
.mutation(async ({ input, ctx }): Promise<Coupon> => {
.mutation(async ({ input, ctx }) => {
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input;
// Validation: ensure at least one discount type is provided
@ -66,6 +49,17 @@ export const couponRouter = router({
throw new Error("Cannot be both user-based and apply for all users");
}
// If applicableUsers is provided, verify users exist
if (applicableUsers && applicableUsers.length > 0) {
const existingUsers = await db.query.users.findMany({
where: inArray(users.id, applicableUsers),
columns: { id: true },
});
if (existingUsers.length !== applicableUsers.length) {
throw new Error("Some applicable users not found");
}
}
// Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id;
if (!staffUserId) {
@ -75,46 +69,21 @@ export const couponRouter = router({
// Generate coupon code if not provided
let finalCouponCode = couponCode;
if (!finalCouponCode) {
// Generate a unique coupon code
const timestamp = Date.now().toString().slice(-6);
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
finalCouponCode = `MF${timestamp}${random}`;
}
// Using dbService helper (new implementation)
const codeExists = await checkCouponExists(finalCouponCode);
if (codeExists) {
// Check if coupon code already exists
const existingCoupon = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, finalCouponCode),
});
if (existingCoupon) {
throw new Error("Coupon code already exists");
}
// If applicableUsers is provided, verify users exist
if (applicableUsers && applicableUsers.length > 0) {
const usersExist = await checkUsersExist(applicableUsers);
if (!usersExist) {
throw new Error("Some applicable users not found");
}
}
const coupon = await createCouponWithRelations(
{
couponCode: finalCouponCode,
isUserBased: isUserBased || false,
discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(),
productIds: productIds || null,
createdBy: staffUserId,
maxValue: maxValue?.toString(),
isApplyForAll: isApplyForAll || false,
validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser,
exclusiveApply: exclusiveApply || false,
},
applicableUsers,
applicableProducts
);
/*
// Old implementation - direct DB query with transaction:
const result = await db.insert(coupons).values({
couponCode: finalCouponCode,
isUserBased: isUserBased || false,
@ -126,7 +95,7 @@ export const couponRouter = router({
maxValue: maxValue?.toString(),
isApplyForAll: isApplyForAll || false,
validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser,
maxLimitForUser: maxLimitForUser,
exclusiveApply: exclusiveApply || false,
}).returning();
@ -151,7 +120,6 @@ export const couponRouter = router({
}))
);
}
*/
return coupon;
}),
@ -162,22 +130,71 @@ export const couponRouter = router({
limit: z.number().default(50),
search: z.string().optional(),
}))
.query(async ({ input }): Promise<{ coupons: any[]; nextCursor?: number }> => {
.query(async ({ input }) => {
const { cursor, limit, search } = input;
const { coupons: couponsList, hasMore } = await getAllCouponsFromDb(cursor, limit, search);
let whereCondition = undefined;
const conditions = [];
const nextCursor = hasMore ? couponsList[couponsList.length - 1].id : undefined;
if (cursor) {
conditions.push(lt(coupons.id, cursor));
}
if (search && search.trim()) {
conditions.push(like(coupons.couponCode, `%${search}%`));
}
if (conditions.length > 0) {
whereCondition = and(...conditions);
}
const result = await db.query.coupons.findMany({
where: whereCondition,
with: {
creator: true,
applicableUsers: {
with: {
user: true,
},
},
applicableProducts: {
with: {
product: true,
},
},
},
orderBy: (coupons, { desc }) => [desc(coupons.createdAt)],
limit: limit + 1,
});
const hasMore = result.length > limit;
const couponsList = hasMore ? result.slice(0, limit) : result;
const nextCursor = hasMore ? result[result.length - 1].id : undefined;
return { coupons: couponsList, nextCursor };
}),
getById: protectedProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }): Promise<any> => {
.query(async ({ input }) => {
const couponId = input.id;
const result = await getCouponByIdFromDb(couponId);
const result = await db.query.coupons.findFirst({
where: eq(coupons.id, couponId),
with: {
creator: true,
applicableUsers: {
with: {
user: true,
},
},
applicableProducts: {
with: {
product: true,
},
},
},
});
if (!result) {
throw new Error("Coupon not found");
@ -186,8 +203,8 @@ export const couponRouter = router({
return {
...result,
productIds: (result.productIds as number[]) || undefined,
applicableUsers: result.applicableUsers.map((au: any) => au.user),
applicableProducts: result.applicableProducts.map((ap: any) => ap.product),
applicableUsers: result.applicableUsers.map(au => au.user),
applicableProducts: result.applicableProducts.map(ap => ap.product),
};
}),
@ -198,7 +215,7 @@ export const couponRouter = router({
isInvalidated: z.boolean().optional(),
}),
}))
.mutation(async ({ input }): Promise<Coupon> => {
.mutation(async ({ input }) => {
const { id, updates } = input;
// Validation: ensure discount types are valid
@ -208,31 +225,43 @@ export const couponRouter = router({
}
}
// Prepare update data
const updateData: any = {};
if (updates.couponCode !== undefined) updateData.couponCode = updates.couponCode;
if (updates.isUserBased !== undefined) updateData.isUserBased = updates.isUserBased;
if (updates.discountPercent !== undefined) updateData.discountPercent = updates.discountPercent?.toString();
if (updates.flatDiscount !== undefined) updateData.flatDiscount = updates.flatDiscount?.toString();
if (updates.minOrder !== undefined) updateData.minOrder = updates.minOrder?.toString();
if (updates.maxValue !== undefined) updateData.maxValue = updates.maxValue?.toString();
if (updates.isApplyForAll !== undefined) updateData.isApplyForAll = updates.isApplyForAll;
if (updates.validTill !== undefined) updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null;
if (updates.maxLimitForUser !== undefined) updateData.maxLimitForUser = updates.maxLimitForUser;
if (updates.exclusiveApply !== undefined) updateData.exclusiveApply = updates.exclusiveApply;
if (updates.isInvalidated !== undefined) updateData.isInvalidated = updates.isInvalidated;
if (updates.productIds !== undefined) updateData.productIds = updates.productIds;
// If updating to user-based, applicableUsers is required
if (updates.isUserBased && (!updates.applicableUsers || updates.applicableUsers.length === 0)) {
const existingCount = await db.$count(couponApplicableUsers, eq(couponApplicableUsers.couponId, id));
if (existingCount === 0) {
throw new Error("applicableUsers is required for user-based coupons");
}
}
// Using dbService helper (new implementation)
const coupon = await updateCouponWithRelations(
id,
updateData,
updates.applicableUsers,
updates.applicableProducts
);
// If applicableUsers is provided, verify users exist
if (updates.applicableUsers && updates.applicableUsers.length > 0) {
const existingUsers = await db.query.users.findMany({
where: inArray(users.id, updates.applicableUsers),
columns: { id: true },
});
if (existingUsers.length !== updates.applicableUsers.length) {
throw new Error("Some applicable users not found");
}
}
const updateData: any = { ...updates };
delete updateData.applicableUsers; // Remove since we use couponApplicableUsers table
if (updates.discountPercent !== undefined) {
updateData.discountPercent = updates.discountPercent?.toString();
}
if (updates.flatDiscount !== undefined) {
updateData.flatDiscount = updates.flatDiscount?.toString();
}
if (updates.minOrder !== undefined) {
updateData.minOrder = updates.minOrder?.toString();
}
if (updates.maxValue !== undefined) {
updateData.maxValue = updates.maxValue?.toString();
}
if (updates.validTill !== undefined) {
updateData.validTill = updates.validTill ? dayjs(updates.validTill).toDate() : null;
}
/*
// Old implementation - direct DB query:
const result = await db.update(coupons)
.set(updateData)
.where(eq(coupons.id, id))
@ -242,6 +271,8 @@ export const couponRouter = router({
throw new Error("Coupon not found");
}
console.log('updated coupon successfully')
// Update applicable users: delete existing and insert new
if (updates.applicableUsers !== undefined) {
await db.delete(couponApplicableUsers).where(eq(couponApplicableUsers.couponId, id));
@ -267,33 +298,88 @@ export const couponRouter = router({
);
}
}
*/
return coupon;
return result[0];
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }): Promise<{ message: string }> => {
.mutation(async ({ input }) => {
const { id } = input;
await invalidateCouponInDb(id);
const result = await db.update(coupons)
.set({ isInvalidated: true })
.where(eq(coupons.id, id))
.returning();
if (result.length === 0) {
throw new Error("Coupon not found");
}
return { message: "Coupon invalidated successfully" };
}),
validate: protectedProcedure
.input(validateCouponBodySchema)
.query(async ({ input }): Promise<CouponValidationResult> => {
.query(async ({ input }) => {
const { code, userId, orderAmount } = input;
if (!code || typeof code !== 'string') {
return { valid: false, message: "Invalid coupon code" };
}
const result = await validateCouponInDb(code, userId, orderAmount);
const coupon = await db.query.coupons.findFirst({
where: and(
eq(coupons.couponCode, code.toUpperCase()),
eq(coupons.isInvalidated, false)
),
});
return result;
if (!coupon) {
return { valid: false, message: "Coupon not found or invalidated" };
}
// Check expiry date
if (coupon.validTill && new Date(coupon.validTill) < new Date()) {
return { valid: false, message: "Coupon has expired" };
}
// Check if coupon applies to all users or specific user
if (!coupon.isApplyForAll && !coupon.isUserBased) {
return { valid: false, message: "Coupon is not available for use" };
}
// Check minimum order amount
const minOrderValue = coupon.minOrder ? parseFloat(coupon.minOrder) : 0;
if (minOrderValue > 0 && orderAmount < minOrderValue) {
return { valid: false, message: `Minimum order amount is ${minOrderValue}` };
}
// Calculate discount
let discountAmount = 0;
if (coupon.discountPercent) {
const percent = parseFloat(coupon.discountPercent);
discountAmount = (orderAmount * percent) / 100;
} else if (coupon.flatDiscount) {
discountAmount = parseFloat(coupon.flatDiscount);
}
// Apply max value limit
const maxValueLimit = coupon.maxValue ? parseFloat(coupon.maxValue) : 0;
if (maxValueLimit > 0 && discountAmount > maxValueLimit) {
discountAmount = maxValueLimit;
}
return {
valid: true,
discountAmount,
coupon: {
id: coupon.id,
discountPercent: coupon.discountPercent,
flatDiscount: coupon.flatDiscount,
maxValue: coupon.maxValue,
}
};
}),
generateCancellationCoupon: protectedProcedure
@ -302,7 +388,7 @@ export const couponRouter = router({
orderId: z.number(),
})
)
.mutation(async ({ input, ctx }): Promise<Coupon> => {
.mutation(async ({ input, ctx }) => {
const { orderId } = input;
// Get staff user ID from auth middleware
@ -311,13 +397,31 @@ export const couponRouter = router({
throw new Error("Unauthorized");
}
// Using dbService helper (new implementation)
const order = await getOrderWithUser(orderId);
// Find the order with user and order status information
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
orderStatus: true,
},
});
if (!order) {
throw new Error("Order not found");
}
// Check if order is cancelled (check if any status entry has isCancelled: true)
// const isOrderCancelled = order.orderStatus?.some(status => status.isCancelled) || false;
// if (!isOrderCancelled) {
// throw new Error("Order is not cancelled");
// }
// // Check if payment method is COD
// if (order.isCod) {
// throw new Error("Can't generate refund coupon for CoD Order");
// }
// Verify user exists
if (!order.user) {
throw new Error("User not found for this order");
}
@ -327,29 +431,23 @@ export const couponRouter = router({
const couponCode = `${userNamePrefix}${orderId}`;
// Check if coupon code already exists
const codeExists = await checkCouponExists(couponCode);
if (codeExists) {
const existingCoupon = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, couponCode),
});
if (existingCoupon) {
throw new Error("Coupon code already exists");
}
// Get order total amount
const orderAmount = parseFloat(order.totalAmount);
const coupon = await generateCancellationCoupon(
orderId,
staffUserId,
order.userId,
orderAmount,
couponCode
);
/*
// Old implementation - direct DB query with transaction:
const coupon = await db.transaction(async (tx) => {
// Calculate expiry date (30 days from now)
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 30);
// Create the coupon and update order status in a transaction
const coupon = await db.transaction(async (tx) => {
// Create the coupon
const result = await tx.insert(coupons).values({
couponCode,
@ -378,7 +476,6 @@ export const couponRouter = router({
return coupon;
});
*/
return coupon;
}),
@ -389,66 +486,80 @@ export const couponRouter = router({
limit: z.number().default(50),
search: z.string().optional(),
}))
.query(async ({ input }): Promise<{ coupons: any[]; nextCursor?: number }> => {
.query(async ({ input }) => {
const { cursor, limit, search } = input;
const { coupons: result, hasMore } = await getReservedCouponsFromDb(cursor, limit, search);
let whereCondition = undefined;
const conditions = [];
if (cursor) {
conditions.push(lt(reservedCoupons.id, cursor));
}
if (search && search.trim()) {
conditions.push(or(
like(reservedCoupons.secretCode, `%${search}%`),
like(reservedCoupons.couponCode, `%${search}%`)
));
}
if (conditions.length > 0) {
whereCondition = and(...conditions);
}
const result = await db.query.reservedCoupons.findMany({
where: whereCondition,
with: {
redeemedUser: true,
creator: true,
},
orderBy: (reservedCoupons, { desc }) => [desc(reservedCoupons.createdAt)],
limit: limit + 1, // Fetch one extra to check if there's more
});
const hasMore = result.length > limit;
const coupons = hasMore ? result.slice(0, limit) : result;
const nextCursor = hasMore ? result[result.length - 1].id : undefined;
return {
coupons: result,
coupons,
nextCursor,
};
}),
createReservedCoupon: protectedProcedure
.input(createCouponBodySchema)
.mutation(async ({ input, ctx }): Promise<any> => {
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableProducts, maxValue, validTill, maxLimitForUser, exclusiveApply } = input;
.mutation(async ({ input, ctx }) => {
const { couponCode, isUserBased, discountPercent, flatDiscount, minOrder, productIds, applicableUsers, applicableProducts, maxValue, isApplyForAll, validTill, maxLimitForUser, exclusiveApply } = input;
// Validation: ensure at least one discount type is provided
if ((!discountPercent && !flatDiscount) || (discountPercent && flatDiscount)) {
throw new Error("Either discountPercent or flatDiscount must be provided (but not both)");
}
// For reserved coupons, applicableUsers is not used, as it's redeemed by one user
// Get staff user ID from auth middleware
const staffUserId = ctx.staffUser?.id;
if (!staffUserId) {
throw new Error("Unauthorized");
}
// Generate secret code if not provided
// Generate secret code if not provided (use couponCode as base)
let secretCode = couponCode || `SECRET${Date.now().toString().slice(-6)}${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
// Using dbService helper (new implementation)
const codeExists = await checkReservedCouponExists(secretCode);
if (codeExists) {
// Check if secret code already exists
const existing = await db.query.reservedCoupons.findFirst({
where: eq(reservedCoupons.secretCode, secretCode),
});
if (existing) {
throw new Error("Secret code already exists");
}
const coupon = await createReservedCouponWithProducts(
{
secretCode,
couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`,
discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(),
productIds,
maxValue: maxValue?.toString(),
validTill: validTill ? dayjs(validTill).toDate() : undefined,
maxLimitForUser,
exclusiveApply: exclusiveApply || false,
createdBy: staffUserId,
},
applicableProducts
);
/*
// Old implementation - direct DB query:
const result = await db.insert(reservedCoupons).values({
secretCode,
couponCode: couponCode || RESERVED${Date.now().toString().slice(-6)},
couponCode: couponCode || `RESERVED${Date.now().toString().slice(-6)}`,
discountPercent: discountPercent?.toString(),
flatDiscount: flatDiscount?.toString(),
minOrder: minOrder?.toString(),
@ -471,7 +582,6 @@ export const couponRouter = router({
}))
);
}
*/
return coupon;
}),
@ -482,19 +592,43 @@ export const couponRouter = router({
limit: z.number().min(1).max(50).default(20),
offset: z.number().min(0).default(0),
}))
.query(async ({ input }): Promise<{ users: UserMiniInfo[] }> => {
const { search, limit, offset } = input;
.query(async ({ input }) => {
const { search, limit } = input;
const result = await getUsersForCouponFromDb(search, limit, offset);
let whereCondition = undefined;
if (search && search.trim()) {
whereCondition = or(
like(users.name, `%${search}%`),
like(users.mobile, `%${search}%`)
);
}
return result;
const userList = await db.query.users.findMany({
where: whereCondition,
columns: {
id: true,
name: true,
mobile: true,
},
limit: limit,
offset: input.offset,
orderBy: (users, { asc }) => [asc(users.name)],
});
return {
users: userList.map(user => ({
id: user.id,
name: user.name || 'Unknown',
mobile: user.mobile,
}))
};
}),
createCoupon: protectedProcedure
.input(z.object({
mobile: z.string().min(1, 'Mobile number is required'),
}))
.mutation(async ({ input, ctx }): Promise<{ success: boolean; coupon: any }> => {
.mutation(async ({ input, ctx }) => {
const { mobile } = input;
// Get staff user ID from auth middleware
@ -511,27 +645,13 @@ export const couponRouter = router({
throw new Error("Mobile number must be exactly 10 digits");
}
// Generate unique coupon code
const timestamp = Date.now().toString().slice(-6);
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
const couponCode = `MF${cleanMobile.slice(-4)}${timestamp}${random}`;
// Using dbService helper (new implementation)
const codeExists = await checkCouponExists(couponCode);
if (codeExists) {
throw new Error("Generated coupon code already exists - please try again");
}
const { coupon, user } = await createCouponForUser(cleanMobile, couponCode, staffUserId);
/*
// Old implementation - direct DB query with transaction:
// Check if user exists, create if not
let user = await db.query.users.findFirst({
where: eq(users.mobile, cleanMobile),
});
if (!user) {
// Create new user
const [newUser] = await db.insert(users).values({
name: null,
email: null,
@ -540,18 +660,32 @@ export const couponRouter = router({
user = newUser;
}
// Generate unique coupon code
const timestamp = Date.now().toString().slice(-6);
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
const couponCode = `MF${cleanMobile.slice(-4)}${timestamp}${random}`;
// Check if coupon code already exists (very unlikely but safe)
const existingCode = await db.query.coupons.findFirst({
where: eq(coupons.couponCode, couponCode),
});
if (existingCode) {
throw new Error("Generated coupon code already exists - please try again");
}
// Create the coupon
const [coupon] = await db.insert(coupons).values({
couponCode,
isUserBased: true,
discountPercent: "20",
minOrder: "1000",
maxValue: "500",
maxLimitForUser: 1,
discountPercent: "20", // 20% discount
minOrder: "1000", // ₹1000 minimum order
maxValue: "500", // ₹500 maximum discount
maxLimitForUser: 1, // One-time use
isApplyForAll: false,
exclusiveApply: false,
createdBy: staffUserId,
validTill: dayjs().add(90, 'days').toDate(),
validTill: dayjs().add(90, 'days').toDate(), // 90 days from now
}).returning();
// Associate coupon with user
@ -559,7 +693,6 @@ export const couponRouter = router({
couponId: coupon.id,
userId: user.id,
});
*/
return {
success: true,

View file

@ -1,5 +1,21 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { z } from "zod";
import { db } from "@/src/db/db_index"
import {
orders,
orderItems,
orderStatus,
users,
addresses,
refunds,
coupons,
couponUsage,
complaints,
payments,
} from "@/src/db/schema";
import { eq, and, gte, lt, desc, SQL, inArray } from "drizzle-orm";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { ApiError } from "@/src/lib/api-error"
import {
sendOrderPackagedNotification,
@ -7,38 +23,16 @@ import {
} from "@/src/lib/notif-job";
import { publishCancellation } from "@/src/lib/post-order-handler"
import { getMultipleUserNegativityScores } from "@/src/stores/user-negativity-store"
import {
updateOrderNotes as updateOrderNotesInDb,
getOrderDetails as getOrderDetailsInDb,
updateOrderPackaged as updateOrderPackagedInDb,
updateOrderDelivered as updateOrderDeliveredInDb,
updateOrderItemPackaging as updateOrderItemPackagingInDb,
removeDeliveryCharge as removeDeliveryChargeInDb,
getSlotOrders as getSlotOrdersInDb,
updateAddressCoords as updateAddressCoordsInDb,
getAllOrders as getAllOrdersInDb,
rebalanceSlots as rebalanceSlotsInDb,
cancelOrder as cancelOrderInDb,
deleteOrderById as deleteOrderByIdInDb,
} from '@/src/dbService'
import type {
AdminCancelOrderResult,
AdminGetAllOrdersResult,
AdminGetSlotOrdersResult,
AdminOrderBasicResult,
AdminOrderDetails,
AdminOrderItemPackagingResult,
AdminOrderMessageResult,
AdminOrderRow,
AdminOrderUpdateResult,
AdminRebalanceSlotsResult,
} from "@packages/shared"
const updateOrderNotesSchema = z.object({
orderId: z.number(),
adminNotes: z.string(),
});
const getFullOrderSchema = z.object({
orderId: z.number(),
});
const getOrderDetailsSchema = z.object({
orderId: z.number(),
});
@ -63,6 +57,10 @@ const getSlotOrdersSchema = z.object({
slotId: z.string(),
});
const getTodaysOrdersSchema = z.object({
slotId: z.string().optional(),
});
const getAllOrdersSchema = z.object({
cursor: z.number().optional(),
limit: z.number().default(20),
@ -88,13 +86,9 @@ const getAllOrdersSchema = z.object({
export const orderRouter = router({
updateNotes: protectedProcedure
.input(updateOrderNotesSchema)
.mutation(async ({ input }): Promise<AdminOrderRow> => {
.mutation(async ({ input }) => {
const { orderId, adminNotes } = input;
const result = await updateOrderNotesInDb(orderId, adminNotes || null)
/*
// Old implementation - direct DB query:
const result = await db
.update(orders)
.set({
@ -106,24 +100,125 @@ export const orderRouter = router({
if (result.length === 0) {
throw new Error("Order not found");
}
*/
if (!result) {
throw new Error("Order not found")
return result[0];
}),
getFullOrder: protectedProcedure
.input(getFullOrderSchema)
.query(async ({ input }) => {
const { orderId } = input;
const orderData = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
payment: true,
paymentInfo: true,
},
});
if (!orderData) {
throw new Error("Order not found");
}
return result as AdminOrderRow;
// Get order status separately
const statusRecord = await db.query.orderStatus.findFirst({
where: eq(orderStatus.orderId, orderId),
});
let status: "pending" | "delivered" | "cancelled" = "pending";
if (statusRecord?.isCancelled) {
status = "cancelled";
} else if (statusRecord?.isDelivered) {
status = "delivered";
}
// Get refund details if order is cancelled
let refund = null;
if (status === "cancelled") {
refund = await db.query.refunds.findFirst({
where: eq(refunds.orderId, orderId),
});
}
return {
id: orderData.id,
readableId: orderData.id,
customerName: `${orderData.user.name}`,
customerEmail: orderData.user.email,
customerMobile: orderData.user.mobile,
address: {
line1: orderData.address.addressLine1,
line2: orderData.address.addressLine2,
city: orderData.address.city,
state: orderData.address.state,
pincode: orderData.address.pincode,
phone: orderData.address.phone,
},
slotInfo: orderData.slot
? {
time: orderData.slot.deliveryTime.toISOString(),
sequence: orderData.slot.deliverySequence,
}
: null,
isCod: orderData.isCod,
isOnlinePayment: orderData.isOnlinePayment,
totalAmount: orderData.totalAmount,
adminNotes: orderData.adminNotes,
userNotes: orderData.userNotes,
createdAt: orderData.createdAt,
status,
isPackaged:
orderData.orderItems.every((item) => item.is_packaged) || false,
isDelivered: statusRecord?.isDelivered || false,
items: orderData.orderItems.map((item) => ({
id: item.id,
name: item.product.name,
quantity: item.quantity,
price: item.price,
unit: item.product.unit?.shortNotation,
amount:
parseFloat(item.price.toString()) *
parseFloat(item.quantity || "0"),
})),
payment: orderData.payment
? {
status: orderData.payment.status,
gateway: orderData.payment.gateway,
merchantOrderId: orderData.payment.merchantOrderId,
}
: null,
paymentInfo: orderData.paymentInfo
? {
status: orderData.paymentInfo.status,
gateway: orderData.paymentInfo.gateway,
merchantOrderId: orderData.paymentInfo.merchantOrderId,
}
: null,
// Cancellation details (only present for cancelled orders)
cancelReason: statusRecord?.cancelReason || null,
cancellationReviewed: statusRecord?.cancellationReviewed || false,
isRefundDone: refund?.refundStatus === "processed" || false,
};
}),
getOrderDetails: protectedProcedure
.input(getOrderDetailsSchema)
.query(async ({ input }): Promise<AdminOrderDetails> => {
.query(async ({ input }) => {
const { orderId } = input;
const orderDetails = await getOrderDetailsInDb(orderId)
/*
// Old implementation - direct DB queries:
// Single optimized query with all relations
const orderData = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
@ -142,8 +237,8 @@ export const orderRouter = router({
},
payment: true,
paymentInfo: true,
orderStatus: true,
refunds: true,
orderStatus: true, // Include in main query
refunds: true, // Include in main query
},
});
@ -153,7 +248,7 @@ export const orderRouter = router({
// Get coupon usage for this specific order using new orderId field
const couponUsageData = await db.query.couponUsage.findMany({
where: eq(couponUsage.orderId, orderData.id),
where: eq(couponUsage.orderId, orderData.id), // Use new orderId field
with: {
coupon: true,
},
@ -285,24 +380,13 @@ export const orderRouter = router({
refundRecord: refund,
isFlashDelivery: orderData.isFlashDelivery,
};
*/
if (!orderDetails) {
throw new Error('Order not found')
}
return orderDetails
}),
updatePackaged: protectedProcedure
.input(updatePackagedSchema)
.mutation(async ({ input }): Promise<AdminOrderUpdateResult> => {
.mutation(async ({ input }) => {
const { orderId, isPackaged } = input;
const result = await updateOrderPackagedInDb(orderId, isPackaged)
/*
// Old implementation - direct DB queries:
// Update all order items to the specified packaged state
await db
.update(orderItems)
@ -328,22 +412,13 @@ export const orderRouter = router({
if (order) await sendOrderPackagedNotification(order.userId, orderId);
return { success: true };
*/
if (result.userId) await sendOrderPackagedNotification(result.userId, orderId)
return { success: true, userId: result.userId }
}),
updateDelivered: protectedProcedure
.input(updateDeliveredSchema)
.mutation(async ({ input }): Promise<AdminOrderUpdateResult> => {
.mutation(async ({ input }) => {
const { orderId, isDelivered } = input;
const result = await updateOrderDeliveredInDb(orderId, isDelivered)
/*
// Old implementation - direct DB queries:
await db
.update(orderStatus)
.set({ isDelivered })
@ -355,22 +430,13 @@ export const orderRouter = router({
if (order) await sendOrderDeliveredNotification(order.userId, orderId);
return { success: true };
*/
if (result.userId) await sendOrderDeliveredNotification(result.userId, orderId)
return { success: true, userId: result.userId }
}),
updateOrderItemPackaging: protectedProcedure
.input(updateOrderItemPackagingSchema)
.mutation(async ({ input }): Promise<AdminOrderItemPackagingResult> => {
.mutation(async ({ input }) => {
const { orderItemId, isPackaged, isPackageVerified } = input;
const result = await updateOrderItemPackagingInDb(orderItemId, isPackaged, isPackageVerified)
/*
// Old implementation - direct DB queries:
// Validate that orderItem exists
const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId),
@ -396,24 +462,13 @@ export const orderRouter = router({
.where(eq(orderItems.id, orderItemId));
return { success: true };
*/
if (!result.updated) {
throw new ApiError('Order item not found', 404)
}
return result
}),
removeDeliveryCharge: protectedProcedure
.input(z.object({ orderId: z.number() }))
.mutation(async ({ input }): Promise<AdminOrderMessageResult> => {
.mutation(async ({ input }) => {
const { orderId } = input;
const result = await removeDeliveryChargeInDb(orderId)
/*
// Old implementation - direct DB queries:
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
});
@ -435,24 +490,13 @@ export const orderRouter = router({
.where(eq(orders.id, orderId));
return { success: true, message: 'Delivery charge removed' };
*/
if (!result) {
throw new Error('Order not found')
}
return result
}),
getSlotOrders: protectedProcedure
.input(getSlotOrdersSchema)
.query(async ({ input }): Promise<AdminGetSlotOrdersResult> => {
.query(async ({ input }) => {
const { slotId } = input;
const result = await getSlotOrdersInDb(slotId)
/*
// Old implementation - direct DB queries:
const slotOrders = await db.query.orders.findMany({
where: eq(orders.slotId, parseInt(slotId)),
with: {
@ -529,9 +573,97 @@ export const orderRouter = router({
});
return { success: true, data: formattedOrders };
*/
}),
return result
getTodaysOrders: protectedProcedure
.input(getTodaysOrdersSchema)
.query(async ({ input }) => {
const { slotId } = input;
const start = dayjs().startOf("day").toDate();
const end = dayjs().endOf("day").toDate();
let whereCondition = and(
gte(orders.createdAt, start),
lt(orders.createdAt, end)
);
if (slotId) {
whereCondition = and(
whereCondition,
eq(orders.slotId, parseInt(slotId))
);
}
const todaysOrders = await db.query.orders.findMany({
where: whereCondition,
with: {
user: true,
address: true,
slot: true,
orderItems: {
with: {
product: {
with: {
unit: true,
},
},
},
},
orderStatus: true,
},
});
const filteredOrders = todaysOrders.filter((order) => {
const statusRecord = order.orderStatus[0];
return (
order.isCod ||
(statusRecord && statusRecord.paymentStatus === "success")
);
});
const formattedOrders = filteredOrders.map((order) => {
const statusRecord = order.orderStatus[0]; // assuming one status per order
let status: "pending" | "delivered" | "cancelled" = "pending";
if (statusRecord?.isCancelled) {
status = "cancelled";
} else if (statusRecord?.isDelivered) {
status = "delivered";
}
const items = order.orderItems.map((item) => ({
name: item.product.name,
quantity: parseFloat(item.quantity),
price: parseFloat(item.price.toString()),
amount: parseFloat(item.quantity) * parseFloat(item.price.toString()),
unit: item.product.unit?.shortNotation || "",
}));
return {
orderId: order.id.toString(),
readableId: order.id,
customerName: order.user.name,
address: `${order.address.addressLine1}${
order.address.addressLine2 ? `, ${order.address.addressLine2}` : ""
}, ${order.address.city}, ${order.address.state} - ${
order.address.pincode
}`,
totalAmount: parseFloat(order.totalAmount),
items,
deliveryTime: order.slot?.deliveryTime.toISOString() || null,
status,
isPackaged:
order.orderItems.every((item) => item.is_packaged) || false,
isDelivered: statusRecord?.isDelivered || false,
isCod: order.isCod,
paymentMode: order.isCod ? "COD" : "Online",
paymentStatus: statusRecord?.paymentStatus || "pending",
slotId: order.slotId,
adminNotes: order.adminNotes,
userNotes: order.userNotes,
};
});
return { success: true, data: formattedOrders };
}),
updateAddressCoords: protectedProcedure
@ -542,13 +674,9 @@ export const orderRouter = router({
longitude: z.number(),
})
)
.mutation(async ({ input }): Promise<AdminOrderBasicResult> => {
.mutation(async ({ input }) => {
const { addressId, latitude, longitude } = input;
const result = await updateAddressCoordsInDb(addressId, latitude, longitude)
/*
// Old implementation - direct DB queries:
const result = await db
.update(addresses)
.set({
@ -563,33 +691,12 @@ export const orderRouter = router({
}
return { success: true };
*/
if (!result.success) {
throw new ApiError('Address not found', 404)
}
return result
}),
getAll: protectedProcedure
.input(getAllOrdersSchema)
.query(async ({ input }): Promise<AdminGetAllOrdersResult | undefined> => {
.query(async ({ input }) => {
try {
const result = await getAllOrdersInDb(input)
const userIds = [...new Set(result.orders.map((order) => order.userId))]
const negativityScores = await getMultipleUserNegativityScores(userIds)
const orders = result.orders.map((order) => {
const { userId, userNegativityScore, ...rest } = order
return {
...rest,
userNegativityScore: negativityScores[userId] || 0,
}
})
/*
// Old implementation - direct DB queries:
const {
cursor,
limit,
@ -751,12 +858,6 @@ export const orderRouter = router({
? ordersToReturn[ordersToReturn.length - 1].id
: undefined,
};
*/
return {
orders,
nextCursor: result.nextCursor,
}
} catch (e) {
console.log({ e });
}
@ -764,13 +865,9 @@ export const orderRouter = router({
rebalanceSlots: protectedProcedure
.input(z.object({ slotIds: z.array(z.number()).min(1).max(50) }))
.mutation(async ({ input }): Promise<AdminRebalanceSlotsResult> => {
.mutation(async ({ input }) => {
const slotIds = input.slotIds;
const result = await rebalanceSlotsInDb(slotIds)
/*
// Old implementation - direct DB queries:
const ordersList = await db.query.orders.findMany({
where: inArray(orders.slotId, slotIds),
with: {
@ -839,9 +936,6 @@ export const orderRouter = router({
});
return { success: true, updatedOrders: updatedOrderIds, message: `Rebalanced ${updatedOrderIds.length} orders.` };
*/
return result
}),
cancelOrder: protectedProcedure
@ -849,13 +943,9 @@ export const orderRouter = router({
orderId: z.number(),
reason: z.string().min(1, "Cancellation reason is required"),
}))
.mutation(async ({ input }): Promise<AdminCancelOrderResult> => {
.mutation(async ({ input }) => {
const { orderId, reason } = input;
const result = await cancelOrderInDb(orderId, reason)
/*
// Old implementation - direct DB queries:
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
@ -907,40 +997,14 @@ export const orderRouter = router({
await publishCancellation(result.orderId, 'admin', reason);
return { success: true, message: "Order cancelled successfully" };
*/
if (!result.success) {
if (result.error === 'order_not_found') {
throw new ApiError(result.message, 404)
}
if (result.error === 'status_not_found') {
throw new ApiError(result.message, 400)
}
if (result.error === 'already_cancelled') {
throw new ApiError(result.message, 400)
}
if (result.error === 'already_delivered') {
throw new ApiError(result.message, 400)
}
throw new ApiError(result.message, 400)
}
if (result.orderId) {
await publishCancellation(result.orderId, 'admin', reason)
}
return { success: true, message: result.message }
}),
});
// {"id": "order_Rhh00qJNdjUp8o", "notes": {"retry": "true", "customerOrderId": "14"}, "amount": 21000, "entity": "order", "status": "created", "receipt": "order_14_retry", "attempts": 0, "currency": "INR", "offer_id": null, "signature": "6df20655021f1d6841340f2a2ef2ef9378cb3d43495ab09e85f08aea1a851583", "amount_due": 21000, "created_at": 1763575791, "payment_id": "pay_Rhh15cLL28YM7j", "amount_paid": 0}
export async function deleteOrderById(orderId: number): Promise<void> {
await deleteOrderByIdInDb(orderId)
type RefundStatus = "success" | "pending" | "failed" | "none" | "na";
/*
// Old implementation - direct DB queries:
export async function deleteOrderById(orderId: number): Promise<void> {
await db.transaction(async (tx) => {
await tx.delete(orderItems).where(eq(orderItems.orderId, orderId));
await tx.delete(orderStatus).where(eq(orderStatus.orderId, orderId));
@ -950,5 +1014,5 @@ export async function deleteOrderById(orderId: number): Promise<void> {
await tx.delete(complaints).where(eq(complaints.orderId, orderId));
await tx.delete(orders).where(eq(orders.id, orderId));
});
*/
}

View file

@ -1,5 +1,15 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { z } from "zod";
import { db } from "@/src/db/db_index"
import {
orders,
orderStatus,
payments,
refunds,
} from "@/src/db/schema";
import { and, eq } from "drizzle-orm";
import { ApiError } from "@/src/lib/api-error"
import { RazorpayPaymentService } from "@/src/lib/payments-utils"
const initiateRefundSchema = z
.object({
@ -23,6 +33,114 @@ export const adminPaymentsRouter = router({
initiateRefund: protectedProcedure
.input(initiateRefundSchema)
.mutation(async ({ input }) => {
return {}
try {
const { orderId, refundPercent, refundAmount } = input;
// Validate order exists
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
});
if (!order) {
throw new ApiError("Order not found", 404);
}
// Check if order is paid
const orderStatusRecord = await db.query.orderStatus.findFirst({
where: eq(orderStatus.orderId, orderId),
});
if(order.isCod) {
throw new ApiError("Order is a Cash On Delivery. Not eligible for refund")
}
if (
!orderStatusRecord ||
(orderStatusRecord.paymentStatus !== "success" &&
!(order.isCod && orderStatusRecord.isDelivered))
) {
throw new ApiError("Order payment not verified or not eligible for refund", 400);
}
// Calculate refund amount
let calculatedRefundAmount: number;
if (refundPercent !== undefined) {
calculatedRefundAmount =
(parseFloat(order.totalAmount) * refundPercent) / 100;
} else if (refundAmount !== undefined) {
calculatedRefundAmount = refundAmount;
if (calculatedRefundAmount > parseFloat(order.totalAmount)) {
throw new ApiError("Refund amount cannot exceed order total", 400);
}
} else {
throw new ApiError("Invalid refund parameters", 400);
}
let razorpayRefund = null;
let merchantRefundId = null;
// Get payment record for online payments
const payment = await db.query.payments.findFirst({
where: and(
eq(payments.orderId, orderId),
eq(payments.status, "success")
),
});
if (!payment || payment.status !== "success") {
throw new ApiError("Payment not found or not successful", 404);
}
const payload = payment.payload as any;
// Initiate Razorpay refund
razorpayRefund = await RazorpayPaymentService.initiateRefund(
payload.payment_id,
Math.round(calculatedRefundAmount * 100) // Convert to paisa
);
merchantRefundId = razorpayRefund.id;
// Check if refund already exists for this order
const existingRefund = await db.query.refunds.findFirst({
where: eq(refunds.orderId, orderId),
});
const refundStatus = "initiated";
if (existingRefund) {
// Update existing refund
await db
.update(refunds)
.set({
refundAmount: calculatedRefundAmount.toString(),
refundStatus,
merchantRefundId,
refundProcessedAt: order.isCod ? new Date() : null,
})
.where(eq(refunds.id, existingRefund.id));
} else {
// Insert new refund
await db
.insert(refunds)
.values({
orderId,
refundAmount: calculatedRefundAmount.toString(),
refundStatus,
merchantRefundId,
});
}
return {
refundId: merchantRefundId || `cod_${orderId}`,
amount: calculatedRefundAmount,
status: refundStatus,
message: order.isCod ? "COD refund processed successfully" : "Refund initiated successfully",
};
}
catch(e) {
console.log(e);
throw new ApiError("Failed to initiate refund")
}
}),
});

View file

@ -1,47 +1,23 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, users, productGroupInfo, productGroupMembership } from '@/src/db/schema'
import { eq, and, inArray, desc, sql } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error'
import { generateSignedUrlsFromS3Urls, claimUploadUrl } from '@/src/lib/s3-client'
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import {
getAllProducts as getAllProductsInDb,
getProductById as getProductByIdInDb,
deleteProduct as deleteProductInDb,
toggleProductOutOfStock as toggleProductOutOfStockInDb,
updateSlotProducts as updateSlotProductsInDb,
getSlotProductIds as getSlotProductIdsInDb,
getSlotsProductIds as getSlotsProductIdsInDb,
getProductReviews as getProductReviewsInDb,
respondToReview as respondToReviewInDb,
getAllProductGroups as getAllProductGroupsInDb,
createProductGroup as createProductGroupInDb,
updateProductGroup as updateProductGroupInDb,
deleteProductGroup as deleteProductGroupInDb,
updateProductPrices as updateProductPricesInDb,
} from '@/src/dbService'
import type {
AdminProductGroupsResult,
AdminProductGroupResponse,
AdminProductReviewsResult,
AdminProductReviewResponse,
AdminProductListResponse,
AdminProductResponse,
AdminDeleteProductResult,
AdminToggleOutOfStockResult,
AdminUpdateSlotProductsResult,
AdminSlotProductIdsResult,
AdminSlotsProductIdsResult,
AdminUpdateProductPricesResult,
} from '@packages/shared'
import { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUrl, claimUploadUrl } from '@/src/lib/s3-client'
import { deleteS3Image } from '@/src/lib/delete-image'
import type { SpecialDeal } from '@/src/db/types'
import { initializeAllStores } from '@/src/stores/store-initializer'
type CreateDeal = {
quantity: number;
price: number;
validTill: string;
};
export const productRouter = router({
getProducts: protectedProcedure
.query(async (): Promise<AdminProductListResponse> => {
const products = await getAllProductsInDb()
/*
// Old implementation - direct DB query:
.query(async ({ ctx }) => {
const products = await db.query.productInfo.findMany({
orderBy: productInfo.name,
with: {
@ -49,32 +25,28 @@ export const productRouter = router({
store: true,
},
});
*/
// Generate signed URLs for all product images
const productsWithSignedUrls = await Promise.all(
products.map(async (product) => ({
...product,
images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []),
}))
)
);
return {
products: productsWithSignedUrls,
count: productsWithSignedUrls.length,
}
};
}),
getProductById: protectedProcedure
.input(z.object({
id: z.number(),
}))
.query(async ({ input }): Promise<AdminProductResponse> => {
.query(async ({ input, ctx }) => {
const { id } = input;
const product = await getProductByIdInDb(id)
/*
// Old implementation - direct DB queries:
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
with: {
@ -111,33 +83,15 @@ export const productRouter = router({
return {
product: productWithSignedUrls,
};
*/
if (!product) {
throw new ApiError('Product not found', 404)
}
const productWithSignedUrls = {
...product,
images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []),
}
return {
product: productWithSignedUrls,
}
}),
deleteProduct: protectedProcedure
.input(z.object({
id: z.number(),
}))
.mutation(async ({ input }): Promise<AdminDeleteProductResult> => {
.mutation(async ({ input, ctx }) => {
const { id } = input;
const deletedProduct = await deleteProductInDb(id)
/*
// Old implementation - direct DB query:
const [deletedProduct] = await db
.delete(productInfo)
.where(eq(productInfo.id, id))
@ -146,31 +100,22 @@ export const productRouter = router({
if (!deletedProduct) {
throw new ApiError("Product not found", 404);
}
*/
if (!deletedProduct) {
throw new ApiError('Product not found', 404)
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return {
message: 'Product deleted successfully',
}
message: "Product deleted successfully",
};
}),
toggleOutOfStock: protectedProcedure
.input(z.object({
id: z.number(),
}))
.mutation(async ({ input }): Promise<AdminToggleOutOfStockResult> => {
.mutation(async ({ input, ctx }) => {
const { id } = input;
const updatedProduct = await toggleProductOutOfStockInDb(id)
/*
// Old implementation - direct DB queries:
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
});
@ -186,18 +131,14 @@ export const productRouter = router({
})
.where(eq(productInfo.id, id))
.returning();
*/
if (!updatedProduct) {
throw new ApiError('Product not found', 404)
}
scheduleStoreInitialization()
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
product: updatedProduct,
message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`,
}
};
}),
updateSlotProducts: protectedProcedure
@ -205,17 +146,13 @@ export const productRouter = router({
slotId: z.string(),
productIds: z.array(z.string()),
}))
.mutation(async ({ input }): Promise<AdminUpdateSlotProductsResult> => {
.mutation(async ({ input, ctx }) => {
const { slotId, productIds } = input;
if (!Array.isArray(productIds)) {
throw new ApiError("productIds must be an array", 400);
}
const result = await updateSlotProductsInDb(slotId, productIds)
/*
// Old implementation - direct DB queries:
// Get current associations
const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
@ -252,35 +189,22 @@ export const productRouter = router({
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return {
message: "Slot products updated successfully",
added: productsToAdd.length,
removed: productsToRemove.length,
};
*/
scheduleStoreInitialization()
return {
message: 'Slot products updated successfully',
added: result.added,
removed: result.removed,
}
}),
getSlotProductIds: protectedProcedure
.input(z.object({
slotId: z.string(),
}))
.query(async ({ input }): Promise<AdminSlotProductIdsResult> => {
.query(async ({ input, ctx }) => {
const { slotId } = input;
const productIds = await getSlotProductIdsInDb(slotId)
/*
// Old implementation - direct DB queries:
const associations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
@ -293,28 +217,19 @@ export const productRouter = router({
return {
productIds,
};
*/
return {
productIds,
}
}),
getSlotsProductIds: protectedProcedure
.input(z.object({
slotIds: z.array(z.number()),
}))
.query(async ({ input }): Promise<AdminSlotsProductIdsResult> => {
.query(async ({ input, ctx }) => {
const { slotIds } = input;
if (!Array.isArray(slotIds)) {
throw new ApiError("slotIds must be an array", 400);
}
const result = await getSlotsProductIdsInDb(slotIds)
/*
// Old implementation - direct DB queries:
if (slotIds.length === 0) {
return {};
}
@ -345,9 +260,6 @@ export const productRouter = router({
});
return result;
*/
return result
}),
getProductReviews: protectedProcedure
@ -356,13 +268,9 @@ export const productRouter = router({
limit: z.number().int().min(1).max(50).optional().default(10),
offset: z.number().int().min(0).optional().default(0),
}))
.query(async ({ input }): Promise<AdminProductReviewsResult> => {
.query(async ({ input }) => {
const { productId, limit, offset } = input;
const { reviews, totalCount } = await getProductReviewsInDb(productId, limit, offset)
/*
// Old implementation - direct DB queries:
const reviews = await db
.select({
id: productReviews.id,
@ -400,19 +308,6 @@ export const productRouter = router({
const hasMore = offset + limit < totalCount;
return { reviews: reviewsWithSignedUrls, hasMore };
*/
const reviewsWithSignedUrls = await Promise.all(
reviews.map(async (review) => ({
...review,
signedImageUrls: await generateSignedUrlsFromS3Urls((review.imageUrls as string[]) || []),
signedAdminImageUrls: await generateSignedUrlsFromS3Urls((review.adminResponseImages as string[]) || []),
}))
)
const hasMore = offset + limit < totalCount
return { reviews: reviewsWithSignedUrls, hasMore }
}),
respondToReview: protectedProcedure
@ -422,13 +317,9 @@ export const productRouter = router({
adminResponseImages: z.array(z.string()).optional().default([]),
uploadUrls: z.array(z.string()).optional().default([]),
}))
.mutation(async ({ input }): Promise<AdminProductReviewResponse> => {
.mutation(async ({ input }) => {
const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input;
const updatedReview = await respondToReviewInDb(reviewId, adminResponse, adminResponseImages)
/*
// Old implementation - direct DB queries:
const [updatedReview] = await db
.update(productReviews)
.set({
@ -449,25 +340,10 @@ export const productRouter = router({
}
return { success: true, review: updatedReview };
*/
if (!updatedReview) {
throw new ApiError('Review not found', 404)
}
if (uploadUrls && uploadUrls.length > 0) {
await Promise.all(uploadUrls.map(url => claimUploadUrl(url)))
}
return { success: true, review: updatedReview }
}),
getGroups: protectedProcedure
.query(async (): Promise<AdminProductGroupsResult> => {
const groups = await getAllProductGroupsInDb()
/*
// Old implementation - direct DB queries:
.query(async ({ ctx }) => {
const groups = await db.query.productGroupInfo.findMany({
with: {
memberships: {
@ -478,18 +354,14 @@ export const productRouter = router({
},
orderBy: desc(productGroupInfo.createdAt),
});
*/
return {
groups: groups.map(group => ({
...group,
products: group.memberships.map(m => ({
...m.product,
images: (m.product.images as string[]) || null,
})),
products: group.memberships.map(m => m.product),
productCount: group.memberships.length,
})),
}
};
}),
createGroup: protectedProcedure
@ -498,13 +370,9 @@ export const productRouter = router({
description: z.string().optional(),
product_ids: z.array(z.number()).default([]),
}))
.mutation(async ({ input }): Promise<AdminProductGroupResponse> => {
.mutation(async ({ input, ctx }) => {
const { group_name, description, product_ids } = input;
const newGroup = await createProductGroupInDb(group_name, description, product_ids)
/*
// Old implementation - direct DB queries:
const [newGroup] = await db
.insert(productGroupInfo)
.values({
@ -523,20 +391,12 @@ export const productRouter = router({
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return {
group: newGroup,
message: 'Group created successfully',
};
*/
scheduleStoreInitialization()
return {
group: newGroup,
message: 'Group created successfully',
}
}),
updateGroup: protectedProcedure
@ -546,13 +406,9 @@ export const productRouter = router({
description: z.string().optional(),
product_ids: z.array(z.number()).optional(),
}))
.mutation(async ({ input }): Promise<AdminProductGroupResponse> => {
.mutation(async ({ input, ctx }) => {
const { id, group_name, description, product_ids } = input;
const updatedGroup = await updateProductGroupInDb(id, group_name, description, product_ids)
/*
// Old implementation - direct DB queries:
const updateData: any = {};
if (group_name !== undefined) updateData.groupName = group_name;
if (description !== undefined) updateData.description = description;
@ -583,37 +439,21 @@ export const productRouter = router({
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return {
group: updatedGroup,
message: 'Group updated successfully',
};
*/
if (!updatedGroup) {
throw new ApiError('Group not found', 404)
}
scheduleStoreInitialization()
return {
group: updatedGroup,
message: 'Group updated successfully',
}
}),
deleteGroup: protectedProcedure
.input(z.object({
id: z.number(),
}))
.mutation(async ({ input }): Promise<AdminDeleteProductResult> => {
.mutation(async ({ input, ctx }) => {
const { id } = input;
const deletedGroup = await deleteProductGroupInDb(id)
/*
// Old implementation - direct DB queries:
// Delete memberships first
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id));
@ -628,22 +468,11 @@ export const productRouter = router({
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return {
message: 'Group deleted successfully',
};
*/
if (!deletedGroup) {
throw new ApiError('Group not found', 404)
}
scheduleStoreInitialization()
return {
message: 'Group deleted successfully',
}
}),
updateProductPrices: protectedProcedure
@ -656,17 +485,9 @@ export const productRouter = router({
isFlashAvailable: z.boolean().optional(),
})),
}))
.mutation(async ({ input }): Promise<AdminUpdateProductPricesResult> => {
.mutation(async ({ input, ctx }) => {
const { updates } = input;
if (updates.length === 0) {
throw new ApiError('No updates provided', 400)
}
const result = await updateProductPricesInDb(updates)
/*
// Old implementation - direct DB queries:
if (updates.length === 0) {
throw new ApiError('No updates provided', 400);
}
@ -703,23 +524,11 @@ export const productRouter = router({
await Promise.all(updatePromises);
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return {
message: `Updated prices for ${updates.length} product(s)`,
updatedCount: updates.length,
};
*/
if (result.invalidIds.length > 0) {
throw new ApiError(`Invalid product IDs: ${result.invalidIds.join(', ')}`, 400)
}
scheduleStoreInitialization()
return {
message: `Updated prices for ${result.updatedCount} product(s)`,
updatedCount: result.updatedCount,
}
}),
});

View file

@ -1,39 +1,14 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { db } from "@/src/db/db_index"
import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "@/src/db/schema"
import { eq, inArray, and, desc } from "drizzle-orm";
import { ApiError } from "@/src/lib/api-error"
import { appUrl } from "@/src/lib/env-exporter"
import redisClient from "@/src/lib/redis-client"
import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import {
getActiveSlotsWithProducts as getActiveSlotsWithProductsInDb,
getActiveSlots as getActiveSlotsInDb,
getSlotsAfterDate as getSlotsAfterDateInDb,
getSlotByIdWithRelations as getSlotByIdWithRelationsInDb,
createSlotWithRelations as createSlotWithRelationsInDb,
updateSlotWithRelations as updateSlotWithRelationsInDb,
deleteSlotById as deleteSlotByIdInDb,
updateSlotCapacity as updateSlotCapacityInDb,
getSlotDeliverySequence as getSlotDeliverySequenceInDb,
updateSlotDeliverySequence as updateSlotDeliverySequenceInDb,
updateSlotProducts as updateSlotProductsInDb,
getSlotsProductIds as getSlotsProductIdsInDb,
} from '@/src/dbService'
import type {
AdminDeliverySequenceResult,
AdminSlotResult,
AdminSlotsResult,
AdminSlotsListResult,
AdminSlotCreateResult,
AdminSlotUpdateResult,
AdminSlotDeleteResult,
AdminUpdateDeliverySequenceResult,
AdminUpdateSlotCapacityResult,
AdminSlotsProductIdsResult,
AdminUpdateSlotProductsResult,
} from '@packages/shared'
import { initializeAllStores } from '@/src/stores/store-initializer'
interface CachedDeliverySequence {
[userId: string]: number[];
@ -88,15 +63,11 @@ const updateDeliverySequenceSchema = z.object({
export const slotsRouter = router({
// Exact replica of GET /av/slots
getAll: protectedProcedure.query(async ({ ctx }): Promise<AdminSlotsResult> => {
getAll: protectedProcedure.query(async ({ ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const slots = await getActiveSlotsWithProductsInDb()
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo
.findMany({
where: eq(deliverySlotInfo.isActive, true),
@ -122,18 +93,17 @@ export const slotsRouter = router({
products: slot.productSlots.map((ps) => ps.product),
}))
);
*/
return {
slots,
count: slots.length,
}
};
}),
// Exact replica of POST /av/products/slots/product-ids
getSlotsProductIds: protectedProcedure
.input(z.object({ slotIds: z.array(z.number()) }))
.query(async ({ input, ctx }): Promise<AdminSlotsProductIdsResult> => {
.query(async ({ input, ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
@ -147,10 +117,6 @@ export const slotsRouter = router({
});
}
const result = await getSlotsProductIdsInDb(slotIds)
/*
// Old implementation - direct DB queries:
if (slotIds.length === 0) {
return {};
}
@ -181,9 +147,6 @@ export const slotsRouter = router({
});
return result;
*/
return result
}),
// Exact replica of PUT /av/products/slots/:slotId/products
@ -194,7 +157,7 @@ export const slotsRouter = router({
productIds: z.array(z.number()),
})
)
.mutation(async ({ input, ctx }): Promise<AdminUpdateSlotProductsResult> => {
.mutation(async ({ input, ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
@ -208,10 +171,6 @@ export const slotsRouter = router({
});
}
const result = await updateSlotProductsInDb(String(slotId), productIds.map(String))
/*
// Old implementation - direct DB queries:
// Get current associations
const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, slotId),
@ -256,27 +215,18 @@ export const slotsRouter = router({
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return {
message: "Slot products updated successfully",
added: productsToAdd.length,
removed: productsToRemove.length,
};
*/
scheduleStoreInitialization()
return {
message: result.message,
added: result.added,
removed: result.removed,
}
}),
createSlot: protectedProcedure
.input(createSlotSchema)
.mutation(async ({ input, ctx }): Promise<AdminSlotCreateResult> => {
.mutation(async ({ input, ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
@ -288,17 +238,6 @@ export const slotsRouter = router({
throw new ApiError("Delivery time and orders close time are required", 400);
}
const result = await createSlotWithRelationsInDb({
deliveryTime,
freezeTime,
isActive,
productIds,
vendorSnippets: snippets,
groupIds,
})
/*
// Old implementation - direct DB queries:
const result = await db.transaction(async (tx) => {
// Create slot
const [newSlot] = await tx
@ -357,47 +296,37 @@ export const slotsRouter = router({
message: "Slot created successfully",
};
});
*/
// Reinitialize stores to reflect changes (outside transaction)
scheduleStoreInitialization()
await initializeAllStores();
return result
return result;
}),
getSlots: protectedProcedure.query(async ({ ctx }): Promise<AdminSlotsListResult> => {
getSlots: protectedProcedure.query(async ({ ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const slots = await getActiveSlotsInDb()
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
});
*/
return {
slots,
count: slots.length,
}
};
}),
getSlotById: protectedProcedure
.input(getSlotByIdSchema)
.query(async ({ input, ctx }): Promise<AdminSlotResult> => {
.query(async ({ input, ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const { id } = input;
const slot = await getSlotByIdWithRelationsInDb(id)
/*
// Old implementation - direct DB queries:
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id),
with: {
@ -415,26 +344,28 @@ export const slotsRouter = router({
vendorSnippets: true,
},
});
*/
if (!slot) {
throw new ApiError('Slot not found', 404)
throw new ApiError("Slot not found", 404);
}
return {
slot: {
...slot,
vendorSnippets: slot.vendorSnippets.map(snippet => ({
deliverySequence: slot.deliverySequence as number[],
groupIds: slot.groupIds as number[],
products: slot.productSlots.map((ps) => ps.product),
vendorSnippets: slot.vendorSnippets?.map(snippet => ({
...snippet,
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`,
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`
})),
},
}
};
}),
updateSlot: protectedProcedure
.input(updateSlotSchema)
.mutation(async ({ input, ctx }): Promise<AdminSlotUpdateResult> => {
.mutation(async ({ input, ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
@ -445,18 +376,6 @@ export const slotsRouter = router({
throw new ApiError("Delivery time and orders close time are required", 400);
}
const result = await updateSlotWithRelationsInDb({
id,
deliveryTime,
freezeTime,
isActive,
productIds,
vendorSnippets: snippets,
groupIds,
})
/*
// Old implementation - direct DB queries:
// Filter groupIds to only include valid (existing) groups
let validGroupIds = groupIds;
if (groupIds && groupIds.length > 0) {
@ -536,16 +455,11 @@ export const slotsRouter = router({
message: "Slot updated successfully",
};
});
*/
if (!result) {
throw new ApiError('Slot not found', 404)
}
// Reinitialize stores to reflect changes (outside transaction)
scheduleStoreInitialization()
await initializeAllStores();
return result
return result;
}
catch(e) {
console.log(e)
@ -555,17 +469,13 @@ export const slotsRouter = router({
deleteSlot: protectedProcedure
.input(deleteSlotSchema)
.mutation(async ({ input, ctx }): Promise<AdminSlotDeleteResult> => {
.mutation(async ({ input, ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const { id } = input;
const deletedSlot = await deleteSlotByIdInDb(id)
/*
// Old implementation - direct DB queries:
const [deletedSlot] = await db
.update(deliverySlotInfo)
.set({ isActive: false })
@ -575,23 +485,18 @@ export const slotsRouter = router({
if (!deletedSlot) {
throw new ApiError("Slot not found", 404);
}
*/
if (!deletedSlot) {
throw new ApiError('Slot not found', 404)
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return {
message: 'Slot deleted successfully',
}
message: "Slot deleted successfully",
};
}),
getDeliverySequence: protectedProcedure
.input(getDeliverySequenceSchema)
.query(async ({ input, ctx }): Promise<AdminDeliverySequenceResult> => {
.query(async ({ input, ctx }) => {
const { id } = input;
const slotId = parseInt(id);
@ -601,7 +506,7 @@ export const slotsRouter = router({
const cached = await redisClient.get(cacheKey);
if (cached) {
const parsed = JSON.parse(cached);
const validated = cachedSequenceSchema.parse(parsed);
const validated = cachedSequenceSchema.parse(parsed) as CachedDeliverySequence;
console.log('sending cached response')
return { deliverySequence: validated };
@ -612,10 +517,6 @@ export const slotsRouter = router({
}
// Fallback to DB
const slot = await getSlotDeliverySequenceInDb(slotId)
/*
// Old implementation - direct DB queries:
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
});
@ -624,13 +525,6 @@ export const slotsRouter = router({
throw new ApiError("Slot not found", 404);
}
const sequence = cachedSequenceSchema.parse(slot.deliverySequence || {});
*/
if (!slot) {
throw new ApiError('Slot not found', 404)
}
const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence;
// Cache the validated result
@ -641,22 +535,18 @@ export const slotsRouter = router({
console.warn('Redis cache write failed:', cacheError);
}
return { deliverySequence: sequence }
return { deliverySequence: sequence };
}),
updateDeliverySequence: protectedProcedure
.input(updateDeliverySequenceSchema)
.mutation(async ({ input, ctx }): Promise<AdminUpdateDeliverySequenceResult> => {
.mutation(async ({ input, ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const { id, deliverySequence } = input;
const updatedSlot = await updateSlotDeliverySequenceInDb(id, deliverySequence)
/*
// Old implementation - direct DB queries:
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ deliverySequence })
@ -669,11 +559,6 @@ export const slotsRouter = router({
if (!updatedSlot) {
throw new ApiError("Slot not found", 404);
}
*/
if (!updatedSlot) {
throw new ApiError('Slot not found', 404)
}
// Cache the updated sequence
const cacheKey = getSlotSequenceKey(id);
@ -686,8 +571,8 @@ export const slotsRouter = router({
return {
slot: updatedSlot,
message: 'Delivery sequence updated successfully',
}
message: "Delivery sequence updated successfully",
};
}),
updateSlotCapacity: protectedProcedure
@ -695,17 +580,13 @@ export const slotsRouter = router({
slotId: z.number(),
isCapacityFull: z.boolean(),
}))
.mutation(async ({ input, ctx }): Promise<AdminUpdateSlotCapacityResult> => {
.mutation(async ({ input, ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const { slotId, isCapacityFull } = input;
const result = await updateSlotCapacityInDb(slotId, isCapacityFull)
/*
// Old implementation - direct DB queries:
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ isCapacityFull })
@ -717,21 +598,12 @@ export const slotsRouter = router({
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return {
success: true,
slot: updatedSlot,
message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`,
};
*/
if (!result) {
throw new ApiError('Slot not found', 404)
}
scheduleStoreInitialization()
return result
}),
});

View file

@ -1,20 +1,11 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { staffUsers, staffRoles, users, userDetails, orders } from '@/src/db/schema'
import { eq, or, ilike, and, lt, desc } from 'drizzle-orm';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { ApiError } from '@/src/lib/api-error'
import {
getStaffUserByName,
getAllStaff,
getAllUsers,
getUserWithDetails,
upsertUserSuspension,
checkStaffUserExists,
checkStaffRoleExists,
createStaffUser,
getAllRoles,
} from '@/src/dbService'
import type { StaffUser, StaffRole } from '@packages/shared'
export const staffUserRouter = router({
login: publicProcedure
@ -29,7 +20,9 @@ export const staffUserRouter = router({
throw new ApiError('Name and password are required', 400);
}
const staff = await getStaffUserByName(name);
const staff = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
});
if (!staff) {
throw new ApiError('Invalid credentials', 401);
@ -55,7 +48,23 @@ export const staffUserRouter = router({
getStaff: protectedProcedure
.query(async ({ ctx }) => {
const staff = await getAllStaff();
const staff = await db.query.staffUsers.findMany({
columns: {
id: true,
name: true,
},
with: {
role: {
with: {
rolePermissions: {
with: {
permission: true,
},
},
},
},
},
});
// Transform the data to include role and permissions in a cleaner format
const transformedStaff = staff.map((user) => ({
@ -65,7 +74,7 @@ export const staffUserRouter = router({
id: user.role.id,
name: user.role.roleName,
} : null,
permissions: user.role?.rolePermissions.map((rp: any) => ({
permissions: user.role?.rolePermissions.map((rp) => ({
id: rp.permission.id,
name: rp.permission.permissionName,
})) || [],
@ -85,9 +94,34 @@ export const staffUserRouter = router({
.query(async ({ input }) => {
const { cursor, limit, search } = input;
const { users: usersToReturn, hasMore } = await getAllUsers(cursor, limit, search);
let whereCondition = undefined;
const formattedUsers = usersToReturn.map((user: any) => ({
if (search) {
whereCondition = or(
ilike(users.name, `%${search}%`),
ilike(users.email, `%${search}%`),
ilike(users.mobile, `%${search}%`)
);
}
if (cursor) {
const cursorCondition = lt(users.id, cursor);
whereCondition = whereCondition ? and(whereCondition, cursorCondition) : cursorCondition;
}
const allUsers = await db.query.users.findMany({
where: whereCondition,
with: {
userDetails: true,
},
orderBy: desc(users.id),
limit: limit + 1, // fetch one extra to check if there's more
});
const hasMore = allUsers.length > limit;
const usersToReturn = hasMore ? allUsers.slice(0, limit) : allUsers;
const formattedUsers = usersToReturn.map(user => ({
id: user.id,
name: user.name,
email: user.email,
@ -106,7 +140,16 @@ export const staffUserRouter = router({
.query(async ({ input }) => {
const { userId } = input;
const user = await getUserWithDetails(userId);
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
userDetails: true,
orders: {
orderBy: desc(orders.createdAt),
limit: 1,
},
},
});
if (!user) {
throw new ApiError("User not found", 404);
@ -130,7 +173,13 @@ export const staffUserRouter = router({
.mutation(async ({ input }) => {
const { userId, isSuspended } = input;
await upsertUserSuspension(userId, isSuspended);
await db
.insert(userDetails)
.values({ userId, isSuspended })
.onConflictDoUpdate({
target: userDetails.userId,
set: { isSuspended },
});
return { success: true };
}),
@ -145,16 +194,20 @@ export const staffUserRouter = router({
const { name, password, roleId } = input;
// Check if staff user already exists
const existingUser = await checkStaffUserExists(name);
const existingUser = await db.query.staffUsers.findFirst({
where: eq(staffUsers.name, name),
});
if (existingUser) {
throw new ApiError('Staff user with this name already exists', 409);
}
// Check if role exists
const roleExists = await checkStaffRoleExists(roleId);
const role = await db.query.staffRoles.findFirst({
where: eq(staffRoles.id, roleId),
});
if (!roleExists) {
if (!role) {
throw new ApiError('Invalid role selected', 400);
}
@ -162,17 +215,26 @@ export const staffUserRouter = router({
const hashedPassword = await bcrypt.hash(password, 12);
// Create staff user
const newUser = await createStaffUser(name, hashedPassword, roleId);
const [newUser] = await db.insert(staffUsers).values({
name: name.trim(),
password: hashedPassword,
staffRoleId: roleId,
}).returning();
return { success: true, user: { id: newUser.id, name: newUser.name } };
}),
getRoles: protectedProcedure
.query(async ({ ctx }) => {
const roles = await getAllRoles();
const roles = await db.query.staffRoles.findMany({
columns: {
id: true,
roleName: true,
},
});
return {
roles: roles.map((role: any) => ({
roles: roles.map(role => ({
id: role.id,
name: role.roleName,
})),

View file

@ -1,29 +1,29 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod';
import { db } from '@/src/db/db_index'
import { storeInfo, productInfo } from '@/src/db/schema'
import { eq, inArray } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error'
import { extractKeyFromPresignedUrl, deleteImageUtil, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { scheduleStoreInitialization } from '@/src/stores/store-initializer'
import {
getAllStores as getAllStoresFromDb,
getStoreById as getStoreByIdFromDb,
createStore as createStoreInDb,
updateStore as updateStoreInDb,
deleteStore as deleteStoreFromDb,
} from '@/src/dbService'
import type { Store } from '@packages/shared'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { initializeAllStores } from '@/src/stores/store-initializer'
export const storeRouter = router({
getStores: protectedProcedure
.query(async ({ ctx }): Promise<{ stores: any[]; count: number }> => {
const stores = await getAllStoresFromDb();
.query(async ({ ctx }) => {
const stores = await db.query.storeInfo.findMany({
with: {
owner: true,
},
});
await Promise.all(stores.map(async store => {
Promise.all(stores.map(async store => {
if(store.imageUrl)
store.imageUrl = await generateSignedUrlFromS3Url(store.imageUrl)
})).catch((e) => {
throw new ApiError("Unable to find store image urls")
})
}
)
return {
stores,
count: stores.length,
@ -34,10 +34,15 @@ export const storeRouter = router({
.input(z.object({
id: z.number(),
}))
.query(async ({ input, ctx }): Promise<{ store: any }> => {
.query(async ({ input, ctx }) => {
const { id } = input;
const store = await getStoreByIdFromDb(id);
const store = await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, id),
with: {
owner: true,
},
});
if (!store) {
throw new ApiError("Store not found", 404);
@ -56,23 +61,11 @@ export const storeRouter = router({
owner: z.number().min(1, "Owner is required"),
products: z.array(z.number()).optional(),
}))
.mutation(async ({ input, ctx }): Promise<{ store: Store; message: string }> => {
.mutation(async ({ input, ctx }) => {
const { name, description, imageUrl, owner, products } = input;
const imageKey = imageUrl ? extractKeyFromPresignedUrl(imageUrl) : undefined;
const newStore = await createStoreInDb(
{
name,
description,
imageUrl: imageKey,
owner,
},
products
);
/*
// Old implementation - direct DB query:
const [newStore] = await db
.insert(storeInfo)
.values({
@ -90,10 +83,9 @@ export const storeRouter = router({
.set({ storeId: newStore.id })
.where(inArray(productInfo.id, products));
}
*/
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return {
store: newStore,
@ -110,10 +102,12 @@ export const storeRouter = router({
owner: z.number().min(1, "Owner is required"),
products: z.array(z.number()).optional(),
}))
.mutation(async ({ input, ctx }): Promise<{ store: Store; message: string }> => {
.mutation(async ({ input, ctx }) => {
const { id, name, description, imageUrl, owner, products } = input;
const existingStore = await getStoreByIdFromDb(id);
const existingStore = await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, id),
});
if (!existingStore) {
throw new ApiError("Store not found", 404);
@ -137,19 +131,6 @@ export const storeRouter = router({
}
}
const updatedStore = await updateStoreInDb(
id,
{
name,
description,
imageUrl: newImageKey,
owner,
},
products
);
/*
// Old implementation - direct DB query:
const [updatedStore] = await db
.update(storeInfo)
.set({
@ -181,10 +162,9 @@ export const storeRouter = router({
.where(inArray(productInfo.id, products));
}
}
*/
// Reinitialize stores to reflect changes
scheduleStoreInitialization()
await initializeAllStores();
return {
store: updatedStore,
@ -196,13 +176,9 @@ export const storeRouter = router({
.input(z.object({
storeId: z.number(),
}))
.mutation(async ({ input, ctx }): Promise<{ message: string }> => {
.mutation(async ({ input, ctx }) => {
const { storeId } = input;
const result = await deleteStoreFromDb(storeId);
/*
// Old implementation - direct DB query with transaction:
const result = await db.transaction(async (tx) => {
// First, update all products of this store to set storeId to null
await tx
@ -224,10 +200,9 @@ export const storeRouter = router({
message: "Store deleted successfully",
};
});
*/
// Reinitialize stores to reflect changes (outside transaction)
scheduleStoreInitialization()
await initializeAllStores();
return result;
}),

View file

@ -1,38 +1,15 @@
import { protectedProcedure } from '@/src/trpc/trpc-index';
import { z } from 'zod';
import { db } from '@/src/db/db_index';
import { users, complaints, orders, orderItems, notifCreds, unloggedUserTokens, userDetails, userIncidents } from '@/src/db/schema';
import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm';
import { ApiError } from '@/src/lib/api-error';
import { notificationQueue } from '@/src/lib/notif-job';
import { recomputeUserNegativityScore } from '@/src/stores/user-negativity-store';
import {
createUserByMobile,
getUserByMobile,
getUnresolvedComplaintsCount,
getAllUsersWithFilters,
getOrderCountsByUserIds,
getLastOrdersByUserIds,
getSuspensionStatusesByUserIds,
getUserBasicInfo,
getUserSuspensionStatus,
getUserOrders,
getOrderStatusesByOrderIds,
getItemCountsByOrderIds,
upsertUserSuspension,
searchUsers,
getAllNotifCreds,
getAllUnloggedTokens,
getNotifTokensByUserIds,
getUserIncidentsWithRelations,
createUserIncident,
} from '@/src/dbService';
export const userRouter = {
createUserByMobile: protectedProcedure
.input(z.object({
mobile: z.string().min(1, 'Mobile number is required'),
}))
.mutation(async ({ input }) => {
async function createUserByMobile(mobile: string): Promise<typeof users.$inferSelect> {
// Clean mobile number (remove non-digits)
const cleanMobile = input.mobile.replace(/\D/g, '');
const cleanMobile = mobile.replace(/\D/g, '');
// Validate: exactly 10 digits
if (cleanMobile.length !== 10) {
@ -40,13 +17,36 @@ export const userRouter = {
}
// Check if user already exists
const existingUser = await getUserByMobile(cleanMobile);
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.mobile, cleanMobile))
.limit(1);
if (existingUser) {
throw new ApiError('User with this mobile number already exists', 409);
}
const newUser = await createUserByMobile(cleanMobile);
// Create user
const [newUser] = await db
.insert(users)
.values({
name: null,
email: null,
mobile: cleanMobile,
})
.returning();
return newUser;
}
export const userRouter = {
createUserByMobile: protectedProcedure
.input(z.object({
mobile: z.string().min(1, 'Mobile number is required'),
}))
.mutation(async ({ input }) => {
const newUser = await createUserByMobile(input.mobile);
return {
success: true,
@ -56,10 +56,10 @@ export const userRouter = {
getEssentials: protectedProcedure
.query(async () => {
const count = await getUnresolvedComplaintsCount();
const count = await db.$count(complaints, eq(complaints.isResolved, false));
return {
unresolvedComplaints: count,
unresolvedComplaints: count || 0,
};
}),
@ -72,14 +72,71 @@ export const userRouter = {
.query(async ({ input }) => {
const { limit, cursor, search } = input;
const { users: usersToReturn, hasMore } = await getAllUsersWithFilters(limit, cursor, search);
// Build where conditions
const whereConditions = [];
if (search && search.trim()) {
whereConditions.push(sql`${users.mobile} ILIKE ${`%${search.trim()}%`}`);
}
if (cursor) {
whereConditions.push(sql`${users.id} > ${cursor}`);
}
// Get users with filters applied
const usersList = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
createdAt: users.createdAt,
})
.from(users)
.where(whereConditions.length > 0 ? sql.join(whereConditions, sql` AND `) : undefined)
.orderBy(asc(users.id))
.limit(limit + 1); // Get one extra to determine if there's more
// Check if there are more results
const hasMore = usersList.length > limit;
const usersToReturn = hasMore ? usersList.slice(0, limit) : usersList;
// Get order stats for each user
const userIds = usersToReturn.map((u: any) => u.id);
const userIds = usersToReturn.map(u => u.id);
const orderCounts = await getOrderCountsByUserIds(userIds);
const lastOrders = await getLastOrdersByUserIds(userIds);
const suspensionStatuses = await getSuspensionStatusesByUserIds(userIds);
let orderCounts: { userId: number; totalOrders: number }[] = [];
let lastOrders: { userId: number; lastOrderDate: Date | null }[] = [];
let suspensionStatuses: { userId: number; isSuspended: boolean }[] = [];
if (userIds.length > 0) {
// Get total orders per user
orderCounts = await db
.select({
userId: orders.userId,
totalOrders: count(orders.id),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId);
// Get last order date per user
lastOrders = await db
.select({
userId: orders.userId,
lastOrderDate: max(orders.createdAt),
})
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId);
// Get suspension status for each user
suspensionStatuses = await db
.select({
userId: userDetails.userId,
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(sql`${userDetails.userId} IN (${sql.join(userIds, sql`, `)})`);
}
// Create lookup maps
const orderCountMap = new Map(orderCounts.map(o => [o.userId, o.totalOrders]));
@ -87,7 +144,7 @@ export const userRouter = {
const suspensionMap = new Map(suspensionStatuses.map(s => [s.userId, s.isSuspended]));
// Combine data
const usersWithStats = usersToReturn.map((user: any) => ({
const usersWithStats = usersToReturn.map(user => ({
...user,
totalOrders: orderCountMap.get(user.id) || 0,
lastOrderDate: lastOrderMap.get(user.id) || null,
@ -112,24 +169,69 @@ export const userRouter = {
const { userId } = input;
// Get user info
const user = await getUserBasicInfo(userId);
const user = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
createdAt: users.createdAt,
})
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user) {
if (!user || user.length === 0) {
throw new ApiError('User not found', 404);
}
// Get user suspension status
const isSuspended = await getUserSuspensionStatus(userId);
const userDetail = await db
.select({
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
// Get all orders for this user
const userOrders = await getUserOrders(userId);
// Get all orders for this user with order items count
const userOrders = await db
.select({
id: orders.id,
readableId: orders.readableId,
totalAmount: orders.totalAmount,
createdAt: orders.createdAt,
isFlashDelivery: orders.isFlashDelivery,
})
.from(orders)
.where(eq(orders.userId, userId))
.orderBy(desc(orders.createdAt));
// Get order status for each order
const orderIds = userOrders.map((o: any) => o.id);
const orderStatuses = await getOrderStatusesByOrderIds(orderIds);
const orderIds = userOrders.map(o => o.id);
let orderStatuses: { orderId: number; isDelivered: boolean; isCancelled: boolean }[] = [];
if (orderIds.length > 0) {
const { orderStatus } = await import('@/src/db/schema');
orderStatuses = await db
.select({
orderId: orderStatus.orderId,
isDelivered: orderStatus.isDelivered,
isCancelled: orderStatus.isCancelled,
})
.from(orderStatus)
.where(sql`${orderStatus.orderId} IN (${sql.join(orderIds, sql`, `)})`);
}
// Get item counts for each order
const itemCounts = await getItemCountsByOrderIds(orderIds);
const itemCounts = await db
.select({
orderId: orderItems.orderId,
itemCount: count(orderItems.id),
})
.from(orderItems)
.where(sql`${orderItems.orderId} IN (${sql.join(orderIds, sql`, `)})`)
.groupBy(orderItems.orderId);
// Create lookup maps
const statusMap = new Map(orderStatuses.map(s => [s.orderId, s]));
@ -144,7 +246,7 @@ export const userRouter = {
};
// Combine data
const ordersWithDetails = userOrders.map((order: any) => {
const ordersWithDetails = userOrders.map(order => {
const status = statusMap.get(order.id);
return {
id: order.id,
@ -159,8 +261,8 @@ export const userRouter = {
return {
user: {
...user,
isSuspended,
...user[0],
isSuspended: userDetail[0]?.isSuspended ?? false,
},
orders: ordersWithDetails,
};
@ -174,7 +276,39 @@ export const userRouter = {
.mutation(async ({ input }) => {
const { userId, isSuspended } = input;
await upsertUserSuspension(userId, isSuspended);
// Check if user exists
const user = await db
.select({ id: users.id })
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user || user.length === 0) {
throw new ApiError('User not found', 404);
}
// Check if user_details record exists
const existingDetail = await db
.select({ id: userDetails.id })
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
if (existingDetail.length > 0) {
// Update existing record
await db
.update(userDetails)
.set({ isSuspended })
.where(eq(userDetails.userId, userId));
} else {
// Insert new record
await db
.insert(userDetails)
.values({
userId,
isSuspended,
});
}
return {
success: true,
@ -189,15 +323,36 @@ export const userRouter = {
.query(async ({ input }) => {
const { search } = input;
const usersList = await searchUsers(search);
// Get all users
let usersList;
if (search && search.trim()) {
usersList = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
})
.from(users)
.where(sql`${users.mobile} ILIKE ${`%${search.trim()}%`} OR ${users.name} ILIKE ${`%${search.trim()}%`}`);
} else {
usersList = await db
.select({
id: users.id,
name: users.name,
mobile: users.mobile,
})
.from(users);
}
// Get eligible users (have notif_creds entry)
const eligibleUsers = await getAllNotifCreds();
const eligibleUsers = await db
.select({ userId: notifCreds.userId })
.from(notifCreds);
const eligibleSet = new Set(eligibleUsers.map(u => u.userId));
return {
users: usersList.map((user: any) => ({
users: usersList.map(user => ({
id: user.id,
name: user.name,
mobile: user.mobile,
@ -220,8 +375,8 @@ export const userRouter = {
if (userIds.length === 0) {
// Send to all users - get tokens from both logged-in and unlogged users
const loggedInTokens = await getAllNotifCreds();
const unloggedTokens = await getAllUnloggedTokens();
const loggedInTokens = await db.select({ token: notifCreds.token }).from(notifCreds);
const unloggedTokens = await db.select({ token: unloggedUserTokens.token }).from(unloggedUserTokens);
tokens = [
...loggedInTokens.map(t => t.token),
@ -229,7 +384,11 @@ export const userRouter = {
];
} else {
// Send to specific users - get their tokens
const userTokens = await getNotifTokensByUserIds(userIds);
const userTokens = await db
.select({ token: notifCreds.token })
.from(notifCreds)
.where(inArray(notifCreds.userId, userIds));
tokens = userTokens.map(t => t.token);
}
@ -268,10 +427,21 @@ export const userRouter = {
.query(async ({ input }) => {
const { userId } = input;
const incidents = await getUserIncidentsWithRelations(userId);
const incidents = await db.query.userIncidents.findMany({
where: eq(userIncidents.userId, userId),
with: {
order: {
with: {
orderStatus: true,
},
},
addedBy: true,
},
orderBy: desc(userIncidents.dateAdded),
});
return {
incidents: incidents.map((incident: any) => ({
incidents: incidents.map(incident => ({
id: incident.id,
userId: incident.userId,
orderId: incident.orderId,
@ -300,13 +470,14 @@ export const userRouter = {
throw new ApiError('Admin user not authenticated', 401);
}
const incident = await createUserIncident(
userId,
orderId,
adminComment,
adminUserId,
negativityScore
);
const incidentObj = { userId, orderId, adminComment, addedBy: adminUserId, negativityScore };
const [incident] = await db.insert(userIncidents)
.values({
...incidentObj,
})
.returning();
recomputeUserNegativityScore(userId);

View file

@ -1,33 +1,10 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'
import dayjs from 'dayjs'
import { z } from 'zod';
import dayjs from 'dayjs';
import { db } from '@/src/db/db_index'
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, users, orderStatus } from '@/src/db/schema'
import { eq, and, inArray, isNotNull, gt, sql, asc, ne } from 'drizzle-orm';
import { appUrl } from '@/src/lib/env-exporter'
import {
checkVendorSnippetExists as checkVendorSnippetExistsInDb,
getVendorSnippetById as getVendorSnippetByIdInDb,
getVendorSnippetByCode as getVendorSnippetByCodeInDb,
getAllVendorSnippets as getAllVendorSnippetsInDb,
createVendorSnippet as createVendorSnippetInDb,
updateVendorSnippet as updateVendorSnippetInDb,
deleteVendorSnippet as deleteVendorSnippetInDb,
getProductsByIds as getProductsByIdsInDb,
getVendorSlotById as getVendorSlotByIdInDb,
getVendorOrdersBySlotId as getVendorOrdersBySlotIdInDb,
getVendorOrders as getVendorOrdersInDb,
updateVendorOrderItemPackaging as updateVendorOrderItemPackagingInDb,
getSlotsAfterDate as getSlotsAfterDateInDb,
} from '@/src/dbService'
import type {
AdminVendorSnippet,
AdminVendorSnippetWithProducts,
AdminVendorSnippetWithSlot,
AdminVendorSnippetDeleteResult,
AdminVendorSnippetOrdersResult,
AdminVendorSnippetOrdersWithSlotResult,
AdminVendorOrderSummary,
AdminUpcomingSlotsResult,
AdminVendorUpdatePackagingResult,
} from '@packages/shared'
const createSnippetSchema = z.object({
snippetCode: z.string().min(1, "Snippet code is required"),
@ -49,7 +26,7 @@ const updateSnippetSchema = z.object({
export const vendorSnippetsRouter = router({
create: protectedProcedure
.input(createSnippetSchema)
.mutation(async ({ input, ctx }): Promise<AdminVendorSnippet> => {
.mutation(async ({ input, ctx }) => {
const { snippetCode, slotId, productIds, validTill, isPermanent } = input;
// Get staff user ID from auth middleware
@ -58,33 +35,6 @@ export const vendorSnippetsRouter = router({
throw new Error("Unauthorized");
}
if(slotId) {
const slot = await getVendorSlotByIdInDb(slotId)
if (!slot) {
throw new Error("Invalid slot ID")
}
}
const products = await getProductsByIdsInDb(productIds)
if (products.length !== productIds.length) {
throw new Error("One or more invalid product IDs")
}
const existingSnippet = await checkVendorSnippetExistsInDb(snippetCode)
if (existingSnippet) {
throw new Error("Snippet code already exists")
}
const result = await createVendorSnippetInDb({
snippetCode,
slotId,
productIds,
isPermanent,
validTill: validTill ? new Date(validTill) : undefined,
})
/*
// Old implementation - direct DB queries:
// Validate slot exists
if(slotId) {
const slot = await db.query.deliverySlotInfo.findFirst({
@ -120,32 +70,13 @@ export const vendorSnippetsRouter = router({
}).returning();
return result[0];
*/
return result
}),
getAll: protectedProcedure
.query(async (): Promise<AdminVendorSnippetWithProducts[]> => {
.query(async () => {
console.log('from the vendor snipptes methods')
try {
const result = await getAllVendorSnippetsInDb()
const snippetsWithProducts = await Promise.all(
result.map(async (snippet) => {
const products = await getProductsByIdsInDb(snippet.productIds)
return {
...snippet,
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`,
products,
}
})
)
/*
// Old implementation - direct DB queries:
const result = await db.query.vendorSnippets.findMany({
with: {
slot: true,
@ -169,25 +100,18 @@ export const vendorSnippetsRouter = router({
);
return snippetsWithProducts;
*/
return snippetsWithProducts
}
catch(e) {
console.log(e)
}
return []
return [];
}),
getById: protectedProcedure
.input(z.object({ id: z.number().int().positive() }))
.query(async ({ input }): Promise<AdminVendorSnippetWithSlot> => {
.query(async ({ input }) => {
const { id } = input;
const result = await getVendorSnippetByIdInDb(id)
/*
// Old implementation - direct DB queries:
const result = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
with: {
@ -200,57 +124,14 @@ export const vendorSnippetsRouter = router({
}
return result;
*/
if (!result) {
throw new Error('Vendor snippet not found')
}
return result
}),
update: protectedProcedure
.input(updateSnippetSchema)
.mutation(async ({ input }): Promise<AdminVendorSnippet> => {
.mutation(async ({ input }) => {
const { id, updates } = input;
const existingSnippet = await getVendorSnippetByIdInDb(id)
if (!existingSnippet) {
throw new Error('Vendor snippet not found')
}
if (updates.slotId) {
const slot = await getVendorSlotByIdInDb(updates.slotId)
if (!slot) {
throw new Error('Invalid slot ID')
}
}
if (updates.productIds) {
const products = await getProductsByIdsInDb(updates.productIds)
if (products.length !== updates.productIds.length) {
throw new Error('One or more invalid product IDs')
}
}
if (updates.snippetCode && updates.snippetCode !== existingSnippet.snippetCode) {
const duplicateSnippet = await checkVendorSnippetExistsInDb(updates.snippetCode)
if (duplicateSnippet) {
throw new Error('Snippet code already exists')
}
}
const updateData = {
...updates,
validTill: updates.validTill !== undefined
? (updates.validTill ? new Date(updates.validTill) : null)
: undefined,
}
const result = await updateVendorSnippetInDb(id, updateData)
/*
// Old implementation - direct DB queries:
// Check if snippet exists
const existingSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
});
@ -303,24 +184,13 @@ export const vendorSnippetsRouter = router({
}
return result[0];
*/
if (!result) {
throw new Error('Failed to update vendor snippet')
}
return result
}),
delete: protectedProcedure
.input(z.object({ id: z.number().int().positive() }))
.mutation(async ({ input }): Promise<AdminVendorSnippetDeleteResult> => {
.mutation(async ({ input }) => {
const { id } = input;
const result = await deleteVendorSnippetInDb(id)
/*
// Old implementation - direct DB queries:
const result = await db.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id))
.returning();
@ -330,26 +200,15 @@ export const vendorSnippetsRouter = router({
}
return { message: "Vendor snippet deleted successfully" };
*/
if (!result) {
throw new Error('Vendor snippet not found')
}
return { message: 'Vendor snippet deleted successfully' }
}),
getOrdersBySnippet: publicProcedure
.input(z.object({
snippetCode: z.string().min(1, "Snippet code is required")
}))
.query(async ({ input }): Promise<AdminVendorSnippetOrdersResult> => {
.query(async ({ input }) => {
const { snippetCode } = input;
const snippet = await getVendorSnippetByCodeInDb(snippetCode)
/*
// Old implementation - direct DB queries:
// Find the snippet
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
@ -383,21 +242,6 @@ export const vendorSnippetsRouter = router({
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
});
*/
if (!snippet) {
throw new Error('Vendor snippet not found')
}
if (snippet.validTill && new Date(snippet.validTill) < new Date()) {
throw new Error('Vendor snippet has expired')
}
if (!snippet.slotId) {
throw new Error('Vendor snippet not associated with a slot')
}
const matchingOrders = await getVendorOrdersBySlotIdInDb(snippet.slotId)
// Filter orders that contain at least one of the snippet's products
const filteredOrders = matchingOrders.filter(order => {
@ -432,7 +276,7 @@ export const vendorSnippetsRouter = router({
return {
orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(),
customerName: order.user.name || '',
customerName: order.user.name,
totalAmount: orderTotal,
slotInfo: order.slot ? {
time: order.slot.deliveryTime.toISOString(),
@ -456,15 +300,11 @@ export const vendorSnippetsRouter = router({
createdAt: snippet.createdAt.toISOString(),
isPermanent: snippet.isPermanent,
},
}
};
}),
getVendorOrders: protectedProcedure
.query(async (): Promise<AdminVendorOrderSummary[]> => {
const vendorOrders = await getVendorOrdersInDb()
/*
// Old implementation - direct DB queries:
.query(async () => {
const vendorOrders = await db.query.orders.findMany({
with: {
user: true,
@ -480,11 +320,10 @@ export const vendorSnippetsRouter = router({
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
});
*/
return vendorOrders.map(order => ({
id: order.id,
status: 'pending',
status: 'pending', // Default status since orders table may not have status field
orderDate: order.createdAt.toISOString(),
totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0),
products: order.orderItems.map(item => ({
@ -492,16 +331,12 @@ export const vendorSnippetsRouter = router({
quantity: parseFloat(item.quantity || '0'),
unit: item.product.unit?.shortNotation || 'unit',
})),
}))
}));
}),
getUpcomingSlots: publicProcedure
.query(async (): Promise<AdminUpcomingSlotsResult> => {
.query(async () => {
const threeHoursAgo = dayjs().subtract(3, 'hour').toDate();
const slots = await getSlotsAfterDateInDb(threeHoursAgo)
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
@ -509,7 +344,6 @@ export const vendorSnippetsRouter = router({
),
orderBy: asc(deliverySlotInfo.deliveryTime),
});
*/
return {
success: true,
@ -519,7 +353,7 @@ export const vendorSnippetsRouter = router({
freezeTime: slot.freezeTime.toISOString(),
deliverySequence: slot.deliverySequence,
})),
}
};
}),
getOrdersBySnippetAndSlot: publicProcedure
@ -527,14 +361,9 @@ export const vendorSnippetsRouter = router({
snippetCode: z.string().min(1, "Snippet code is required"),
slotId: z.number().int().positive("Valid slot ID is required"),
}))
.query(async ({ input }): Promise<AdminVendorSnippetOrdersWithSlotResult> => {
.query(async ({ input }) => {
const { snippetCode, slotId } = input;
const snippet = await getVendorSnippetByCodeInDb(snippetCode)
const slot = await getVendorSlotByIdInDb(slotId)
/*
// Old implementation - direct DB queries:
// Find the snippet
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
@ -572,17 +401,6 @@ export const vendorSnippetsRouter = router({
},
orderBy: (orders, { desc }) => [desc(orders.createdAt)],
});
*/
if (!snippet) {
throw new Error('Vendor snippet not found')
}
if (!slot) {
throw new Error('Slot not found')
}
const matchingOrders = await getVendorOrdersBySlotIdInDb(slotId)
// Filter orders that contain at least one of the snippet's products
const filteredOrders = matchingOrders.filter(order => {
@ -617,7 +435,7 @@ export const vendorSnippetsRouter = router({
return {
orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(),
customerName: order.user.name || '',
customerName: order.user.name,
totalAmount: orderTotal,
slotInfo: order.slot ? {
time: order.slot.deliveryTime.toISOString(),
@ -647,7 +465,7 @@ export const vendorSnippetsRouter = router({
freezeTime: slot.freezeTime.toISOString(),
deliverySequence: slot.deliverySequence,
},
}
};
}),
updateOrderItemPackaging: publicProcedure
@ -655,7 +473,7 @@ export const vendorSnippetsRouter = router({
orderItemId: z.number().int().positive("Valid order item ID required"),
is_packaged: z.boolean()
}))
.mutation(async ({ input, ctx }): Promise<AdminVendorUpdatePackagingResult> => {
.mutation(async ({ input, ctx }) => {
const { orderItemId, is_packaged } = input;
// Get staff user ID from auth middleware
@ -664,10 +482,6 @@ export const vendorSnippetsRouter = router({
// throw new Error("Unauthorized");
// }
const result = await updateVendorOrderItemPackagingInDb(orderItemId, is_packaged)
/*
// Old implementation - direct DB queries:
// Check if order item exists and get related data
const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId),
@ -713,12 +527,5 @@ export const vendorSnippetsRouter = router({
orderItemId,
is_packaged
};
*/
if (!result.success) {
throw new Error(result.message)
}
return result
}),
});

View file

@ -9,32 +9,9 @@ import { generateUploadUrl } from '@/src/lib/s3-client'
import { ApiError } from '@/src/lib/api-error'
import { getAllConstValues } from '@/src/lib/const-store'
import { CONST_KEYS } from '@/src/lib/const-keys'
import { assetsDomain, apiCacheKey } from '@/src/lib/env-exporter'
const polygon = turf.polygon(mbnrGeoJson.features[0].geometry.coordinates);
export async function scaffoldEssentialConsts() {
const consts = await getAllConstValues();
return {
freeDeliveryThreshold: consts[CONST_KEYS.freeDeliveryThreshold] ?? 200,
deliveryCharge: consts[CONST_KEYS.deliveryCharge] ?? 0,
flashFreeDeliveryThreshold: consts[CONST_KEYS.flashFreeDeliveryThreshold] ?? 500,
flashDeliveryCharge: consts[CONST_KEYS.flashDeliveryCharge] ?? 69,
popularItems: consts[CONST_KEYS.popularItems] ?? '5,3,2,4,1',
versionNum: consts[CONST_KEYS.versionNum] ?? '1.1.0',
playStoreUrl: consts[CONST_KEYS.playStoreUrl] ?? 'https://play.google.com/store/apps/details?id=in.freshyo.app',
appStoreUrl: consts[CONST_KEYS.appStoreUrl] ?? 'https://apps.apple.com/in/app/freshyo/id6756889077',
webViewHtml: null,
isWebviewClosable: true,
isFlashDeliveryEnabled: consts[CONST_KEYS.isFlashDeliveryEnabled] ?? true,
supportMobile: consts[CONST_KEYS.supportMobile] ?? '',
supportEmail: consts[CONST_KEYS.supportEmail] ?? '',
assetsDomain,
apiCacheKey,
};
}
export const commonApiRouter = router({
product: commonRouter,
getStoresSummary: publicProcedure
@ -122,8 +99,23 @@ export const commonApiRouter = router({
}),
essentialConsts: publicProcedure
.query(async () => {
const response = await scaffoldEssentialConsts();
return response;
const consts = await getAllConstValues();
return {
freeDeliveryThreshold: consts[CONST_KEYS.freeDeliveryThreshold] ?? 200,
deliveryCharge: consts[CONST_KEYS.deliveryCharge] ?? 0,
flashFreeDeliveryThreshold: consts[CONST_KEYS.flashFreeDeliveryThreshold] ?? 500,
flashDeliveryCharge: consts[CONST_KEYS.flashDeliveryCharge] ?? 69,
popularItems: consts[CONST_KEYS.popularItems] ?? '5,3,2,4,1',
versionNum: consts[CONST_KEYS.versionNum] ?? '1.1.0',
playStoreUrl: consts[CONST_KEYS.playStoreUrl] ?? 'https://play.google.com/store/apps/details?id=in.freshyo.app',
appStoreUrl: consts[CONST_KEYS.appStoreUrl] ?? 'https://apps.apple.com/in/app/freshyo/id6756889077',
webViewHtml: null,
isWebviewClosable: true,
isFlashDeliveryEnabled: consts[CONST_KEYS.isFlashDeliveryEnabled] ?? true,
supportMobile: consts[CONST_KEYS.supportMobile] ?? '',
supportEmail: consts[CONST_KEYS.supportEmail] ?? '',
};
}),
});

View file

@ -1,10 +1,12 @@
import { router, publicProcedure } from '@/src/trpc/trpc-index'
import { db } from '@/src/db/db_index'
import { productInfo, units, productSlots, deliverySlotInfo, storeInfo } from '@/src/db/schema'
import { productInfo, units, productSlots, deliverySlotInfo, storeInfo, productTags, productTagInfo } from '@/src/db/schema'
import { eq, gt, and, sql, inArray } from 'drizzle-orm';
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
import { z } from 'zod';
import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
import { getDashboardTags as getDashboardTagsFromCache } from '@/src/stores/product-tag-store'
import Fuse from 'fuse.js';
export const getNextDeliveryDate = async (productId: number): Promise<Date | null> => {
const result = await db
@ -26,11 +28,66 @@ export const getNextDeliveryDate = async (productId: number): Promise<Date | nul
return result[0]?.deliveryTime || null;
};
export async function scaffoldProducts() {
export const commonRouter = router({
getDashboardTags: publicProcedure
.query(async () => {
// Get dashboard tags from cache
const tags = await getDashboardTagsFromCache();
return {
tags: tags,
};
}),
getAllProductsSummary: publicProcedure
.input(z.object({
searchQuery: z.string().optional(),
tagId: z.number().optional()
}))
.query(async ({ input }) => {
const { searchQuery, tagId } = input;
// Get all products from cache
let products = await getAllProductsFromCache();
products = products.filter(item => Boolean(item.id))
// Apply tag filtering if tagId is provided
if (tagId) {
// Get products that have this tag from the database
const taggedProducts = await db
.select({ productId: productTags.productId })
.from(productTags)
.where(eq(productTags.tagId, tagId));
const taggedProductIds = new Set(taggedProducts.map(tp => tp.productId));
// Filter products based on tag
products = products.filter(product => taggedProductIds.has(product.id));
}
// Apply search filtering if searchQuery is provided using Fuse.js
if (searchQuery) {
const fuse = new Fuse(products, {
keys: [
'name',
'shortDescription',
'longDescription',
'store.name', // Search in store name too
'productTags', // Search in product tags too
],
threshold: 0.3, // Adjust fuzziness (0.0 = exact match, 1.0 = match anything)
includeScore: true,
shouldSort: true,
});
const fuseResults = fuse.search(searchQuery);
products = fuseResults.map(result => result.item);
}
// Get suspended product IDs to filter them out
const suspendedProducts = await db
.select({ id: productInfo.id })
@ -60,33 +117,16 @@ export async function scaffoldProducts() {
isOutOfStock: product.isOutOfStock,
isFlashAvailable: product.isFlashAvailable,
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
images: product.images,
flashPrice: product.flashPrice
images: product.images, // Already signed URLs from cache
};
})
);
return {
products: formattedProducts,
count: formattedProducts.length,
};
}
export const commonRouter = router({
getDashboardTags: publicProcedure
.query(async () => {
// Get dashboard tags from cache
const tags = await getDashboardTagsFromCache();
return {
tags: tags,
};
}),
getAllProductsSummary: publicProcedure
.query(async () => {
const response = await scaffoldProducts();
return response;
}),
getStoresSummary: publicProcedure

View file

@ -1,30 +1,38 @@
import { db } from '@/src/db/db_index';
import { homeBanners } from '@/src/db/schema';
import { publicProcedure, router } from '@/src/trpc/trpc-index';
import { scaffoldAssetUrl } from '@/src/lib/s3-client';
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client';
import { isNotNull, asc } from 'drizzle-orm';
export async function scaffoldBanners() {
export const bannerRouter = router({
getBanners: publicProcedure
.query(async () => {
const banners = await db.query.homeBanners.findMany({
where: isNotNull(homeBanners.serialNum), // Only show assigned banners
orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4
});
// Convert S3 keys to signed URLs for client
const bannersWithSignedUrls = banners.map((banner) => ({
const bannersWithSignedUrls = await Promise.all(
banners.map(async (banner) => {
try {
return {
...banner,
imageUrl: banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl,
}));
imageUrl: banner.imageUrl ? await generateSignedUrlFromS3Url(banner.imageUrl) : banner.imageUrl,
};
} catch (error) {
console.error(`Failed to generate signed URL for banner ${banner.id}:`, error);
return {
...banner,
imageUrl: banner.imageUrl, // Keep original on error
};
}
})
);
return {
banners: bannersWithSignedUrls,
};
}
export const bannerRouter = router({
getBanners: publicProcedure
.query(async () => {
const response = await scaffoldBanners();
return response;
}),
});

View file

@ -23,6 +23,7 @@ import {
sendOrderPlacedNotification,
sendOrderCancelledNotification,
} from "@/src/lib/notif-job";
import { RazorpayPaymentService } from "@/src/lib/payments-utils";
import { getNextDeliveryDate } from "@/src/trpc/apis/common-apis/common";
import { CONST_KEYS, getConstant, getConstants } from "@/src/lib/const-store";
import { publishFormattedOrder, publishCancellation } from "@/src/lib/post-order-handler";
@ -315,15 +316,15 @@ const placeOrderUtil = async (params: {
await tx.insert(orderStatus).values(allOrderStatuses);
if (paymentMethod === "online" && sharedPaymentInfoId) {
// const razorpayOrder = await RazorpayPaymentService.createOrder(
// sharedPaymentInfoId,
// totalWithDelivery.toString()
// );
// await RazorpayPaymentService.insertPaymentRecord(
// sharedPaymentInfoId,
// razorpayOrder,
// tx
// );
const razorpayOrder = await RazorpayPaymentService.createOrder(
sharedPaymentInfoId,
totalWithDelivery.toString()
);
await RazorpayPaymentService.insertPaymentRecord(
sharedPaymentInfoId,
razorpayOrder,
tx
);
}
return insertedOrders;

View file

@ -52,8 +52,7 @@ export const paymentRouter = router({
await RazorpayPaymentService.insertPaymentRecord(parseInt(orderId), razorpayOrder);
return {
razorpayOrderId: 0,
// razorpayOrderId: razorpayOrder.id,
razorpayOrderId: razorpayOrder.id,
key: razorpayId,
};
}),

View file

@ -7,7 +7,7 @@ import {
productInfo,
units,
} from "@/src/db/schema";
import { eq, and } from "drizzle-orm";
import { eq, and, gt, asc } from "drizzle-orm";
import { getAllSlots as getAllSlotsFromCache, getSlotById as getSlotByIdFromCache } from "@/src/stores/slot-store";
import dayjs from 'dayjs';
@ -32,42 +32,6 @@ async function getSlotData(slotId: number) {
};
}
export async function scaffoldSlotsWithProducts() {
const allSlots = await getAllSlotsFromCache();
const currentTime = new Date();
const validSlots = allSlots
.filter((slot) => {
return dayjs(slot.freezeTime).isAfter(currentTime) &&
dayjs(slot.deliveryTime).isAfter(currentTime) &&
!slot.isCapacityFull;
})
.sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf());
// Fetch all products for availability info
const allProducts = await db
.select({
id: productInfo.id,
name: productInfo.name,
isOutOfStock: productInfo.isOutOfStock,
isFlashAvailable: productInfo.isFlashAvailable,
})
.from(productInfo)
.where(eq(productInfo.isSuspended, false));
const productAvailability = allProducts.map(product => ({
id: product.id,
name: product.name,
isOutOfStock: product.isOutOfStock,
isFlashAvailable: product.isFlashAvailable,
}));
return {
slots: validSlots,
productAvailability,
count: validSlots.length,
};
}
export const slotsRouter = router({
getSlots: publicProcedure.query(async () => {
const slots = await db.query.deliverySlotInfo.findMany({
@ -80,8 +44,40 @@ export const slotsRouter = router({
}),
getSlotsWithProducts: publicProcedure.query(async () => {
const response = await scaffoldSlotsWithProducts();
return response;
const allSlots = await getAllSlotsFromCache();
const currentTime = new Date();
const validSlots = allSlots
.filter((slot) => {
return dayjs(slot.freezeTime).isAfter(currentTime) &&
dayjs(slot.deliveryTime).isAfter(currentTime) &&
!slot.isCapacityFull;
})
.sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf());
return {
slots: validSlots,
count: validSlots.length,
};
}),
nextMajorDelivery: publicProcedure.query(async () => {
const now = new Date();
// Find the next upcoming active delivery slot ID
const nextSlot = await db.query.deliverySlotInfo.findFirst({
where: and(
eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, now),
),
orderBy: asc(deliverySlotInfo.deliveryTime),
});
if (!nextSlot) {
return null; // No upcoming delivery slots
}
// Get formatted data using helper method
return await getSlotData(nextSlot.id);
}),
getSlotById: publicProcedure

View file

@ -5,9 +5,10 @@ import { storeInfo, productInfo, units } from '@/src/db/schema';
import { eq, and, sql } from 'drizzle-orm';
import { scaffoldAssetUrl } from '@/src/lib/s3-client';
import { ApiError } from '@/src/lib/api-error';
import { getTagsByStoreId } from '@/src/stores/product-tag-store';
export async function scaffoldStores() {
export const storesRouter = router({
getStores: publicProcedure
.query(async () => {
const storesData = await db
.select({
id: storeInfo.id,
@ -65,9 +66,15 @@ export async function scaffoldStores() {
return {
stores: storesWithDetails,
};
}
}),
getStoreWithProducts: publicProcedure
.input(z.object({
storeId: z.number(),
}))
.query(async ({ input }) => {
const { storeId } = input;
export async function scaffoldStoreWithProducts(storeId: number) {
// Fetch store info
const storeData = await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, storeId),
@ -123,8 +130,6 @@ export async function scaffoldStoreWithProducts(storeId: number) {
}))
);
const tags = await getTagsByStoreId(storeId);
return {
store: {
id: storeData.id,
@ -133,30 +138,6 @@ export async function scaffoldStoreWithProducts(storeId: number) {
signedImageUrl,
},
products: productsWithSignedUrls,
tags: tags.map(tag => ({
id: tag.id,
tagName: tag.tagName,
tagDescription: tag.tagDescription,
imageUrl: tag.imageUrl,
productIds: tag.productIds,
})),
};
}
export const storesRouter = router({
getStores: publicProcedure
.query(async () => {
const response = await scaffoldStores();
return response;
}),
getStoreWithProducts: publicProcedure
.input(z.object({
storeId: z.number(),
}))
.query(async ({ input }) => {
const { storeId } = input;
const response = await scaffoldStoreWithProducts(storeId);
return response;
}),
});

View file

@ -3,11 +3,6 @@ import { z } from 'zod';
import { adminRouter } from '@/src/trpc/apis/admin-apis/apis/admin-trpc-index'
import { userRouter } from '@/src/trpc/apis/user-apis/apis/user-trpc-index'
import { commonApiRouter } from '@/src/trpc/apis/common-apis/common-trpc-index'
import { scaffoldProducts } from './apis/common-apis/common';
import { scaffoldStores, scaffoldStoreWithProducts } from './apis/user-apis/apis/stores';
import { scaffoldSlotsWithProducts } from './apis/user-apis/apis/slots';
import { scaffoldEssentialConsts } from './apis/common-apis/common-trpc-index';
import { scaffoldBanners } from './apis/user-apis/apis/banners';
// Create the main app router
export const appRouter = router({
@ -21,13 +16,5 @@ export const appRouter = router({
common: commonApiRouter,
});
// Export type definition of API
export type AppRouter = typeof appRouter;
export type AllProductsApiType = Awaited<ReturnType<typeof scaffoldProducts>>;
export type StoresApiType = Awaited<ReturnType<typeof scaffoldStores>>;
export type SlotsApiType = Awaited<ReturnType<typeof scaffoldSlotsWithProducts>>;
export type EssentialConstsApiType = Awaited<ReturnType<typeof scaffoldEssentialConsts>>;
export type BannersApiType = Awaited<ReturnType<typeof scaffoldBanners>>;
export type StoreWithProductsApiType = Awaited<ReturnType<typeof scaffoldStoreWithProducts>>;

View file

@ -1,4 +0,0 @@
// Database Types - Re-exports from shared package
// Central type definitions for backend database operations
export type { Banner } from '@packages/shared';

View file

@ -33,12 +33,6 @@
"shared-types": ["../shared-types"],
"@commonTypes": ["../../packages/ui/shared-types"],
"@commonTypes/*": ["../../packages/ui/shared-types/*"],
"@packages/shared": ["../../packages/shared"],
"@packages/shared/*": ["../../packages/shared/*"],
"postgresService": ["../../packages/db_helper_postgres"],
"postgresService/*": ["../../packages/db_helper_postgres/*"],
"global-shared": ["../../packages/shared"],
"global-shared/*": ["../../packages/shared/*"]
},
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [""], /* Specify multiple folders that act like './node_modules/@types'. */
@ -122,6 +116,6 @@
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["src", "types", "index.ts", "../shared-types", "../../packages/shared"]
"include": ["src", "types", "index.ts", "../shared-types"]
}

View file

@ -11,7 +11,6 @@ import { CreateCouponRoute } from './routes/create-coupon'
import { LocationMarkerRoute } from './routes/location-marker'
import { UserConnectRoute } from './routes/user-connect'
import Inauguration from './routes/inauguration'
import { DemoRoute } from './routes/demo'
import { AuthWrapper } from './components/AuthWrapper'
import { SuperAdminGuard } from './components/SuperAdminGuard'
import { cn } from '@/lib/utils'
@ -125,16 +124,6 @@ const locationMarkerRoute = new Route({
)
})
const demoRoute = new Route({
getParentRoute: () => rootRoute,
path: '/demo',
component: () => (
<Suspense fallback={<p>Loading demo</p>}>
<DemoRoute />
</Suspense>
)
})
const routeTree = rootRoute.addChildren([
dashboardRoute,
vendorOrderListRoute,
@ -144,8 +133,7 @@ const routeTree = rootRoute.addChildren([
createCouponRoute,
userConnectRoute,
locationMarkerRoute,
inaugurationRoute,
demoRoute
inaugurationRoute
])
export function createAppRouter() {

View file

@ -1,259 +0,0 @@
import { useState } from 'react'
import { getAuthToken } from '@/services/auth'
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000'
export function DemoRoute() {
const [method, setMethod] = useState<string>('GET')
const [endpoint, setEndpoint] = useState<string>('/api/test')
const [headers, setHeaders] = useState<string>('{}')
const [body, setBody] = useState<string>('{}')
const [response, setResponse] = useState<any>(null)
const [error, setError] = useState<string>('')
const [loading, setLoading] = useState(false)
const [history, setHistory] = useState<Array<{ method: string; endpoint: string; timestamp: string }>>([])
const handleSubmit = async () => {
setLoading(true)
setError('')
setResponse(null)
try {
const token = await getAuthToken()
const url = `${API_BASE_URL}${endpoint}`
let parsedHeaders: Record<string, string> = {}
try {
parsedHeaders = JSON.parse(headers)
} catch {
throw new Error('Invalid headers JSON')
}
const fetchOptions: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...parsedHeaders,
},
}
if (method !== 'GET' && method !== 'HEAD') {
try {
const parsedBody = JSON.parse(body)
fetchOptions.body = JSON.stringify(parsedBody)
} catch {
throw new Error('Invalid body JSON')
}
}
const startTime = performance.now()
const res = await fetch(url, fetchOptions)
const endTime = performance.now()
const duration = Math.round(endTime - startTime)
let data
const contentType = res.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
data = await res.json()
} else {
data = await res.text()
}
setResponse({
status: res.status,
statusText: res.statusText,
duration: `${duration}ms`,
headers: Object.fromEntries(res.headers.entries()),
data,
})
// Add to history
setHistory(prev => [
{ method, endpoint, timestamp: new Date().toLocaleTimeString() },
...prev.slice(0, 9), // Keep last 10
])
} catch (err: any) {
setError(err.message || 'An error occurred')
} finally {
setLoading(false)
}
}
const loadFromHistory = (item: { method: string; endpoint: string }) => {
setMethod(item.method)
setEndpoint(item.endpoint)
}
const getStatusColor = (status: number) => {
if (status >= 200 && status < 300) return 'bg-green-500'
if (status >= 300 && status < 400) return 'bg-yellow-500'
if (status >= 400) return 'bg-red-500'
return 'bg-gray-500'
}
return (
<div className="max-w-6xl mx-auto p-6 space-y-6">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-gray-900">API Demo & Testing</h1>
<span className="px-3 py-1 bg-gray-200 rounded-full text-sm text-gray-700">
{API_BASE_URL}
</span>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Request Panel */}
<div className="lg:col-span-2 bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Request</h2>
<div className="space-y-4">
<div className="flex gap-2">
<select
value={method}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setMethod(e.target.value)}
className="px-4 py-2 border rounded-md bg-white w-32"
>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
<input
type="text"
placeholder="/api/endpoint"
value={endpoint}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEndpoint(e.target.value)}
className="flex-1 px-4 py-2 border rounded-md"
/>
<button
onClick={handleSubmit}
disabled={loading}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-blue-400 transition-colors"
>
{loading ? 'Sending...' : 'Send'}
</button>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">Headers (JSON)</label>
<textarea
value={headers}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setHeaders(e.target.value)}
placeholder='{"Custom-Header": "value"}'
className="w-full px-4 py-2 border rounded-md font-mono text-sm min-h-[80px]"
/>
</div>
{method !== 'GET' && method !== 'HEAD' && (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">Body (JSON)</label>
<textarea
value={body}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setBody(e.target.value)}
placeholder='{"key": "value"}'
className="w-full px-4 py-2 border rounded-md font-mono text-sm min-h-[120px]"
/>
</div>
)}
</div>
</div>
{/* History Panel */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">History</h2>
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{history.length === 0 ? (
<p className="text-gray-500 text-sm">No requests yet</p>
) : (
history.map((item, index) => (
<button
key={index}
onClick={() => loadFromHistory(item)}
className="w-full text-left p-3 rounded hover:bg-gray-100 transition-colors text-sm border"
>
<div className="flex items-center gap-2">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
item.method === 'GET' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
}`}>
{item.method}
</span>
<span className="truncate flex-1 text-gray-900">{item.endpoint}</span>
</div>
<div className="text-xs text-gray-500 mt-1">
{item.timestamp}
</div>
</button>
))
)}
</div>
</div>
{/* Response Panel */}
<div className="lg:col-span-3 bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Response</h2>
{error ? (
<div className="p-4 bg-red-50 border border-red-200 rounded-md text-red-700">
<p className="font-medium">Error</p>
<p className="text-sm">{error}</p>
</div>
) : response ? (
<div className="space-y-4">
<div className="flex items-center gap-4">
<span className={`px-3 py-1 rounded-full text-white text-sm font-medium ${getStatusColor(response.status)}`}>
{response.status} {response.statusText}
</span>
<span className="px-3 py-1 border rounded-full text-sm">
{response.duration}
</span>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">Response Headers</label>
<pre className="bg-gray-100 p-3 rounded-md overflow-x-auto text-xs">
{JSON.stringify(response.headers, null, 2)}
</pre>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">Response Body</label>
<pre className="bg-gray-100 p-3 rounded-md overflow-x-auto text-xs">
{typeof response.data === 'string'
? response.data
: JSON.stringify(response.data, null, 2)}
</pre>
</div>
</div>
) : (
<div className="text-center py-12 text-gray-500">
<p>Send a request to see the response</p>
</div>
)}
</div>
</div>
{/* Quick Links */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Quick Test Endpoints</h2>
<div className="flex flex-wrap gap-2">
{[
{ method: 'GET', endpoint: '/trpc/user.banner.getActiveBanners' },
{ method: 'GET', endpoint: '/trpc/user.product.getAllProducts' },
{ method: 'GET', endpoint: '/trpc/user.user.getCurrentUser' },
{ method: 'POST', endpoint: '/trpc/user.auth.login' },
].map((item, index) => (
<button
key={index}
onClick={() => loadFromHistory(item)}
className="px-4 py-2 border rounded-md hover:bg-gray-50 transition-colors text-sm"
>
<span className="mr-2 px-2 py-0.5 bg-gray-200 rounded text-xs">
{item.method}
</span>
{item.endpoint}
</button>
))}
</div>
</div>
</div>
)
}

View file

@ -24,6 +24,12 @@
"@/*": [
"./src/*"
],
"common-ui": [
"../../packages/ui"
],
"common-ui/*": [
"../../packages/ui/*"
]
},
"types": [
"node"

View file

@ -5,7 +5,7 @@ import { trpc } from '@/src/trpc-client';
import { MyText, MyTouchableOpacity, tw, AppContainer } from 'common-ui';
import { useRouter } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api';
export default function FlashDeliveryBaseLayout() {
const router = useRouter();

View file

@ -21,15 +21,13 @@ import AddToCartDialog from "@/src/components/AddToCartDialog";
import MyFlatList from "common-ui/src/components/flat-list";
import { trpc } from "@/src/trpc-client";
import { useAllProducts, useStores, useSlots, useGetEssentialConsts } from "@/src/hooks/prominent-api-hooks";
import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
import { useCentralSlotStore } from "@/src/store/centralSlotStore";
import { useCentralProductStore } from "@/src/store/centralProductStore";
import FloatingCartBar from "@/components/floating-cart-bar";
import BannerCarousel from "@/components/BannerCarousel";
import { useUserDetails } from "@/src/contexts/AuthContext";
import TabLayoutWrapper from "@/components/TabLayoutWrapper";
import { useNavigationStore } from "@/src/store/navigationStore";
import { useGetEssentialConsts } from "@/src/api-hooks/essential-consts.api";
import NextOrderGlimpse from "@/components/NextOrderGlimpse";
dayjs.extend(relativeTime);
@ -362,6 +360,8 @@ export default function Dashboard() {
const router = useRouter();
const userDetails = useUserDetails();
const [inputQuery, setInputQuery] = useState("");
const [searchQuery, setSearchQuery] = useState("");
const [selectedTagId, setSelectedTagId] = useState<number | null>(null);
const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false);
const [gradientHeight, setGradientHeight] = useState(0);
const [displayedProducts, setDisplayedProducts] = useState<any[]>([]);
@ -369,21 +369,22 @@ export default function Dashboard() {
const [isLoadingMore, setIsLoadingMore] = useState(false);
const { backgroundColor } = useStatusBarStore();
const { getQuickestSlot } = useProductSlotIdentifier();
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const refetchProducts = useCentralProductStore((state) => state.refetchProducts);
const refetchSlotsFromStore = useCentralSlotStore((state) => state.refetchSlots);
const [isRefreshing, setIsRefreshing] = useState(false);
const {
data: productsData,
isLoading,
error,
} = useAllProducts();
refetch,
} = trpc.common.product.getAllProductsSummary.useQuery({
searchQuery: searchQuery || undefined,
tagId: selectedTagId || undefined,
});
const { data: essentialConsts, isLoading: isLoadingConsts, error: constsError, refetch: refetchConsts } = useGetEssentialConsts();
const { data: storesData, refetch: refetchStores } = useStores();
const { data: slotsData } = useSlots();
const { data: storesData, refetch: refetchStores } = trpc.user.stores.getStores.useQuery();
const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlotsWithProducts.useQuery();
const products = productsData?.products || [];
@ -396,18 +397,15 @@ export default function Dashboard() {
const slotB = getQuickestSlot(b.id);
if (slotA && !slotB) return -1;
if (!slotA && slotB) return 1;
const aOutOfStock = productSlotsMap[a.id]?.isOutOfStock;
const bOutOfStock = productSlotsMap[b.id]?.isOutOfStock;
if (aOutOfStock && !bOutOfStock) return 1;
if (!aOutOfStock && bOutOfStock) return -1;
if (a.isOutOfStock && !b.isOutOfStock) return 1;
if (!a.isOutOfStock && b.isOutOfStock) return -1;
return 0;
});
console.log('setting the displayed products')
setDisplayedProducts(initialBatch);
setHasMore(products.length > 10);
}
}, [productsData, productSlotsMap]);
}, [productsData]);
const popularItemIds = useMemo(() => {
const popularItems = essentialConsts?.popularItems;
@ -442,22 +440,11 @@ export default function Dashboard() {
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
try {
const promises = [];
if (refetchProducts) {
promises.push(refetchProducts());
}
if (refetchSlotsFromStore) {
promises.push(refetchSlotsFromStore());
}
promises.push(refetchStores());
promises.push(refetchConsts());
await Promise.all(promises);
await Promise.all([refetch(), refetchStores(), refetchSlots(), refetchConsts()]);
} finally {
setIsRefreshing(false);
}
}, [refetchProducts, refetchSlotsFromStore, refetchStores, refetchConsts]);
}, [refetch, refetchStores, refetchSlots, refetchConsts]);
useManualRefresh(() => {
handleRefresh();
@ -481,7 +468,6 @@ export default function Dashboard() {
const renderProductItem = useCallback(({ item }: { item: any }) => (
<ProductItem item={item} onPress={handleProductPress} />
// <Image style={{ width: 150, height: 235 }} source={{ uri: item.images[0]}} />
), [handleProductPress]);
const listHeader = useMemo(() => (
@ -526,9 +512,7 @@ export default function Dashboard() {
</View>
);
}
let str = ''
displayedProducts.forEach(product => str += `${product.id}-`)
// console.log(str)
return (
<TabLayoutWrapper>
<View style={searchBarContainerStyle}>

View file

@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from "react";
import React, { useState, useRef, useEffect, useCallback } from "react";
import { View, Dimensions } from "react-native";
import { useRouter, useLocalSearchParams } from "expo-router";
import {
@ -10,8 +10,7 @@ import {
SearchBar,
} from "common-ui";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import Fuse from "fuse.js";
import { useAllProducts } from "@/src/hooks/prominent-api-hooks";
import { trpc } from "@/src/trpc-client";
import ProductCard from "@/components/ProductCard";
import FloatingCartBar from "@/components/floating-cart-bar";
@ -52,27 +51,12 @@ export default function SearchResults() {
});
}, []);
const { data: productsData, isLoading, error, refetch } = useAllProducts();
const allProducts = productsData?.products || [];
// Client-side search filtering using Fuse.js
const products = useMemo(() => {
if (!debouncedQuery.trim()) return allProducts;
const fuse = new Fuse(allProducts, {
keys: [
'name',
'shortDescription',
],
threshold: 0.3,
includeScore: true,
shouldSort: true,
const { data: productsData, isLoading, error, refetch } =
trpc.common.product.getAllProductsSummary.useQuery({
searchQuery: debouncedQuery || undefined,
});
const fuseResults = fuse.search(debouncedQuery);
return fuseResults.map(result => result.item);
}, [allProducts, debouncedQuery]);
const products = productsData?.products || [];
useManualRefresh(() => {
refetch();

View file

@ -4,7 +4,7 @@ import { Image } from 'expo-image';
import { MyText, tw, useManualRefresh, MyFlatList, useMarkDataFetchers, theme, MyTouchableOpacity } from 'common-ui';
import { MaterialIcons, Ionicons } from '@expo/vector-icons';
import { trpc } from '@/src/trpc-client';
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api';
import dayjs from 'dayjs';
import { useRouter } from 'expo-router';

View file

@ -0,0 +1,9 @@
import { Stack } from 'expo-router'
function DeliverySlotsLayout() {
return (
<Stack screenOptions={{ headerShown: true, title: 'Delivery Slots' }} />
)
}
export default DeliverySlotsLayout

View file

@ -0,0 +1,230 @@
import React, { useState } from 'react';
import { View, ScrollView } from 'react-native';
import { Image } from 'expo-image';
import { useRouter } from 'expo-router';
import { MyFlatList, MyText, tw, useMarkDataFetchers, BottomDialog, theme, MyTouchableOpacity } from 'common-ui';
import { trpc } from '@/src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import dayjs from 'dayjs';
export default function DeliverySlots() {
const router = useRouter();
const { data, isLoading, error, refetch } = trpc.user.slots.getSlotsWithProducts.useQuery();
const [selectedSlotForDialog, setSelectedSlotForDialog] = useState<any>(null);
useMarkDataFetchers(() => {
refetch();
});
if (isLoading) {
return (
<MyFlatList
data={[]}
renderItem={() => null}
ListHeaderComponent={() => (
<View style={tw`flex-1 justify-center items-center p-8`}>
<MyText style={tw`text-gray-600`}>Loading delivery slots...</MyText>
</View>
)}
/>
);
}
if (error) {
return (
<MyFlatList
data={[]}
renderItem={() => null}
ListHeaderComponent={() => (
<View style={tw`flex-1 justify-center items-center p-8`}>
<MyText style={tw`text-red-600`}>Error loading delivery slots</MyText>
<MyTouchableOpacity
onPress={() => refetch()}
style={tw`mt-4 bg-blue-500 px-4 py-2 rounded-lg`}
>
<MyText style={tw`text-white font-semibold`}>Retry</MyText>
</MyTouchableOpacity>
</View>
)}
/>
);
}
const slots = data?.slots || [];
if (slots.length === 0) {
return (
<MyFlatList
data={[]}
renderItem={() => null}
ListHeaderComponent={() => (
<View style={tw`flex-1 justify-center items-center p-8`}>
<MaterialIcons name="schedule" size={64} color="#D1D5DB" />
<MyText style={tw`text-gray-500 text-center mt-4 text-lg`}>
No upcoming delivery slots available
</MyText>
<MyText style={tw`text-gray-400 text-center mt-2`}>
Check back later for new delivery schedules
</MyText>
</View>
)}
/>
);
}
return (
<>
<MyFlatList
data={slots}
keyExtractor={(item) => item.id.toString()}
// ListHeaderComponent={() => (
// <View style={tw`p-4 pb-2`}>
// <MyText style={tw`text-2xl font-bold text-gray-800`}>Delivery Slots</MyText>
// <MyText style={tw`text-gray-600 mt-1`}>
// Choose your preferred delivery time
// </MyText>
// </View>
// )}
renderItem={({ item: slot }) => (
<View style={tw`mx-4 mb-4 bg-white rounded-xl shadow-md overflow-hidden`}>
{/* Slot Header */}
<View style={tw`bg-pink-50 p-4 border-b border-pink-100`}>
<View style={tw`flex-row items-center justify-between`}>
<View>
<MyText style={tw`text-lg font-bold text-gray-800`}>
{dayjs(slot.deliveryTime).format('ddd DD MMM, h:mm a')}
</MyText>
<MyText style={tw`text-sm text-gray-600 mt-1`}>
Orders close by: {dayjs(slot.freezeTime).format('h:mm a')}
</MyText>
</View>
<View style={tw`flex-row items-center`}>
<View style={tw`bg-pink-500 px-3 py-1 rounded-full mr-3`}>
<MyText style={tw`text-white text-sm font-semibold`}>
{slot.products.length} items
</MyText>
</View>
<MyTouchableOpacity
onPress={() => router.push(`/(drawer)/(tabs)/home/cart?slot=${slot.id}`)}
style={tw`bg-pink-500 p-2 rounded-full`}
>
<MaterialIcons name="flash-on" size={16} color="white" />
</MyTouchableOpacity>
</View>
</View>
</View>
{/* Products List */}
<View style={tw`p-4`}>
<MyText style={tw`text-base font-semibold text-gray-700 mb-3`}>
Available Products
</MyText>
<View style={tw`space-y-2`}>
{slot.products.slice(0, 2).map((product) => (
<MyTouchableOpacity
key={product.id}
onPress={() => router.push(`/(drawer)/(tabs)/home/product-detail/${product.id}`)}
style={tw`bg-gray-50 rounded-lg p-3 flex-row items-center`}
>
{product.images && product.images.length > 0 ? (
<Image
source={{ uri: product.images[0] }}
style={tw`w-8 h-8 rounded mr-3`}
resizeMode="cover"
/>
) : (
<View style={tw`w-8 h-8 bg-gray-200 rounded mr-3 justify-center items-center`}>
<MaterialIcons name="image" size={16} color="#9CA3AF" />
</View>
)}
<View style={tw`flex-1`}>
<MyText style={tw`text-sm font-medium text-gray-800`} numberOfLines={1}>
{product.name}
</MyText>
<MyText style={tw`text-xs text-gray-600`}>
{product.price} {product.unit && `per ${product.unit}`}
</MyText>
</View>
{product.isOutOfStock && (
<MyText style={tw`text-xs text-red-500 font-medium`}>Out of stock</MyText>
)}
</MyTouchableOpacity>
))}
{slot.products.length > 2 && (
<MyTouchableOpacity
onPress={() => setSelectedSlotForDialog(slot)}
style={tw`bg-pink-50 rounded-lg p-3 flex-row items-center justify-center border border-pink-200`}
>
<MyText style={tw`text-sm font-medium text-pink-700`}>
+{slot.products.length - 2} more products
</MyText>
<MaterialIcons name="chevron-right" size={16} color={theme.colors.brand500} style={tw`ml-1`} />
</MyTouchableOpacity>
)}
</View>
</View>
</View>
)}
ListFooterComponent={() => <View style={tw`h-4`} />}
showsVerticalScrollIndicator={false}
contentContainerStyle={tw`pt-2`}
/>
{/* Products Dialog */}
<BottomDialog
open={!!selectedSlotForDialog}
onClose={() => setSelectedSlotForDialog(null)}
>
<View style={tw`p-6`}>
<MyText style={tw`text-xl font-bold text-gray-800 mb-4`}>
All Products - {dayjs(selectedSlotForDialog?.deliveryTime).format('ddd DD MMM, h:mm a')}
</MyText>
<ScrollView style={tw`max-h-96`} showsVerticalScrollIndicator={false}>
<View style={tw`space-y-3`}>
{selectedSlotForDialog?.products.map((product: any) => (
<MyTouchableOpacity
key={product.id}
onPress={() => {
setSelectedSlotForDialog(null);
router.push(`/(drawer)/(tabs)/home/product-detail/${product.id}`);
}}
style={tw`bg-gray-50 rounded-lg p-4 flex-row items-center`}
>
{product.images && product.images.length > 0 ? (
<Image
source={{ uri: product.images[0] }}
style={tw`w-12 h-12 rounded mr-4`}
resizeMode="cover"
/>
) : (
<View style={tw`w-12 h-12 bg-gray-200 rounded mr-4 justify-center items-center`}>
<MaterialIcons name="image" size={20} color="#9CA3AF" />
</View>
)}
<View style={tw`flex-1`}>
<MyText style={tw`text-base font-medium text-gray-800`} numberOfLines={1}>
{product.name}
</MyText>
<MyText style={tw`text-sm text-gray-600 mt-1`}>
{product.price} {product.unit && `per ${product.unit}`}
</MyText>
{product.marketPrice && (
<MyText style={tw`text-sm text-gray-500 line-through`}>
{product.marketPrice}
</MyText>
)}
</View>
{product.isOutOfStock && (
<MyText style={tw`text-xs text-red-500 font-medium`}>Out of stock</MyText>
)}
</MyTouchableOpacity>
))}
</View>
</ScrollView>
</View>
</BottomDialog>
</>
);
}

View file

@ -16,7 +16,7 @@ import {
} from "common-ui";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { Ionicons } from "@expo/vector-icons";
import { useStores } from "@/src/hooks/prominent-api-hooks";
import { trpc } from "@/src/trpc-client";
import { LinearGradient } from "expo-linear-gradient";
import TabLayoutWrapper from "@/components/TabLayoutWrapper";
import FloatingCartBar from "@/components/floating-cart-bar";
@ -157,7 +157,7 @@ export default function Stores() {
isLoading,
error,
refetch,
} = useStores();
} = trpc.user.stores.getStores.useQuery();
const stores = storesData?.stores || [];

View file

@ -1,4 +1,4 @@
import React, { useMemo, useState } from "react";
import React, { useState } from "react";
import { View, Dimensions, ScrollView, TouchableOpacity } from "react-native";
import { useRouter, useLocalSearchParams } from "expo-router";
import { Image } from 'expo-image';
@ -13,10 +13,10 @@ import {
} from "common-ui";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import FontAwesome5 from "@expo/vector-icons/FontAwesome5";
import { trpc } from "@/src/trpc-client";
import ProductCard from "@/components/ProductCard";
import FloatingCartBar from "@/components/floating-cart-bar";
import { useStoreHeaderStore } from "@/src/store/storeHeaderStore";
import { useAllProducts, useStoreWithProducts } from "@/src/hooks/prominent-api-hooks";
const { width: screenWidth } = Dimensions.get("window");
const itemWidth = (screenWidth - 48) / 2;
@ -63,32 +63,24 @@ export default function StoreDetail() {
const [selectedTagId, setSelectedTagId] = useState<number | null>(null);
const { data: storeData, isLoading, refetch, error } =
useStoreWithProducts(storeIdNum);
trpc.user.stores.getStoreWithProducts.useQuery(
{ storeId: storeIdNum },
{ enabled: !!storeIdNum }
);
const { data: productsData, isLoading: isProductsLoading } = useAllProducts();
const productById = useMemo(() => {
const map = new Map<number, any>();
productsData?.products?.forEach((product) => {
map.set(product.id, product);
});
return map;
}, [productsData]);
const storeProducts = useMemo(() => {
if (!storeData?.products) return [];
return storeData.products
.map((product) => productById.get(product.id))
.filter(Boolean);
}, [storeData, productById]);
const { data: tagsData, isLoading: isLoadingTags } =
trpc.user.tags.getTagsByStore.useQuery(
{ storeId: storeIdNum },
{ enabled: !!storeIdNum }
);
// Filter products based on selected tag
const filteredProducts = selectedTagId
? storeProducts.filter(product => {
const selectedTag = storeData?.tags.find(t => t.id === selectedTagId);
? storeData?.products.filter(product => {
const selectedTag = tagsData?.tags.find(t => t.id === selectedTagId);
return selectedTag?.productIds?.includes(product.id) ?? false;
})
: storeProducts;
}) || []
: storeData?.products || [];
// Set the store header title
const setStoreHeaderTitle = useStoreHeaderStore((state) => state.setTitle);
@ -106,12 +98,10 @@ export default function StoreDetail() {
useDrawerTitle(storeData?.store?.name || "Store", [storeData?.store?.name]);
if (isLoading || isProductsLoading) {
if (isLoading) {
return (
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
<MyText style={tw`text-gray-500 font-medium`}>
{isLoading ? 'Loading store...' : 'Loading products...'}
</MyText>
<MyText style={tw`text-gray-500 font-medium`}>Loading store...</MyText>
</View>
);
}
@ -194,13 +184,13 @@ export default function StoreDetail() {
)}
</View>
{/* Tags Section */}
{storeData?.tags && storeData.tags.length > 0 && (
{tagsData && tagsData.tags.length > 0 && (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={tw`gap-2 mt-6`}
>
{storeData.tags.map((tag) => (
{tagsData.tags.map((tag) => (
<Chip
key={tag.id}
tag={tag}
@ -216,7 +206,7 @@ export default function StoreDetail() {
<MaterialIcons name="grid-view" size={20} color="#374151" />
<MyText style={tw`text-lg font-bold text-gray-900 ml-2`}>
{selectedTagId
? `${storeData?.tags.find(t => t.id === selectedTagId)?.tagName} items`
? `${tagsData?.tags.find(t => t.id === selectedTagId)?.tagName} items`
: `${filteredProducts.length} products`}
</MyText>
</View>

View file

@ -22,7 +22,6 @@ import LocationTestWrapper from "@/components/LocationTestWrapper";
import HealthTestWrapper from "@/components/HealthTestWrapper";
import FirstUserWrapper from "@/components/FirstUserWrapper";
import UpdateChecker from "@/components/UpdateChecker";
import CentralStoreInitializer from "@/src/components/CentralStoreInitializer";
import { RefreshProvider } from "../../../packages/ui/src/lib/refresh-context";
import WebViewWrapper from "@/components/WebViewWrapper";
import BackHandlerWrapper from "@/components/BackHandler";
@ -69,11 +68,9 @@ export default function RootLayout() {
<PaperProvider>
<LocationTestWrapper>
<RefreshProvider queryClient={queryClient}>
<CentralStoreInitializer>
<BackHandlerWrapper />
<Stack screenOptions={{ headerShown: false }} />
<AddToCartDialog />
</CentralStoreInitializer>
</RefreshProvider>
</LocationTestWrapper>
</PaperProvider>

View file

@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
import { View, Dimensions, Image, ScrollView, NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
import { MyTouchableOpacity, MyText, tw } from 'common-ui';
import { useRouter } from 'expo-router';
import { useBanners } from '@/src/hooks/prominent-api-hooks';
import { trpc } from '@/src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
const { width: screenWidth } = Dimensions.get('window');
@ -25,7 +25,7 @@ export default function BannerCarousel() {
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
// Fetch banners data
const { data: bannersData, isLoading, error } = useBanners();
const { data: bannersData, isLoading, error } = trpc.user.banner.getBanners.useQuery();
const banners = bannersData?.banners || [];
@ -123,7 +123,7 @@ export default function BannerCarousel() {
{/* Pagination Dots */}
{banners.length > 1 && (
<View style={tw`flex-row justify-center mt-3`}>
{banners.map((_: Banner, index: number) => (
{banners.map((_, index: number) => (
<MyTouchableOpacity
key={index}
onPress={() => goToSlide(index)}

View file

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { View, ActivityIndicator, Platform } from 'react-native';
import { tw, theme, MyText, MyTouchableOpacity , BottomDialog } from 'common-ui';
import { trpc, trpcClient } from '@/src/trpc-client';
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api';
import Constants from 'expo-constants';
import * as Linking from 'expo-linking';

View file

@ -8,7 +8,7 @@ import dayjs from 'dayjs';
import { trpc } from '@/src/trpc-client';
import { Image } from 'expo-image';
import { orderStatusManipulator } from '@/src/lib/string-manipulators';
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api';
interface OrderItem {
productName: string;

View file

@ -6,8 +6,6 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
// import RazorpayCheckout from 'react-native-razorpay';
import { trpc } from '@/src/trpc-client';
import { useCentralProductStore } from '@/src/store/centralProductStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { clearLocalCart } from '@/hooks/cart-query-hooks';
import { useQueryClient } from '@tanstack/react-query';
import { FontAwesome5, FontAwesome6 } from '@expo/vector-icons';
@ -56,19 +54,17 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
};
const products = useCentralProductStore((state) => state.products);
const productsById = useCentralProductStore((state) => state.productsById);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
// Memoized flash-eligible product IDs
const flashEligibleProductIds = useMemo(() => {
if (!products.length) return new Set<number>();
if (!productsData?.products) return new Set<number>();
return new Set(
products
.filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
.map((product) => product.id)
productsData.products
.filter((product: any) => product.isFlashAvailable)
.map((product: any) => product.id)
);
}, [products, productSlotsMap]);
}, [productsData]);
const placeOrderMutation = trpc.user.order.placeOrder.useMutation({
onSuccess: (data) => {
@ -130,7 +126,7 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
const availableItems = cartItems
.filter(item => {
if (productSlotsMap[item.productId]?.isOutOfStock) return false;
if (item.product?.isOutOfStock) return false;
// For flash delivery, check if product supports flash delivery
if (isFlashDelivery) {
return flashEligibleProductIds.has(item.productId);

View file

@ -1,5 +1,5 @@
import React from 'react';
import { View, Alert, ActivityIndicator } from 'react-native';
import React, { useMemo } from 'react';
import { View, Alert, TouchableOpacity, Text } from 'react-native';
import { Image } from 'expo-image';
import { tw, theme, MyText, MyTouchableOpacity, Quantifier, MiniQuantifier } from 'common-ui';
import CartIcon from '@/components/icons/CartIcon';
@ -14,7 +14,7 @@ import {
} from '@/hooks/cart-query-hooks';
import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier';
import { useCartStore } from '@/src/store/cartStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { trpc } from '@/src/trpc-client';
import { Image as RnImage } from 'react-native'
@ -46,18 +46,6 @@ const ProductCard: React.FC<ProductCardProps> = ({
containerComp: ContainerComp = React.Fragment,
useAddToCartDialog = false,
}) => {
const imageUri = item.images?.[0]
const [imageStatus, setImageStatus] = React.useState<'loading' | 'loaded' | 'error'>('loading')
const [imageError, setImageError] = React.useState<string | null>(null)
const [updater, setUpdater] = React.useState(0)
React.useEffect(() => {
const intervalId = setInterval(() => {
setUpdater(prev => prev + 1)
}, 5000)
return () => clearInterval(intervalId)
}, [])
const { data: cartData } = useGetCart();
const { getQuickestSlot } = useProductSlotIdentifier();
const { setAddedToCartProduct } = useCartStore();
@ -81,41 +69,25 @@ const ProductCard: React.FC<ProductCardProps> = ({
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id);
const quantity = cartItem?.quantity || 0;
// Get slots data from central store
const slots = useCentralSlotStore((state) => state.slots);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
// Query all slots with products
const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery();
// Create slot lookup map
const slotMap = React.useMemo(() => {
const slotMap = useMemo(() => {
const map: Record<number, any> = {};
slots?.forEach((slot: any) => {
slotsData?.slots?.forEach((slot: any) => {
map[slot.id] = slot;
});
return map;
}, [slots]);
}, [slotsData]);
// Get cart item's slot delivery time if item is in cart
const cartSlot = cartItem?.slotId ? slotMap[cartItem.slotId] : null;
const displayDeliveryDate = cartSlot?.deliveryTime || item.nextDeliveryDate;
React.useEffect(() => {
if (imageUri) {
setImageStatus('loading')
setImageError(null)
return
}
setImageStatus('error')
setImageError('No image available')
}, [imageUri])
// Precompute the next slot and determine display out of stock status
const slotId = getQuickestSlot(item.id);
// Use isOutOfStock from productSlotsMap (all products now included)
const productSlotInfo = productSlotsMap[item.id];
const isOutOfStockFromSlots = productSlotInfo?.isOutOfStock;
const displayIsOutOfStock = isOutOfStockFromSlots || !slotId;
const displayIsOutOfStock = item.isOutOfStock || !slotId;
// if(item.name.startsWith('Mutton Curry Cut')) {
// console.log({slotId, displayIsOutOfStock})
@ -147,7 +119,6 @@ const ProductCard: React.FC<ProductCardProps> = ({
}
};
// console.log('rendering the product cart for id', item.id)
return (
<ContainerComp>
<MyTouchableOpacity
@ -160,32 +131,9 @@ const ProductCard: React.FC<ProductCardProps> = ({
>
<View style={tw`relative`}>
<RnImage
source={{ uri: imageUri }}
// source={{uri: 'https://pub-6bf1fbc4048a4cbaa533ddbb13bf9de6.r2.dev/product-images/1763796113884-0'}}
source={{ uri: item.images?.[0] }}
style={{ width: "100%", height: itemWidth, resizeMode: "cover" }}
onLoadStart={() => {
setImageStatus('loading')
setImageError(null)
}}
// onLoadEnd={() => {
// setImageError('loading stopped indefinitely')
//
// }}
onLoad={() => setImageStatus('loaded')}
onError={(event) => {
setImageStatus('error')
setImageError( 'Image failed to load')
}}
/>
{imageStatus === 'error' && (
<View style={tw`absolute inset-0 items-center justify-center bg-gray-100`}>
<MaterialIcons name="broken-image" size={22} color="#94A3B8" />
<MyText style={tw`text-[10px] text-gray-500 mt-1`}>
{imageError || 'Image failed to load'}
</MyText>
</View>
)}
{displayIsOutOfStock && (
<View style={tw`absolute inset-0 bg-black/40 items-center justify-center`}>
<View style={tw`bg-red-500 px-3 py-1 rounded-full`}>

View file

@ -12,11 +12,9 @@ import { trpc, trpcClient } from '@/src/trpc-client';
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier';
import { useFlashNavigationStore } from '@/components/stores/flashNavigationStore';
import { useSlots } from '@/src/hooks/prominent-api-hooks';
import FloatingCartBar from './floating-cart-bar';
import { useStoreHeaderStore } from '@/src/store/storeHeaderStore';
import { useCartStore } from '@/src/store/cartStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
const { width: screenWidth } = Dimensions.get("window");
const carouselWidth = screenWidth;
@ -59,28 +57,15 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
const { getQuickestSlot } = useProductSlotIdentifier();
const { setShouldNavigateToCart } = useFlashNavigationStore();
const { setAddedToCartProduct } = useCartStore();
const { data: slotsData } = useSlots();
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const productAvailability = useMemo(() => {
if (!productDetail) return null;
return productSlotsMap[productDetail.id];
}, [productDetail, productSlotsMap]);
const sortedDeliverySlots = useMemo(() => {
if (!slotsData?.slots || !productDetail) return []
// Filter slots that contain this product
const productSlots = slotsData.slots.filter((slot: any) =>
slot.products?.some((p: any) => p.id === productDetail.id)
)
return productSlots.sort((a: any, b: any) => {
if (!productDetail?.deliverySlots) return []
return [...productDetail.deliverySlots].sort((a, b) => {
const deliveryDiff = new Date(a.deliveryTime).getTime() - new Date(b.deliveryTime).getTime()
if (deliveryDiff !== 0) return deliveryDiff
return new Date(a.freezeTime).getTime() - new Date(b.freezeTime).getTime()
})
}, [slotsData, productDetail])
}, [productDetail?.deliverySlots])
// Find current quantity from cart data
const cartItem = productDetail ? cartData?.data?.items?.find((item: any) => item.productId === productDetail.id) : null;
@ -109,7 +94,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
const handleAddToCart = (productId: number) => {
if (isFlashDelivery) {
if (!productAvailability?.isFlashAvailable) {
if (!productDetail?.isFlashAvailable) {
Alert.alert("Error", "This product is not available for flash delivery");
return;
}
@ -128,7 +113,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
const handleBuyNow = (productId: number) => {
if (isFlashDelivery) {
if (!productAvailability?.isFlashAvailable) {
if (!productDetail?.isFlashAvailable) {
Alert.alert("Error", "This product is not available for flash delivery");
return;
}
@ -256,13 +241,13 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
<View style={tw`flex-row justify-between items-start mb-2`}>
<MyText style={tw`text-2xl font-bold text-gray-900 flex-1 mr-2`}>{productDetail.name}</MyText>
<View style={tw`flex-row gap-2`}>
{productAvailability?.isFlashAvailable && (
{productDetail.isFlashAvailable && (
<View style={tw`bg-pink-100 px-3 py-1 rounded-full flex-row items-center`}>
<MaterialIcons name="bolt" size={12} color="#EC4899" style={tw`mr-1`} />
<MyText style={tw`text-pink-700 text-xs font-bold`}>1 Hr Delivery</MyText>
</View>
)}
{productAvailability?.isOutOfStock && (
{productDetail.isOutOfStock && (
<View style={tw`bg-red-100 px-3 py-1 rounded-full`}>
<MyText style={tw`text-red-700 text-xs font-bold`}>Out of Stock</MyText>
</View>
@ -292,7 +277,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
</View>
{/* Flash price on separate line - smaller and less prominent */}
{productAvailability?.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && (
{productDetail.isFlashAvailable && productDetail.flashPrice && productDetail.flashPrice !== productDetail.price && (
<View style={tw`mt-1`}>
<MyText style={tw`text-pink-600 text-lg font-bold`}>
1 Hr Delivery: {productDetail.flashPrice} / {formatQuantity(productDetail.productQuantity || 1, productDetail.unitNotation).display}
@ -319,11 +304,11 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
// Show "Add to Cart" button when not in cart
<MyTouchableOpacity
style={[tw`flex-1 py-3.5 rounded-xl items-center border`, {
borderColor: (productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500,
borderColor: (productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500,
backgroundColor: 'white'
}]}
onPress={() => {
if (productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) {
if (productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) {
return;
}
if (isFlashDelivery) {
@ -334,10 +319,10 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
setAddedToCartProduct({ productId: productDetail.id, product: productDetail });
}
}}
disabled={productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)}
disabled={productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)}
>
<MyText style={[tw`font-bold text-base`, { color: (productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500 }]}>
{(productAvailability?.isOutOfStock || (isFlashDelivery && !productAvailability?.isFlashAvailable)) ? 'Unavailable' : 'Add to Cart'}
<MyText style={[tw`font-bold text-base`, { color: (productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) ? '#9ca3af' : theme.colors.brand500 }]}>
{(productDetail.isOutOfStock || (isFlashDelivery && !productDetail.isFlashAvailable)) ? 'Unavailable' : 'Add to Cart'}
</MyText>
</MyTouchableOpacity>
)}
@ -345,26 +330,26 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
{isFlashDelivery ? (
<MyTouchableOpacity
style={[tw`flex-1 py-3.5 rounded-xl items-center shadow-md`, {
backgroundColor: (productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable) ? '#9ca3af' : '#FDF2F8'
backgroundColor: (productDetail.isOutOfStock || !productDetail.isFlashAvailable) ? '#9ca3af' : '#FDF2F8'
}]}
onPress={() => !(productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable) && handleBuyNow(productDetail.id)}
disabled={productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable}
onPress={() => !(productDetail.isOutOfStock || !productDetail.isFlashAvailable) && handleBuyNow(productDetail.id)}
disabled={productDetail.isOutOfStock || !productDetail.isFlashAvailable}
>
<MyText style={tw`text-base font-bold ${productAvailability?.isOutOfStock || !productAvailability?.isFlashAvailable ? 'text-gray-400' : 'text-pink-600'}`}>
{productAvailability?.isOutOfStock ? 'Out of Stock' :
(!productAvailability?.isFlashAvailable ? 'Not Flash Eligible' : 'Get in 1 Hour')}
<MyText style={tw`text-base font-bold ${productDetail.isOutOfStock || !productDetail.isFlashAvailable ? 'text-gray-400' : 'text-pink-600'}`}>
{productDetail.isOutOfStock ? 'Out of Stock' :
(!productDetail.isFlashAvailable ? 'Not Flash Eligible' : 'Get in 1 Hour')}
</MyText>
</MyTouchableOpacity>
) : productAvailability?.isFlashAvailable ? (
) : productDetail.isFlashAvailable ? (
<MyTouchableOpacity
style={[tw`flex-1 py-3.5 rounded-xl items-center shadow-md`, {
backgroundColor: sortedDeliverySlots.length === 0 ? '#9ca3af' : '#FDF2F8'
backgroundColor: productDetail.deliverySlots.length === 0 ? '#9ca3af' : '#FDF2F8'
}]}
onPress={() => sortedDeliverySlots.length > 0 && handleBuyNow(productDetail.id)}
disabled={sortedDeliverySlots.length === 0}
onPress={() => productDetail.deliverySlots.length > 0 && handleBuyNow(productDetail.id)}
disabled={productDetail.deliverySlots.length === 0}
>
<MyText style={tw`text-base font-bold ${sortedDeliverySlots.length === 0 ? 'text-gray-400' : 'text-pink-600'}`}>
{sortedDeliverySlots.length === 0 ? 'No Slots' : 'Get in 1 Hour'}
<MyText style={tw`text-base font-bold ${productDetail.deliverySlots.length === 0 ? 'text-gray-400' : 'text-pink-600'}`}>
{productDetail.deliverySlots.length === 0 ? 'No Slots' : 'Get in 1 Hour'}
</MyText>
</MyTouchableOpacity>
) : (
@ -393,7 +378,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
key={index}
style={tw`flex-row items-start mb-4 bg-gray-50 p-3 rounded-xl border border-gray-100`}
onPress={() => handleSlotAddToCart(productDetail.id, slot.id)}
disabled={productAvailability?.isOutOfStock}
disabled={productDetail.isOutOfStock}
activeOpacity={0.7}
>
<MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} />
@ -605,7 +590,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
key={index}
style={tw`flex-row items-start mb-4 bg-gray-50 p-4 rounded-xl border border-gray-100`}
onPress={() => handleSlotAddToCart(productDetail.id, slot.id)}
disabled={productAvailability?.isOutOfStock}
disabled={productDetail.isOutOfStock}
activeOpacity={0.7}
>
<MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} />

View file

@ -6,7 +6,6 @@ import { BottomDialog, MyTouchableOpacity, MyText, tw, theme } from 'common-ui';
import { useAuth } from '@/src/contexts/AuthContext';
import { trpc } from '@/src/trpc-client';
import { useAddressStore } from '@/src/store/addressStore';
import { useSlots } from '@/src/hooks/prominent-api-hooks';
import dayjs from 'dayjs';
interface QuickDeliveryAddressSelectorProps {
@ -32,13 +31,13 @@ const QuickDeliveryAddressSelector: React.FC<QuickDeliveryAddressSelectorProps>
const { data: addressesData } = trpc.user.address.getUserAddresses.useQuery(undefined, {
enabled: isAuthenticated,
});
const { data: slotsData } = useSlots();
const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery();
const defaultAddress = defaultAddressData?.data;
const addresses = addressesData?.data || [];
// Format time range helper
const formatTimeRange = (deliveryTime: string | Date) => {
const formatTimeRange = (deliveryTime: string) => {
const time = dayjs(deliveryTime);
const endTime = time.add(1, 'hour');
const startPeriod = time.format('A');

View file

@ -7,11 +7,7 @@ import { useRouter, usePathname } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { tw, theme, MyText, MyTouchableOpacity, MyFlatList, AppContainer, MiniQuantifier } from 'common-ui';
import { trpc } from '@/src/trpc-client';
import { useAllProducts, useStores, useSlots } from '@/src/hooks/prominent-api-hooks';
import { AllProductsApiType } from '@backend/trpc/router';
import { useQuickDeliveryStore } from '@/src/store/quickDeliveryStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { useCentralProductStore } from '@/src/store/centralProductStore';
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
import { useHideTabNav } from '@/src/hooks/useHideTabNav';
import CartIcon from '@/components/icons/CartIcon';
@ -36,7 +32,7 @@ interface SlotLayoutProps {
function CustomDrawerContent(baseUrl: string, drawerProps: DrawerContentComponentProps, slotIdParent?: number, storeIdParent?: number) {
const router = useRouter();
const pathname = usePathname();
const { data: storesData } = useStores();
const { data: storesData } = trpc.user.stores.getStores.useQuery();
const setStoreId = useSlotStore(state => state.setStoreId);
const { slotId, storeId } = useSlotStore();
@ -183,10 +179,17 @@ export function SlotLayout({ slotId, storeId, baseUrl, isForFlashDelivery }: Slo
router.replace(`${baseUrl}?slotId=${newSlotId}` as any);
};
const slotQuery = slotId
? trpc.user.slots.getSlotById.useQuery({ slotId: Number(slotId) })
: trpc.user.slots.nextMajorDelivery.useQuery();
const deliveryTime = dayjs(slotQuery.data?.deliveryTime).format('DD MMM hh:mm A');
return (
<>
<View style={tw` w-full flex-row bg-white px-4 py-2 mb-1`}>
<QuickDeliveryAddressSelector
deliveryTime={deliveryTime}
slotId={Number(slotId)}
onSlotChange={handleSlotChange}
isForFlashDelivery={isForFlashDelivery}
@ -240,7 +243,6 @@ const CompactProductCard = ({
// Cart management for miniView
const { data: cartData } = useGetCart({}, cartType);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const updateCartItem = useUpdateCartItem({
showSuccessAlert: false,
showErrorAlert: false,
@ -254,7 +256,6 @@ const CompactProductCard = ({
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id);
const quantity = cartItem?.quantity || 0;
const isOutOfStock = productSlotsMap[item.id]?.isOutOfStock;
const handleQuantityChange = (newQuantity: number) => {
if (newQuantity === 0 && cartItem) {
@ -280,7 +281,7 @@ const CompactProductCard = ({
source={{ uri: item.images?.[0] }}
style={{ width: "100%", height: itemWidth, resizeMode: "cover" }}
/>
{isOutOfStock && (
{item.isOutOfStock && (
<View style={tw`absolute inset-0 bg-black/30 items-center justify-center`}>
<MyText style={tw`text-white text-xs font-bold`}>Out of Stock</MyText>
</View>
@ -339,20 +340,22 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
const slotId = slotIdParent;
const storeId = storeIdParent;
const storeIdNum = storeId;
// const { storeId, slotId: slotIdRaw } = useLocalSearchParams();
// const slotId = Number(slotIdRaw);
const { data: slotsData, isLoading: slotsLoading, error: slotsError } = useSlots();
const { productsById } = useCentralProductStore();
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
// Find the specific slot from cached data
const slot = slotsData?.slots?.find(s => s.id === slotId);
// const storeIdNum = storeId ? Number(storeId) : undefined;
const slotQuery = trpc.user.slots.getSlotById.useQuery({ slotId: slotId! }, { enabled: !!slotId });
const productsQuery = trpc.common.product.getAllProductsSummary.useQuery({});
const { addToCart = () => { } } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, "regular") || {};
const handleAddToCart = (productId: number) => {
setIsLoadingDialogOpen(true);
const item = filteredProducts.find((p) => p.id === productId);
const deliveryTime = slot?.deliveryTime ? dayjs(slot.deliveryTime).format('ddd, DD MMM • h:mm A') : '';
const item = filteredProducts.find((p: any) => p.id === productId);
const deliveryTime = slotQuery.data?.deliveryTime ? dayjs(slotQuery.data.deliveryTime).format('ddd, DD MMM • h:mm A') : '';
addToCart(productId, 1, slotId || 0, () => {
setIsLoadingDialogOpen(false);
if (item) {
@ -361,7 +364,7 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
});
};
if (slotsLoading) {
if (slotQuery.isLoading || (storeIdNum && productsQuery?.isLoading)) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
@ -371,7 +374,7 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
);
}
if (slotsError) {
if (slotQuery.error || (storeIdNum && productsQuery?.error)) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
@ -383,7 +386,7 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
);
}
if (!slot) {
if (!slotQuery.data) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center`}>
@ -394,16 +397,14 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
);
}
// Get product details from central store using slot product IDs
// Filter: 1) Must exist in productsById, 2) Must not be out of stock (from slots data)
const slotProducts = slot.products
?.map(p => productsById[p.id])
?.filter((product): product is NonNullable<typeof product> => product !== null && product !== undefined)
?.filter(product => !productSlotsMap[product.id]?.isOutOfStock) || [];
// Create a Set of product IDs from slot data for O(1) lookup
const slotProductIds = new Set(slotQuery.data.products?.map((p: any) => p.id) || []);
const filteredProducts = storeIdNum
? slotProducts.filter(p => p.storeId === storeIdNum)
: slotProducts;
const filteredProducts: any[] = storeIdNum
? productsQuery?.data?.products?.filter(p =>
p.storeId === storeIdNum && slotProductIds.has(p.id)
) || []
: slotQuery.data.products;
return (
<View testID="slot-detail-page" style={tw`flex-1`}>
@ -421,7 +422,7 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
keyExtractor={(item, index) => index.toString()}
columnWrapperStyle={{ gap: 16, justifyContent: 'flex-start' }}
contentContainerStyle={[tw`pb-24 px-4`, { gap: 16 }]}
onRefresh={() => {}}
onRefresh={() => slotQuery.refetch()}
ListEmptyComponent={
storeIdNum ? (
<View style={tw`items-center justify-center py-10`}>
@ -447,8 +448,7 @@ export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProduc
const storeId = storeIdParent;
const storeIdNum = storeId;
const productsQuery = useAllProducts();
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const productsQuery = trpc.common.product.getAllProductsSummary.useQuery({});
const { addToCart = () => { } } = useAddToCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, "flash") || {};
@ -486,22 +486,20 @@ export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProduc
}
// Filter products to only include those eligible for flash delivery
let flashProducts: AllProductsApiType['products'][number][] = [];
let flashProducts: any[] = [];
if (storeIdNum) {
// Filter by store, flash availability, and stock status
flashProducts = productsQuery?.data?.products?.filter(p => {
const productInfo = productSlotsMap[p.id];
return p.storeId === storeIdNum &&
productInfo?.isFlashAvailable &&
!productInfo?.isOutOfStock;
}) || [];
flashProducts = productsQuery?.data?.products?.filter(p =>
p.storeId === storeIdNum &&
p.isFlashAvailable &&
!p.isOutOfStock
) || [];
} else {
// Show all flash-available products that are in stock
flashProducts = productsQuery?.data?.products?.filter(p => {
const productInfo = productSlotsMap[p.id];
return productInfo?.isFlashAvailable &&
!productInfo?.isOutOfStock;
}) || [];
flashProducts = productsQuery?.data?.products?.filter(p =>
p.isFlashAvailable &&
!p.isOutOfStock
) || [];
}
return (

View file

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { View, ActivityIndicator } from 'react-native';
import { WebView } from 'react-native-webview';
import { trpc } from '@/src/trpc-client';
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api';
import { theme, MyText, MyTouchableOpacity } from 'common-ui';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';

View file

@ -24,10 +24,8 @@ import TestingPhaseNote from "@/components/TestingPhaseNote";
import dayjs from "dayjs";
import { trpc } from "@/src/trpc-client";
import { useCentralProductStore } from '@/src/store/centralProductStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api';
interface CartPageProps {
isFlashDelivery?: boolean;
@ -82,34 +80,33 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
const { data: couponsRaw, error: couponsError } = trpc.user.coupon.getEligible.useQuery();
const { data: constsData } = useGetEssentialConsts();
const products = useCentralProductStore((state) => state.products);
const productsById = useCentralProductStore((state) => state.productsById);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
const cartItems = cartData?.items || [];
// Memoized flash-eligible product IDs
const flashEligibleProductIds = useMemo(() => {
if (!products.length) return new Set<number>();
if (!productsData?.products) return new Set<number>();
return new Set(
products
.filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
.map((product) => product.id)
productsData.products
.filter((product: any) => product.isFlashAvailable)
.map((product: any) => product.id)
);
}, [products, productSlotsMap]);
}, [productsData]);
// Base total price without discounts for coupon eligibility check
const baseTotalPrice = useMemo(
() =>
cartItems
.filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
.reduce((sum, item) => {
const product = productsById[item.productId];
const price = product?.price || 0;
return sum + price * (quantities[item.id] || item.quantity);
}, 0),
[cartItems, quantities, productsById]
.filter((item) => !item.product?.isOutOfStock)
.reduce(
(sum, item) =>
sum +
(item.product?.price || 0) * (quantities[item.id] || item.quantity),
0
),
[cartItems, quantities]
);
const eligibleCoupons = useMemo(() => {
@ -203,11 +200,10 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
);
const totalPrice = cartItems
.filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
.filter((item) => !item.product?.isOutOfStock)
.reduce((sum, item) => {
const product = productsById[item.productId];
const quantity = quantities[item.id] || item.quantity;
const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0);
const price = isFlashDelivery ? (item.product?.flashPrice ?? item.product?.price ?? 0) : (item.product?.price || 0);
return sum + price * quantity;
}, 0);
const dropdownData = useMemo(
@ -277,7 +273,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
const finalTotalWithDelivery = finalTotal + deliveryCharge;
const hasAvailableItems = cartItems.some(item => !productSlotsMap[item.productId]?.isOutOfStock);
const hasAvailableItems = cartItems.some(item => !item.product?.isOutOfStock);
useEffect(() => {
const initial: Record<number, number> = {};
@ -414,12 +410,10 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
const productSlots = getAvailableSlotsForProduct(item.productId);
const selectedSlotForItem = selectedSlots[item.id];
const isFlashEligible = isFlashDelivery ? flashEligibleProductIds.has(item.productId) : true;
const product = productsById[item.productId];
const productSlotInfo = productSlotsMap[item.productId];
// const isAvailable = (productSlots.length > 0 || isFlashDelivery) && !item.product?.isOutOfStock && isFlashEligible;
let isAvailable = true;
if (productSlotInfo?.isOutOfStock) {
if(item.product?.isOutOfStock) {
isAvailable = false;
} else if(isFlashDelivery) {
if(!isFlashEligible) {
@ -436,7 +430,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
// isAvailable = isFlashEligible;
// }
const quantity = quantities[item.id] || item.quantity;
const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0);
const price = isFlashDelivery ? (item.product?.flashPrice ?? item.product?.price ?? 0) : (item.product?.price || 0);
const itemPrice = price * quantity;
return (
@ -444,7 +438,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
<View style={tw`p-4`}>
<View style={tw`flex-row items-center mb-2`}>
<Image
source={{ uri: product?.images?.[0] }}
source={{ uri: item.product.images?.[0] }}
style={tw`w-8 h-8 rounded-lg bg-gray-100 mr-3`}
/>
@ -452,12 +446,12 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
style={tw`text-sm text-gray-900 flex-1 mr-3`}
numberOfLines={2}
>
{product?.name}
{item.product.name}
</MyText>
<MyText style={tw`text-xs text-gray-500 mr-2`}>
{(() => {
const qty = product?.productQuantity || 1;
const unit = product?.unitNotation || '';
const qty = item.product?.productQuantity || 1;
const unit = item.product?.unitNotation || '';
if (unit?.toLowerCase() === 'kg' && qty < 1) {
return `${Math.round(qty * 1000)}g`;
}
@ -518,8 +512,8 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
});
}
}}
step={product?.incrementStep}
unit={product?.unitNotation}
step={item.product.incrementStep}
unit={item.product?.unitNotation}
/>
</View>
</View>
@ -585,7 +579,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
onPress={() => {
Alert.alert(
"Remove Item",
`Remove ${product?.name} from cart?`,
`Remove ${item.product.name} from cart?`,
[
{ text: "Cancel", style: "cancel" },
{
@ -636,7 +630,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
onPress={() => {
Alert.alert(
"Remove Item",
`Remove ${product?.name} from cart?`,
`Remove ${item.product.name} from cart?`,
[
{ text: "Cancel", style: "cancel" },
{
@ -680,7 +674,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
style={tw`bg-red-50 self-start px-2 py-1 rounded-md mt-2`}
>
<MyText style={tw`text-xs font-bold text-red-600`}>
{productSlotInfo?.isOutOfStock
{item.product?.isOutOfStock
? "Out of Stock"
: isFlashDelivery && !flashEligibleProductIds.has(item.productId)
? "Not available for flash delivery. Please remove"
@ -914,7 +908,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
onPress={() => {
const availableItems = cartItems
.filter(item => {
if (productSlotsMap[item.productId]?.isOutOfStock) return false;
if (item.product?.isOutOfStock) return false;
if (isFlashDelivery) {
// Check if product supports flash delivery
return flashEligibleProductIds.has(item.productId);
@ -923,10 +917,12 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
})
.map(item => item.id);
if (availableItems.length === 0) {
// Determine why no items are available
const outOfStockItems = cartItems.filter(item => productSlotsMap[item.productId]?.isOutOfStock);
const inStockItems = cartItems.filter(item => !productSlotsMap[item.productId]?.isOutOfStock);
const outOfStockItems = cartItems.filter(item => item.product?.isOutOfStock);
const inStockItems = cartItems.filter(item => !item.product?.isOutOfStock);
let errorTitle = "Cannot Proceed";
let errorMessage = "";
@ -965,7 +961,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
// Check if there are items without slots (for regular delivery)
if (!isFlashDelivery && availableItems.length < cartItems.length) {
const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !productSlotsMap[item.productId]?.isOutOfStock);
const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !item.product?.isOutOfStock);
if (itemsWithoutSlots.length > 0) {
Alert.alert(
"Delivery Slot Required",

View file

@ -8,10 +8,8 @@ import AddressForm from '@/src/components/AddressForm';
import { useAuthenticatedRoute } from '@/hooks/useAuthenticatedRoute';
import { trpc } from '@/src/trpc-client';
import { useCentralProductStore } from '@/src/store/centralProductStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { useGetCart } from '@/hooks/cart-query-hooks';
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api';
import PaymentAndOrderComponent from '@/components/PaymentAndOrderComponent';
import CheckoutAddressSelector from '@/components/CheckoutAddressSelector';
import { useAddressStore } from '@/src/store/addressStore';
@ -37,9 +35,7 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
const { data: addresses, refetch: refetchAddresses } = trpc.user.address.getUserAddresses.useQuery();
const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlots.useQuery();
const { data: constsData } = useGetEssentialConsts();
const products = useCentralProductStore((state) => state.products);
const productsById = useCentralProductStore((state) => state.productsById);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
useMarkDataFetchers(() => {
refetchCart();
@ -57,13 +53,13 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
// Memoized flash-eligible product IDs
const flashEligibleProductIds = useMemo(() => {
if (!products.length) return new Set<number>();
if (!productsData?.products) return new Set<number>();
return new Set(
products
.filter((product) => productSlotsMap[product.id]?.isFlashAvailable)
.map((product) => product.id)
productsData.products
.filter((product: any) => product.isFlashAvailable)
.map((product: any) => product.id)
);
}, [products, productSlotsMap]);
}, [productsData]);
// Parse slots parameter from URL (format: "1:1,2,3;2:4,5")
const selectedSlots = useMemo(() => {
@ -127,11 +123,10 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
const totalPrice = selectedItems
.filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
.filter((item) => !item.product?.isOutOfStock)
.reduce(
(sum, item) => {
const product = productsById[item.productId];
const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0);
const price = isFlashDelivery ? (item.product?.flashPrice ?? item.product?.price ?? 0) : (item.product?.price || 0);
return sum + price * item.quantity;
},
0

View file

@ -14,6 +14,7 @@ import {
theme,
updateStatusBarColor,
} from "common-ui";
import { trpc } from "@/src/trpc-client";
import {
useGetCart,
useUpdateCartItem,
@ -21,9 +22,8 @@ import {
useAddToCart,
type CartType,
} from "@/hooks/cart-query-hooks";
import { useGetEssentialConsts, useSlots } from "@/src/hooks/prominent-api-hooks"
import { useGetEssentialConsts } from "@/src/api-hooks/essential-consts.api";
import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
import { useCentralProductStore } from "@/src/store/centralProductStore";
import dayjs from "dayjs";
import { LinearGradient } from "expo-linear-gradient";
@ -36,7 +36,7 @@ interface FloatingCartBarProps {
}
// Smart time window formatting function
const formatTimeRange = (deliveryTime: string | Date) => {
const formatTimeRange = (deliveryTime: string) => {
const time = dayjs(deliveryTime);
const endTime = time.add(1, 'hour');
const startPeriod = time.format('A');
@ -79,8 +79,7 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
const setIsExpanded = controlledSetIsExpanded ?? setLocalIsExpanded;
const { data: cartData, refetch: refetchCart } = useGetCart({}, cartType);
const { data: constsData } = useGetEssentialConsts();
const { data: slotsData } = useSlots();
const productsById = useCentralProductStore((state) => state.productsById);
const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery();
const { productSlotsMap } = useProductSlotIdentifier();
const cartItems = cartData?.items || [];
const itemCount = cartItems.length;
@ -115,15 +114,15 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
const itemsToUpdate = cartItems.filter(item => {
if (isFlashDelivery || !item.slotId) return false;
const availableSlots = productSlotsMap[item.productId]?.slots || [];
const isSlotAvailable = availableSlots.some((slot) => slot.id === item.slotId);
const availableSlots = productSlotsMap.get(item.productId) || [];
const isSlotAvailable = availableSlots.includes(item.slotId);
return !isSlotAvailable;
});
itemsToUpdate.forEach((item) => {
const availableSlots = productSlotsMap[item.productId]?.slots || [];
const availableSlots = productSlotsMap.get(item.productId) || [];
if (availableSlots.length > 0 && !isFlashDelivery) {
const nearestSlotId = availableSlots[0].id;
const nearestSlotId = availableSlots[0];
removeFromCart.mutate({ itemId: item.id });
addToCartHook.addToCart(item.productId, item.quantity, nearestSlotId);
}
@ -136,9 +135,7 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
// Calculate total cart value and free delivery info
const totalCartValue = cartItems.reduce(
(sum, item) => {
const product = productsById[item.productId];
const basePrice = product?.price ?? 0;
const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice;
const price = isFlashDelivery ? (item.product.flashPrice ?? item.product.price) : item.product.price;
return sum + price * item.quantity;
},
0
@ -260,16 +257,16 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
<View style={tw`py-4`}>
<View style={tw`flex-row items-center`}>
<Image
source={{ uri: productsById[item.productId]?.images?.[0] }}
source={{ uri: item.product.images?.[0] }}
style={tw`w-8 h-8 rounded-lg bg-slate-50 border border-slate-100`}
/>
<View style={tw`flex-1 ml-4`}>
<View style={tw`flex-row items-center justify-between mb-1`}>
<ProductNameWithQuantity
name={productsById[item.productId]?.name || ''}
productQuantity={productsById[item.productId]?.productQuantity || 0}
unitNotation={productsById[item.productId]?.unitNotation || ''}
name={item.product.name}
productQuantity={item.product.productQuantity}
unitNotation={item.product.unitNotation}
/>
<MiniQuantifier
value={quantities[item.id] || item.quantity}
@ -281,20 +278,21 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
updateCartItem.mutate({ itemId: item.id, quantity: value });
}
}}
step={productsById[item.productId]?.incrementStep || 1}
step={item.product.incrementStep}
showUnits={true}
unit={productsById[item.productId]?.unitNotation}
unit={item.product?.unitNotation}
/>
</View>
<View style={tw`flex-row items-center justify-between`}>
{item.slotId && slotsData && productSlotsMap[item.productId] && (
{item.slotId && slotsData && productSlotsMap.has(item.productId) && (
<BottomDropdown
label="Select Delivery Slot"
value={item.slotId}
options={(productSlotsMap[item.productId]?.slots || []).map((slot) => {
options={(productSlotsMap.get(item.productId) || []).map(slotId => {
const slot = slotsData.slots.find(s => s.id === slotId);
return {
label: slot ? formatTimeRange(slot.deliveryTime) : "N/A",
value: slot.id,
value: slotId,
};
})}
onValueChange={async (val) => {
@ -327,12 +325,7 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
/>
)}
<MyText style={tw`text-slate-900 text-sm font-bold`}>
{(() => {
const product = productsById[item.productId];
const basePrice = product?.price ?? 0;
const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice;
return price * item.quantity;
})()}
{(isFlashDelivery ? (item.product.flashPrice ?? item.product.price) : item.product.price) * item.quantity}
</MyText>
</View>
</View>

View file

@ -1,12 +1,15 @@
import { useAllProducts } from '@/src/hooks/prominent-api-hooks';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { trpc } from '@/src/trpc-client';
import { Alert } from 'react-native';
import { useQuery, useMutation, useQueryClient, UseQueryResult, UseMutationResult } from '@tanstack/react-query';
import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { StorageServiceCasual } from 'common-ui/src/services/StorageServiceCasual';
// Cart type definition
export type CartType = "regular" | "flash";
// const CART_MODE: 'remote' | 'local' = 'remote';
const CART_MODE: 'remote' | 'local' = 'local';
const getCartStorageKey = (cartType: CartType = "regular"): string => {
return cartType === "flash" ? "flash_cart_items" : "cart_items";
};
@ -23,99 +26,15 @@ interface ProductSummary {
id: number;
price: string;
incrementStep: number;
isOutOfStock: boolean;
isFlashAvailable: boolean;
name?: string;
flashPrice?: string | null;
images?: string[];
productQuantity?: number;
unitNotation?: string;
marketPrice?: string | null;
}
export interface CartItem {
interface CartItem {
id: number;
productId: number;
quantity: number;
addedAt: string;
product: ProductSummary;
subtotal: number;
slotId: number;
}
interface CartData {
items: CartItem[];
totalItems: number;
totalAmount: number;
}
interface UseGetCartOptions {
refetchOnWindowFocus?: boolean;
enabled?: boolean;
}
interface UseGetCartReturn {
data: CartData | undefined;
isLoading: boolean;
error: Error | null;
refetch: () => Promise<UseQueryResult<CartData, Error>>;
cartItems: CartItem[];
totalItems: number;
totalPrice: number;
isEmpty: boolean;
hasItems: boolean;
}
interface AddToCartVariables {
productId: number;
quantity: number;
slotId: number;
}
interface UpdateCartVariables {
itemId: number;
quantity: number;
}
interface RemoveCartVariables {
itemId: number;
}
interface MutationOptions<TData, TVariables> {
onSuccess?: (data: TData, variables: TVariables) => void;
onError?: (error: Error) => void;
showSuccessAlert?: boolean;
showErrorAlert?: boolean;
refetchCart?: boolean;
}
interface UseAddToCartReturn {
mutate: UseMutationResult<LocalCartItem[], Error, AddToCartVariables>['mutate'];
mutateAsync: UseMutationResult<LocalCartItem[], Error, AddToCartVariables>['mutateAsync'];
isLoading: boolean;
error: Error | null;
data: LocalCartItem[] | undefined;
addToCart: (productId: number, quantity?: number, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void) => void;
addToCartAsync: (productId: number, quantity?: number, slotId?: number) => Promise<LocalCartItem[]>;
}
interface UseUpdateCartItemReturn {
mutate: UseMutationResult<LocalCartItem[], Error, UpdateCartVariables>['mutate'];
mutateAsync: UseMutationResult<LocalCartItem[], Error, UpdateCartVariables>['mutateAsync'];
isLoading: boolean;
error: Error | null;
data: LocalCartItem[] | undefined;
updateCartItem: (itemId: number, quantity: number) => void;
updateCartItemAsync: (itemId: number, quantity: number) => Promise<LocalCartItem[]>;
}
interface UseRemoveFromCartReturn {
mutate: UseMutationResult<LocalCartItem[], Error, RemoveCartVariables>['mutate'];
mutateAsync: UseMutationResult<LocalCartItem[], Error, RemoveCartVariables>['mutateAsync'];
isLoading: boolean;
error: Error | null;
data: LocalCartItem[] | undefined;
removeFromCart: (itemId: number) => void;
removeFromCartAsync: (itemId: number) => Promise<LocalCartItem[]>;
}
const getLocalCart = async (cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
@ -127,7 +46,8 @@ const getLocalCart = async (cartType: CartType = "regular"): Promise<LocalCartIt
const saveLocalCart = async (items: LocalCartItem[], cartType: CartType = "regular"): Promise<void> => {
const key = getCartStorageKey(cartType);
await StorageServiceCasual.setItem(key, JSON.stringify(items));
await getLocalCart(cartType);
const fetchedItems = await getLocalCart(cartType);
};
const getNextCartItemId = (items: LocalCartItem[]): number => {
@ -135,7 +55,8 @@ const getNextCartItemId = (items: LocalCartItem[]): number => {
return maxId + 1;
};
const addToLocalCart = async (productId: number, quantity: number, slotId: number | undefined, cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
const addToLocalCart = async (productId: number, quantity: number, slotId?: number, cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
const items = await getLocalCart(cartType);
const existingIndex = items.findIndex(item => item.productId === productId);
@ -146,13 +67,13 @@ const addToLocalCart = async (productId: number, quantity: number, slotId: numbe
}
} else {
const newId = getNextCartItemId(items);
const cartItem: LocalCartItem = {
const cartItem = {
id: newId,
productId,
quantity,
slotId: slotId ?? 0,
slotId: slotId ?? 0, // Default to 0 if not provided
addedAt: new Date().toISOString(),
};
}
items.push(cartItem);
}
@ -183,50 +104,68 @@ const clearLocalCart = async (cartType: CartType = "regular"): Promise<void> =>
await StorageServiceCasual.setItem(key, JSON.stringify([]));
};
export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType = "regular"): UseGetCartReturn {
const { data: products } = useAllProducts();
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
export function useGetCart(options?: {
refetchOnWindowFocus?: boolean;
enabled?: boolean;
}, cartType: CartType = "regular") {
if (CART_MODE === 'remote') {
const query = trpc.user.cart.getCart.useQuery(undefined, {
refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true,
enabled: options?.enabled ?? true,
...options
});
const query: UseQueryResult<CartData, Error> = useQuery({
return {
// Original tRPC returns
data: query.data,
isLoading: query.isLoading,
error: query.error,
refetch: query.refetch,
// Computed properties
cartItems: query.data?.items || [],
totalItems: query.data?.totalItems || 0,
totalPrice: query.data?.totalAmount || 0,
// Helper methods
isEmpty: !query.data?.items?.length,
hasItems: Boolean(query.data?.items?.length),
};
} else {
const { data: products } = trpc.common.product.getAllProductsSummary.useQuery({});
const query = useQuery({
queryKey: [`local-cart-${cartType}`],
queryFn: async (): Promise<CartData> => {
queryFn: async () => {
const cartItems = await getLocalCart(cartType);
const productMap: Record<number, Omit<ProductSummary, 'isOutOfStock' | 'isFlashAvailable'>> = Object.fromEntries(
const productMap = Object.fromEntries(
products?.products?.map((p) => [
p.id,
{
id: p.id,
...p,
price: String(p.price),
incrementStep: p.incrementStep,
marketPrice: p.marketPrice === null || p.marketPrice === undefined ? null : String(p.marketPrice),
name: p.name,
flashPrice: p.flashPrice,
images: p.images,
productQuantity: p.productQuantity,
unitNotation: p.unitNotation,
},
]) ?? []
} as ProductSummary,
]) || []
);
const items: CartItem[] = cartItems
.map((cartItem): CartItem | null => {
const productBasic = productMap[cartItem.productId];
const productAvailability = productSlotsMap[cartItem.productId];
if (!productBasic || !productAvailability) return null;
const items: CartItem[] = cartItems.map(cartItem => {
const product = productMap[cartItem.productId];
if (!product) return null as any;
return {
id: cartItem.id,
productId: cartItem.productId,
quantity: cartItem.quantity,
addedAt: cartItem.addedAt,
subtotal: Number(productBasic.price) * cartItem.quantity,
product,
incrementStep: product.incrementStep,
subtotal: Number(product.price) * cartItem.quantity,
slotId: cartItem.slotId,
};
})
.filter((item): item is CartItem => item !== null);
}).filter(Boolean) as CartItem[];
const totalAmount = items.reduce((sum, item) => sum + item.subtotal, 0);
return {
@ -244,29 +183,110 @@ export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType =
isLoading: query.isLoading,
error: query.error,
refetch: query.refetch,
cartItems: query.data?.items ?? [],
totalItems: query.data?.totalItems ?? 0,
totalPrice: query.data?.totalAmount ?? 0,
isEmpty: !(query.data?.items?.length ?? 0),
// Computed properties
cartItems: query.data?.items || [],
totalItems: query.data?.totalItems || 0,
totalPrice: query.data?.totalAmount || 0,
// Helper methods
isEmpty: !query.data?.items?.length,
hasItems: Boolean(query.data?.items?.length),
};
}
}
interface UseAddToCartReturn {
mutate: any;
mutateAsync: any;
isLoading: boolean;
error: any;
data: any;
addToCart: (productId: number, quantity?: number, slotId?: number, onSettled?: (data: any, error: any) => void) => void;
addToCartAsync: (productId: number, quantity?: number, slotId?: number) => Promise<any>;
}
export function useAddToCart(options?: {
onSuccess?: (data: any, variables: any) => void;
onError?: (error: any) => void;
showSuccessAlert?: boolean;
showErrorAlert?: boolean;
refetchCart?: boolean;
}, cartType: CartType = "regular"): UseAddToCartReturn {
if (CART_MODE === 'remote') {
const utils = trpc.useUtils();
const mutation = trpc.user.cart.addToCart.useMutation({
onSuccess: (data, variables) => {
// Default success handling
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Item added to cart!");
}
// Auto-refetch cart if requested
if (options?.refetchCart) {
utils.user.cart.getCart.invalidate();
}
// Custom success callback
options?.onSuccess?.(data, variables);
},
onError: (error) => {
// Default error handling
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to add item to cart");
}
// Custom error callback
options?.onError?.(error);
},
}) as any;
const addToCart = (productId: number, quantity = 1, slotId?: number, onSettled?: (data: any, error: any) => void) => {
if (slotId == null) {
throw new Error('slotId is required for adding to cart');
}
return mutation.mutate({ productId, quantity, slotId }, {
onSettled: (data: any, error: any) => {
onSettled?.(data, error);
}
});
};
return {
// Original mutation returns
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
addToCart,
addToCartAsync: (productId: number, quantity = 1, slotId?: number) => {
if (slotId == null) {
throw new Error('slotId is required for adding to cart');
}
return mutation.mutateAsync({ productId, quantity, slotId });
},
};
} else {
export function useAddToCart(options: MutationOptions<LocalCartItem[], AddToCartVariables> = {}, cartType: CartType = "regular"): UseAddToCartReturn {
const queryClient = useQueryClient();
const mutation: UseMutationResult<LocalCartItem[], Error, AddToCartVariables> = useMutation({
mutationFn: async ({ productId, quantity, slotId }: AddToCartVariables): Promise<LocalCartItem[]> => {
const mutation = useMutation({
mutationFn: async ({ productId, quantity, slotId }: { productId: number, quantity: number, slotId: number }) => {
return await addToLocalCart(productId, quantity, slotId, cartType);
},
onSuccess: (data: LocalCartItem[], variables: AddToCartVariables) => {
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Item added to cart!");
}
options?.onSuccess?.(data, variables);
},
onError: (error: Error) => {
onError: (error) => {
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to add item to cart");
}
@ -274,12 +294,13 @@ export function useAddToCart(options: MutationOptions<LocalCartItem[], AddToCart
},
});
const addToCart = (productId: number, quantity = 1, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void): void => {
const addToCart = (productId: number, quantity = 1, slotId?: number, onSettled?: (data: any, error: any) => void) => {
if (slotId == null) {
throw new Error('slotId is required for adding to cart');
}
mutation.mutate({ productId, quantity, slotId }, {
onSettled: (data: LocalCartItem[] | undefined, error: Error | null) => {
return mutation.mutate({ productId, quantity, slotId }, {
onSettled: (data: any, error: any) => {
onSettled?.(data, error);
}
});
@ -292,7 +313,7 @@ export function useAddToCart(options: MutationOptions<LocalCartItem[], AddToCart
error: mutation.error,
data: mutation.data,
addToCart,
addToCartAsync: (productId: number, quantity = 1, slotId?: number): Promise<LocalCartItem[]> => {
addToCartAsync: (productId: number, quantity = 1, slotId?: number) => {
if (slotId == null) {
throw new Error('slotId is required for adding to cart');
}
@ -300,22 +321,74 @@ export function useAddToCart(options: MutationOptions<LocalCartItem[], AddToCart
},
};
}
}
export function useUpdateCartItem(options: MutationOptions<LocalCartItem[], UpdateCartVariables> = {}, cartType: CartType = "regular"): UseUpdateCartItemReturn {
export function useUpdateCartItem(options?: {
onSuccess?: (data: any, variables: any) => void;
onError?: (error: any) => void;
showSuccessAlert?: boolean;
showErrorAlert?: boolean;
refetchCart?: boolean;
}, cartType: CartType = "regular") {
if (CART_MODE === 'remote') {
const utils = trpc.useUtils();
const mutation = trpc.user.cart.updateCartItem.useMutation({
onSuccess: (data, variables) => {
// Default success handling
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Cart item updated!");
}
// Auto-refetch cart if requested
if (options?.refetchCart) {
utils.user.cart.getCart.invalidate();
}
// Custom success callback
options?.onSuccess?.(data, variables);
},
onError: (error) => {
// Default error handling
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to update cart item");
}
// Custom error callback
options?.onError?.(error);
},
});
return {
// Original mutation returns
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
// Helper methods
updateCartItem: (itemId: number, quantity: number) =>
mutation.mutate({ itemId, quantity }),
updateCartItemAsync: (itemId: number, quantity: number) =>
mutation.mutateAsync({ itemId, quantity }),
};
} else {
const queryClient = useQueryClient();
const mutation: UseMutationResult<LocalCartItem[], Error, UpdateCartVariables> = useMutation({
mutationFn: async ({ itemId, quantity }: UpdateCartVariables): Promise<LocalCartItem[]> => {
const mutation = useMutation({
mutationFn: async ({ itemId, quantity }: { itemId: number, quantity: number }) => {
return await updateLocalCartItem(itemId, quantity, cartType);
},
onSuccess: (data: LocalCartItem[], variables: UpdateCartVariables) => {
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Cart item updated!");
}
options?.onSuccess?.(data, variables);
},
onError: (error: Error) => {
onError: (error) => {
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to update cart item");
}
@ -329,28 +402,82 @@ export function useUpdateCartItem(options: MutationOptions<LocalCartItem[], Upda
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
updateCartItem: (itemId: number, quantity: number): void =>
updateCartItem: (itemId: number, quantity: number) =>
mutation.mutate({ itemId, quantity }),
updateCartItemAsync: (itemId: number, quantity: number): Promise<LocalCartItem[]> =>
updateCartItemAsync: (itemId: number, quantity: number) =>
mutation.mutateAsync({ itemId, quantity }),
};
}
}
export function useRemoveFromCart(options: MutationOptions<LocalCartItem[], RemoveCartVariables> = {}, cartType: CartType = "regular"): UseRemoveFromCartReturn {
export function useRemoveFromCart(options?: {
onSuccess?: (data: any, variables: any) => void;
onError?: (error: any) => void;
showSuccessAlert?: boolean;
showErrorAlert?: boolean;
refetchCart?: boolean;
}, cartType: CartType = "regular") {
if (CART_MODE === 'remote') {
const utils = trpc.useUtils();
const mutation = trpc.user.cart.removeFromCart.useMutation({
onSuccess: (data, variables) => {
// Default success handling
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Item removed from cart!");
}
// Auto-refetch cart if requested
if (options?.refetchCart) {
utils.user.cart.getCart.invalidate();
}
// Custom success callback
options?.onSuccess?.(data, variables);
},
onError: (error) => {
// Default error handling
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to remove item from cart");
}
// Custom error callback
options?.onError?.(error);
},
});
return {
// Original mutation returns
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
// Helper methods
removeFromCart: (itemId: number) =>
mutation.mutate({ itemId }),
removeFromCartAsync: (itemId: number) =>
mutation.mutateAsync({ itemId }),
};
} else {
const queryClient = useQueryClient();
const mutation: UseMutationResult<LocalCartItem[], Error, RemoveCartVariables> = useMutation({
mutationFn: async ({ itemId }: RemoveCartVariables): Promise<LocalCartItem[]> => {
const mutation = useMutation({
mutationFn: async ({ itemId }: { itemId: number }) => {
return await removeFromLocalCart(itemId, cartType);
},
onSuccess: (data: LocalCartItem[], variables: RemoveCartVariables) => {
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Item removed from cart!");
}
options?.onSuccess?.(data, variables);
},
onError: (error: Error) => {
onError: (error) => {
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to remove item from cart");
}
@ -364,12 +491,15 @@ export function useRemoveFromCart(options: MutationOptions<LocalCartItem[], Remo
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
removeFromCart: (itemId: number): void =>
removeFromCart: (itemId: number) =>
mutation.mutate({ itemId }),
removeFromCartAsync: (itemId: number): Promise<LocalCartItem[]> =>
removeFromCartAsync: (itemId: number) =>
mutation.mutateAsync({ itemId }),
};
}
}
// Export clear cart function for direct use
export { clearLocalCart };

View file

@ -1,28 +1,46 @@
import { trpc } from '@/src/trpc-client';
import dayjs from 'dayjs';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
export function useProductSlotIdentifier() {
// Get slots data from central store
const slots = useCentralSlotStore((state) => state.slots);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
// Fetch all slots with products
const { data: slotsData, isLoading: isProductsLoading } = trpc.user.slots.getSlotsWithProducts.useQuery();
const productSlotsMap = new Map<number, number[]>();
if (slotsData?.slots) {
const now = dayjs();
// Build map of productId to available slot IDs
slotsData.slots.forEach(slot => {
if (dayjs(slot.deliveryTime).isAfter(now)) {
slot.products.forEach(product => {
if (!productSlotsMap.has(product.id)) {
productSlotsMap.set(product.id, []);
}
productSlotsMap.get(product.id)!.push(slot.id);
});
}
});
}
const getQuickestSlot = (productId: number): number | null => {
if (!slots?.length) return null;
if (!slotsData?.slots) return null;
const now = dayjs();
const productInfo = productSlotsMap[productId];
if (!productInfo?.slots?.length) return null;
// Find slots that contain this product and have future delivery time
const availableSlots = productInfo.slots.filter((slot: any) =>
const availableSlots = slotsData.slots.filter(slot =>
slot.products.some(product => product.id === productId) &&
dayjs(slot.deliveryTime).isAfter(now)
);
// if(productId === 98)
// console.log(JSON.stringify(slotsData))
if (availableSlots.length === 0) return null;
// Return earliest slot ID (sorted by delivery time)
const earliestSlot = availableSlots.sort((a: any, b: any) =>
const earliestSlot = availableSlots.sort((a, b) =>
dayjs(a.deliveryTime).diff(dayjs(b.deliveryTime))
)[0];

View file

@ -1,20 +1,6 @@
// Learn more on how to setup config for the app: https://docs.expo.dev/guides/config-plugins/#metro-config
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');
const config = getDefaultConfig(__dirname);
// Add the packages directory to watch folders
config.watchFolders = [
...config.watchFolders || [],
path.resolve(__dirname, '../../packages/shared'),
];
// Configure module resolution for @packages/*
config.resolver.extraNodeModules = {
...config.resolver.extraNodeModules,
'@packages/shared': path.resolve(__dirname, '../../packages/shared'),
'global-shared': path.resolve(__dirname, '../../packages/shared'),
};
module.exports = config;

Binary file not shown.

View file

@ -48,7 +48,6 @@
"expo-updates": "~0.28.17",
"expo-web-browser": "~14.2.0",
"formik": "^2.4.6",
"fuse.js": "^7.1.0",
"jwt-decode": "^4.0.0",
"react": "19.0.0",
"react-dom": "19.0.0",

View file

@ -0,0 +1,8 @@
import { trpc } from '@/src/trpc-client';
export const useGetEssentialConsts = () => {
const query = trpc.common.essentialConsts.useQuery(undefined, {
refetchInterval: 60000,
});
return { ...query, refetch: query.refetch };
};

View file

@ -5,9 +5,9 @@ import { tw, BottomDialog, MyText, MyTouchableOpacity, Quantifier } from 'common
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useCartStore } from '@/src/store/cartStore';
import { useFlashCartStore } from '@/src/store/flashCartStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { trpc } from '@/src/trpc-client';
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
import { useGetEssentialConsts, useSlots } from '@/src/hooks/prominent-api-hooks';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api';
import dayjs from 'dayjs';
import { SafeAreaView } from 'react-native-safe-area-context';
@ -31,10 +31,9 @@ export default function AddToCartDialog() {
const [selectedSlotId, setSelectedSlotId] = useState<number | null>(null);
const [selectedFlashDelivery, setSelectedFlashDelivery] = useState(false);
const { data: slotsData } = useSlots();
const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery();
const { data: cartData } = useGetCart();
const { data: constsData } = useGetEssentialConsts();
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
// const isFlashDeliveryEnabled = constsData?.isFlashDeliveryEnabled === true;
const isFlashDeliveryEnabled = true;
@ -114,7 +113,7 @@ export default function AddToCartDialog() {
const isUpdate = (cartItem?.quantity || 0) >= 1;
// Check if flash delivery option should be shown
const showFlashOption = productSlotsMap[product?.id]?.isFlashAvailable === true && isFlashDeliveryEnabled;
const showFlashOption = product?.isFlashAvailable === true && isFlashDeliveryEnabled;
const handleAddToCart = () => {
if (selectedFlashDelivery) {

View file

@ -1,14 +0,0 @@
import React from 'react';
import { useInitializeCentralSlotStore } from '@/src/store/centralSlotStore';
import { useInitializeCentralProductStore } from '@/src/store/centralProductStore';
interface CentralStoreInitializerProps {
children: React.ReactNode;
}
export default function CentralStoreInitializer({ children }: CentralStoreInitializerProps) {
useInitializeCentralSlotStore();
useInitializeCentralProductStore();
return <>{children}</>;
}

View file

@ -1,125 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import axios from 'axios'
import { trpc } from '@/src/trpc-client'
import { AllProductsApiType, StoresApiType, SlotsApiType, EssentialConstsApiType, BannersApiType, StoreWithProductsApiType } from "@backend/trpc/router";
import { CACHE_FILENAMES } from "@packages/shared";
// Local useGetEssentialConsts hook
export const useGetEssentialConsts = () => {
const query = trpc.common.essentialConsts.useQuery(undefined, {
refetchInterval: 60000,
})
return { ...query, refetch: query.refetch }
}
type ProductsResponse = AllProductsApiType;
type StoresResponse = StoresApiType;
type SlotsResponse = SlotsApiType;
type EssentialConstsResponse = EssentialConstsApiType;
type BannersResponse = BannersApiType;
type StoreWithProductsResponse = StoreWithProductsApiType;
function useCacheUrl(filename: string): string | null {
const { data: essentialConsts } = useGetEssentialConsts()
const assetsDomain = essentialConsts?.assetsDomain
const apiCacheKey = essentialConsts?.apiCacheKey
return assetsDomain && apiCacheKey
? `${assetsDomain}${apiCacheKey}/${filename}`
: null
}
export function useAllProducts() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.products)
return useQuery<ProductsResponse>({
queryKey: ['all-products', cacheUrl],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<ProductsResponse>(cacheUrl)
return response.data
},
staleTime: 60000, // 1 minute
enabled: !!cacheUrl,
})
}
export function useStores() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.stores)
return useQuery<StoresResponse>({
queryKey: ['stores', cacheUrl],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<StoresResponse>(cacheUrl)
return response.data
},
staleTime: 60000, // 1 minute
enabled: !!cacheUrl,
})
}
export function useSlots() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.slots)
return useQuery<SlotsResponse>({
queryKey: ['slots', cacheUrl],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<SlotsResponse>(cacheUrl)
return response.data
},
staleTime: 60000, // 1 minute
enabled: !!cacheUrl,
})
}
export function useBanners() {
const cacheUrl = useCacheUrl(CACHE_FILENAMES.banners)
return useQuery<BannersResponse>({
queryKey: ['banners', cacheUrl],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<BannersResponse>(cacheUrl)
return response.data
},
staleTime: 60000, // 1 minute
enabled: !!cacheUrl,
})
}
export function useStoreWithProducts(storeId: number) {
const { data: essentialConsts } = useGetEssentialConsts()
const assetsDomain = essentialConsts?.assetsDomain
const apiCacheKey = essentialConsts?.apiCacheKey
const cacheUrl = assetsDomain && apiCacheKey
? `${assetsDomain}${apiCacheKey}/stores/${storeId}.json`
: null
return useQuery<StoreWithProductsResponse>({
queryKey: ['store-with-products', storeId, cacheUrl],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<StoreWithProductsResponse>(cacheUrl)
return response.data
},
staleTime: 60000, // 1 minute
enabled: !!cacheUrl,
})
}

View file

@ -1,50 +0,0 @@
import { create } from 'zustand'
import { useEffect } from 'react'
import { useAllProducts } from '@/src/hooks/prominent-api-hooks'
import { AllProductsApiType } from '@backend/trpc/router'
type Product = AllProductsApiType['products'][number]
interface CentralProductState {
products: Product[]
productsById: Record<number, Product>
refetchProducts: (() => Promise<void>) | null
setProducts: (products: Product[]) => void
clearProducts: () => void
setRefetchProducts: (refetch: () => Promise<void>) => void
}
export const useCentralProductStore = create<CentralProductState>((set) => ({
products: [],
productsById: {},
refetchProducts: null,
setProducts: (products) => {
const productsById: Record<number, Product> = {}
products.forEach((product) => {
productsById[product.id] = product
})
set({ products, productsById })
},
clearProducts: () => set({ products: [], productsById: {} }),
setRefetchProducts: (refetchProducts) => set({ refetchProducts }),
}))
export function useInitializeCentralProductStore() {
const { data: productsData, refetch } = useAllProducts()
const setProducts = useCentralProductStore((state) => state.setProducts)
const setRefetchProducts = useCentralProductStore((state) => state.setRefetchProducts)
useEffect(() => {
if (productsData?.products) {
setProducts(productsData.products)
}
}, [productsData, setProducts])
useEffect(() => {
setRefetchProducts(async () => {
await refetch()
})
}, [refetch, setRefetchProducts])
}

View file

@ -1,71 +0,0 @@
import { create } from 'zustand';
import { useSlots } from '@/src/hooks/prominent-api-hooks';
import { useEffect } from 'react';
import { SlotsApiType } from "@backend/trpc/router";
type Slot = SlotsApiType['slots'][number];
type ProductAvailability = SlotsApiType['productAvailability'][number];
interface ProductSlotInfo {
slots: Slot[];
isOutOfStock: boolean;
isFlashAvailable: boolean;
}
interface CentralSlotState {
slots: Slot[];
productSlotsMap: Record<number, ProductSlotInfo>;
refetchSlots: (() => Promise<void>) | null;
setSlotsData: (slots: Slot[], productAvailability: ProductAvailability[]) => void;
clearSlotsData: () => void;
setRefetchSlots: (refetch: () => Promise<void>) => void;
}
export const useCentralSlotStore = create<CentralSlotState>((set) => ({
slots: [],
productSlotsMap: {},
refetchSlots: null,
setSlotsData: (slots, productAvailability) => {
const productSlotsMap: Record<number, ProductSlotInfo> = {};
// First, create entries for ALL products from productAvailability
productAvailability.forEach((product) => {
productSlotsMap[product.id] = {
slots: [],
isOutOfStock: product.isOutOfStock,
isFlashAvailable: product.isFlashAvailable,
};
});
// Then, populate slots for products that appear in delivery slots
slots.forEach((slot) => {
slot.products?.forEach((product) => {
if (productSlotsMap[product.id]) {
productSlotsMap[product.id].slots.push(slot);
}
});
});
set({ slots, productSlotsMap });
},
clearSlotsData: () => set({ slots: [], productSlotsMap: {} }),
setRefetchSlots: (refetchSlots) => set({ refetchSlots }),
}));
export function useInitializeCentralSlotStore() {
const { data: slotsData, refetch } = useSlots();
const setSlotsData = useCentralSlotStore((state) => state.setSlotsData);
const setRefetchSlots = useCentralSlotStore((state) => state.setRefetchSlots);
useEffect(() => {
if (slotsData?.slots) {
setSlotsData(slotsData.slots, slotsData.productAvailability || []);
}
}, [slotsData, setSlotsData]);
useEffect(() => {
setRefetchSlots(async () => {
await refetch();
});
}, [refetch, setRefetchSlots]);
}

View file

@ -18,18 +18,6 @@
],
"common-ui/*": [
"../../packages/ui/*"
],
"@packages/shared": [
"../../packages/shared"
],
"@packages/shared/*": [
"../../packages/shared/*"
],
"global-shared": [
"../../packages/shared"
],
"global-shared/*": [
"../../packages/shared/*"
]
},
"moduleSuffixes": [
@ -46,6 +34,5 @@
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts",
"../../packages/shared"
]
}

4028
bun.lock

File diff suppressed because it is too large Load diff

30
ios/.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# OSX
#
.DS_Store
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
.xcode.env.local
# Bundle artifacts
*.jsbundle
# CocoaPods
/Pods/

11
ios/.xcode.env Normal file
View file

@ -0,0 +1,11 @@
# This `.xcode.env` file is versioned and is used to source the environment
# used when running script phases inside Xcode.
# To customize your local environment, you can create an `.xcode.env.local`
# file that is not versioned.
# NODE_BINARY variable contains the PATH to the node executable.
#
# Customize the NODE_BINARY variable here.
# For example, to use nvm with brew, add the following line
# . "$(brew --prefix nvm)/nvm.sh" --no-use
export NODE_BINARY=$(command -v node)

64
ios/Podfile Normal file
View file

@ -0,0 +1,64 @@
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
ENV['RCT_NEW_ARCH_ENABLED'] = '0' if podfile_properties['newArchEnabled'] == 'false'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
install! 'cocoapods',
:deterministic_uuids => false
prepare_react_native_project!
target 'meatfarmermonorepo' do
use_expo_modules!
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
else
config_command = [
'npx',
'expo-modules-autolinking',
'react-native-config',
'--json',
'--platform',
'ios'
]
end
config = use_native_modules!(config_command)
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/..",
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
)
post_install do |installer|
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false,
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
)
# This is necessary for Xcode 14, because it signs resource bundles by default
# when building for devices.
installer.target_installation_results.pod_target_installation_results
.each do |pod_name, target_installation_result|
target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
resource_bundle_target.build_configurations.each do |config|
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
end
end
end
end
end

2324
ios/Podfile.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
{
"expo.jsEngine": "hermes",
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true"
}

View file

@ -0,0 +1,567 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
2C4FDEE95846910CE44B063B /* libPods-meatfarmermonorepo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37BA16CD265ED7F72409550D /* libPods-meatfarmermonorepo.a */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
D20664F10ED8090F4983CFB0 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EB8BC3528ACACFE6A3CFB5D /* ExpoModulesProvider.swift */; };
E5B462C85E4DB34FCCD3D04E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F66C424AA0E1347CF8253669 /* PrivacyInfo.xcprivacy */; };
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
13B07F961A680F5B00A75B9A /* meatfarmermonorepo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = meatfarmermonorepo.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = meatfarmermonorepo/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = meatfarmermonorepo/Info.plist; sourceTree = "<group>"; };
338F04D2CAE7495C1D7CD233 /* Pods-meatfarmermonorepo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-meatfarmermonorepo.debug.xcconfig"; path = "Target Support Files/Pods-meatfarmermonorepo/Pods-meatfarmermonorepo.debug.xcconfig"; sourceTree = "<group>"; };
37BA16CD265ED7F72409550D /* libPods-meatfarmermonorepo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-meatfarmermonorepo.a"; sourceTree = BUILT_PRODUCTS_DIR; };
3EB8BC3528ACACFE6A3CFB5D /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-meatfarmermonorepo/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
533CB49887DAADE200BAF1EB /* Pods-meatfarmermonorepo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-meatfarmermonorepo.release.xcconfig"; path = "Target Support Files/Pods-meatfarmermonorepo/Pods-meatfarmermonorepo.release.xcconfig"; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = meatfarmermonorepo/SplashScreen.storyboard; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = meatfarmermonorepo/AppDelegate.swift; sourceTree = "<group>"; };
F11748442D0722820044C1D9 /* meatfarmermonorepo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "meatfarmermonorepo-Bridging-Header.h"; path = "meatfarmermonorepo/meatfarmermonorepo-Bridging-Header.h"; sourceTree = "<group>"; };
F66C424AA0E1347CF8253669 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = meatfarmermonorepo/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
2C4FDEE95846910CE44B063B /* libPods-meatfarmermonorepo.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
13B07FAE1A68108700A75B9A /* meatfarmermonorepo */ = {
isa = PBXGroup;
children = (
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
F11748442D0722820044C1D9 /* meatfarmermonorepo-Bridging-Header.h */,
BB2F792B24A3F905000567C9 /* Supporting */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
F66C424AA0E1347CF8253669 /* PrivacyInfo.xcprivacy */,
);
name = meatfarmermonorepo;
sourceTree = "<group>";
};
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
37BA16CD265ED7F72409550D /* libPods-meatfarmermonorepo.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
3742C5BD2E66753B5F39026B /* ExpoModulesProviders */ = {
isa = PBXGroup;
children = (
4DACB6B6CCC8B1A8CBF98D1D /* meatfarmermonorepo */,
);
name = ExpoModulesProviders;
sourceTree = "<group>";
};
4DACB6B6CCC8B1A8CBF98D1D /* meatfarmermonorepo */ = {
isa = PBXGroup;
children = (
3EB8BC3528ACACFE6A3CFB5D /* ExpoModulesProvider.swift */,
);
name = meatfarmermonorepo;
sourceTree = "<group>";
};
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
);
name = Libraries;
sourceTree = "<group>";
};
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
13B07FAE1A68108700A75B9A /* meatfarmermonorepo */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
D7B1D84C142A04E96F6A3A3D /* Pods */,
3742C5BD2E66753B5F39026B /* ExpoModulesProviders */,
);
indentWidth = 2;
sourceTree = "<group>";
tabWidth = 2;
usesTabs = 0;
};
83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup;
children = (
13B07F961A680F5B00A75B9A /* meatfarmermonorepo.app */,
);
name = Products;
sourceTree = "<group>";
};
BB2F792B24A3F905000567C9 /* Supporting */ = {
isa = PBXGroup;
children = (
BB2F792C24A3F905000567C9 /* Expo.plist */,
);
name = Supporting;
path = meatfarmermonorepo/Supporting;
sourceTree = "<group>";
};
D7B1D84C142A04E96F6A3A3D /* Pods */ = {
isa = PBXGroup;
children = (
338F04D2CAE7495C1D7CD233 /* Pods-meatfarmermonorepo.debug.xcconfig */,
533CB49887DAADE200BAF1EB /* Pods-meatfarmermonorepo.release.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
13B07F861A680F5B00A75B9A /* meatfarmermonorepo */ = {
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "meatfarmermonorepo" */;
buildPhases = (
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
5BC667EA2551B13AA89D9A66 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
DAF2E4EA15761DC5FE4C4A8B /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = meatfarmermonorepo;
productName = meatfarmermonorepo;
productReference = 13B07F961A680F5B00A75B9A /* meatfarmermonorepo.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1130;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
LastSwiftMigration = 1250;
};
};
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "meatfarmermonorepo" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
13B07F861A680F5B00A75B9A /* meatfarmermonorepo */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
13B07F8E1A680F5B00A75B9A /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
E5B462C85E4DB34FCCD3D04E /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Bundle React Native code and images";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
};
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-meatfarmermonorepo-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
5BC667EA2551B13AA89D9A66 /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[Expo] Configure project";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-meatfarmermonorepo/expo-configure-project.sh\"\n";
};
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-meatfarmermonorepo/Pods-meatfarmermonorepo-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/AppAuth/AppAuthCore_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GTMAppAuth/GTMAppAuth_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/ReachabilitySwift.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AppAuthCore_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMAppAuth_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReachabilitySwift.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-meatfarmermonorepo/Pods-meatfarmermonorepo-resources.sh\"\n";
showEnvVarsInLog = 0;
};
DAF2E4EA15761DC5FE4C4A8B /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-meatfarmermonorepo/Pods-meatfarmermonorepo-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-meatfarmermonorepo/Pods-meatfarmermonorepo-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
13B07F871A680F5B00A75B9A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
D20664F10ED8090F4983CFB0 /* ExpoModulesProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 338F04D2CAE7495C1D7CD233 /* Pods-meatfarmermonorepo.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = meatfarmermonorepo/meatfarmermonorepo.entitlements;
CURRENT_PROJECT_VERSION = 1;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"FB_SONARKIT_ENABLED=1",
);
INFOPLIST_FILE = meatfarmermonorepo/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "com.mohammedshafiuddin54.meat-farmer-monorepo";
PRODUCT_NAME = meatfarmermonorepo;
SWIFT_OBJC_BRIDGING_HEADER = "meatfarmermonorepo/meatfarmermonorepo-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 533CB49887DAADE200BAF1EB /* Pods-meatfarmermonorepo.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = meatfarmermonorepo/meatfarmermonorepo.entitlements;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = meatfarmermonorepo/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = "com.mohammedshafiuddin54.meat-farmer-monorepo";
PRODUCT_NAME = meatfarmermonorepo;
SWIFT_OBJC_BRIDGING_HEADER = "meatfarmermonorepo/meatfarmermonorepo-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
83CBBA201A601CBA00E9B192 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
USE_HERMES = true;
};
name = Debug;
};
83CBBA211A601CBA00E9B192 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "meatfarmermonorepo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
13B07F941A680F5B00A75B9A /* Debug */,
13B07F951A680F5B00A75B9A /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "meatfarmermonorepo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
83CBBA201A601CBA00E9B192 /* Debug */,
83CBBA211A601CBA00E9B192 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
}

View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "meatfarmermonorepo.app"
BlueprintName = "meatfarmermonorepo"
ReferencedContainer = "container:meatfarmermonorepo.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "meatfarmermonorepoTests.xctest"
BlueprintName = "meatfarmermonorepoTests"
ReferencedContainer = "container:meatfarmermonorepo.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "meatfarmermonorepo.app"
BlueprintName = "meatfarmermonorepo"
ReferencedContainer = "container:meatfarmermonorepo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "meatfarmermonorepo.app"
BlueprintName = "meatfarmermonorepo"
ReferencedContainer = "container:meatfarmermonorepo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:meatfarmermonorepo.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View file

@ -0,0 +1,70 @@
import Expo
import React
import ReactAppDependencyProvider
@UIApplicationMain
public class AppDelegate: ExpoAppDelegate {
var window: UIWindow?
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
var reactNativeFactory: RCTReactNativeFactory?
public override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let delegate = ReactNativeDelegate()
let factory = ExpoReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
reactNativeDelegate = delegate
reactNativeFactory = factory
bindReactNativeFactory(factory)
#if os(iOS) || os(tvOS)
window = UIWindow(frame: UIScreen.main.bounds)
factory.startReactNative(
withModuleName: "main",
in: window,
launchOptions: launchOptions)
#endif
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Linking API
public override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
}
// Universal Links
public override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
}
}
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
// Extension point for config-plugins
override func sourceURL(for bridge: RCTBridge) -> URL? {
// needed to return the correct URL for expo-dev-client.
bridge.bundleURL ?? bundleURL()
}
override func bundleURL() -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
#else
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -0,0 +1,14 @@
{
"images": [
{
"filename": "App-Icon-1024x1024@1x.png",
"idiom": "universal",
"platform": "ios",
"size": "1024x1024"
}
],
"info": {
"version": 1,
"author": "expo"
}
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "expo"
}
}

View file

@ -0,0 +1,20 @@
{
"colors": [
{
"color": {
"components": {
"alpha": "1.000",
"blue": "1.00000000000000",
"green": "1.00000000000000",
"red": "1.00000000000000"
},
"color-space": "srgb"
},
"idiom": "universal"
}
],
"info": {
"version": 1,
"author": "expo"
}
}

View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>meat-farmer-monorepo</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.mohammedshafiuddin54.meat-farmer-monorepo</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>exp+meat-farmer-monorepo</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

Some files were not shown because too many files have changed in this diff Show more