Compare commits

..

No commits in common. "main" and "before-notif" have entirely different histories.

1042 changed files with 20156 additions and 629897 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,8 +0,0 @@
> Why do I have a folder named ".expo" in my project?
The ".expo" folder is created when an Expo project is started using "expo start" command.
> What do the files contain?
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
- "settings.json": contains the server configuration that is used to serve the application manifest.
> Should I commit the ".expo" folder?
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.

View file

@ -1,3 +0,0 @@
{
"devices": []
}

View file

@ -1,4 +0,0 @@
{
"dependencies": "c63a16a85154f1ea03750b1df53dcdee0200585f",
"devDependencies": "0a1ec1c6df1c9d5100926df058dd0824b1293819"
}

2
.gitignore vendored
View file

@ -7,9 +7,7 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log* lerna-debug.log*
.pnpm-debug.log* .pnpm-debug.log*
*.apk
**/.wrangler/*
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

View file

@ -1,6 +1,7 @@
# Agent Instructions for Meat Farmer Monorepo # Agent Instructions for Meat Farmer Monorepo
## Important instructions ## Important instructions
- Don't try to build the code or run or compile it. Just make changes and leave the rest for the user.
- Don't run any drizzle migrations. User will handle it. - Don't run any drizzle migrations. User will handle it.
## Code Style Guidelines ## Code Style Guidelines
@ -47,4 +48,6 @@ react-native. They are available in the common-ui as MyText, MyTextInput, MyTouc
- Database: Drizzle ORM with PostgreSQL - Database: Drizzle ORM with PostgreSQL
## Important Notes ## Important Notes
- Don't do anything with git. Don't do git add or git commit. That will be managed entirely by the user - **Do not run build, compile, or migration commands** - These should be handled manually by developers
- Avoid running `npm run build`, `tsc`, `drizzle-kit generate`, or similar compilation/migration commands
- Don't do anything with git. Don't do git add or git commit. That will be managed entirely by the user

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

View file

@ -1,7 +1,3 @@
{ {
"expo": { "expo": {}
"ios": { }
"bundleIdentifier": "com.mohammedshafiuddin54.meat-farmer-monorepo"
}
}
}

View file

@ -1,83 +0,0 @@
/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
args: {
'$0': 'jest',
config: 'e2e/jest.config.js'
},
jest: {
setupTimeout: 120000
}
},
apps: {
'ios.debug': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/YOUR_APP.app',
build: 'xcodebuild -workspace ios/YOUR_APP.xcworkspace -scheme YOUR_APP -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build'
},
'ios.release': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/YOUR_APP.app',
build: 'xcodebuild -workspace ios/YOUR_APP.xcworkspace -scheme YOUR_APP -configuration Release -sdk iphonesimulator -derivedDataPath ios/build'
},
'android.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
reversePorts: [
8081
]
},
'android.release': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release'
}
},
devices: {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPhone 15'
}
},
attached: {
type: 'android.attached',
device: {
adbName: '.*'
}
},
emulator: {
type: 'android.emulator',
device: {
avdName: 'Pixel_3a_API_30_x86'
}
}
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.debug'
},
'ios.sim.release': {
device: 'simulator',
app: 'ios.release'
},
'android.att.debug': {
device: 'attached',
app: 'android.debug'
},
'android.att.release': {
device: 'attached',
app: 'android.release'
},
'android.emu.debug': {
device: 'emulator',
app: 'android.debug'
},
'android.emu.release': {
device: 'emulator',
app: 'android.release'
}
}
};

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
{ {
"dependencies": "4650ceb7c30aaa4d5fd17b9577e186af7a84b50d", "dependencies": "091948e86692e0cce7744b6b0543448538c3125a",
"devDependencies": "b3b38265f32b99a8299270a292f38ca26288d53d" "devDependencies": "b3b38265f32b99a8299270a292f38ca26288d53d"
} }

File diff suppressed because one or more lines are too long

View file

@ -63,21 +63,7 @@
"backgroundColor": "#fff0f6" "backgroundColor": "#fff0f6"
}, },
"edgeToEdgeEnabled": true, "edgeToEdgeEnabled": true,
"package": "in.freshyo.adminui", "package": "in.freshyo.adminui"
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{
"scheme": "https",
"host": "ui.freshyo.in",
"pathPrefix": "/manage-orders/order-details"
}
],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}, },
"web": { "web": {
"bundler": "metro", "bundler": "metro",

View file

@ -114,34 +114,20 @@ function CustomDrawerContent() {
<MaterialIcons name="code" size={size} color={color} /> <MaterialIcons name="code" size={size} color={color} />
)} )}
/> />
<DrawerItem <DrawerItem
label="Stores" label="Stores"
onPress={() => router.push("/(drawer)/stores" as any)} onPress={() => router.push("/(drawer)/stores" as any)}
icon={({ color, size }) => ( icon={({ color, size }) => (
<MaterialIcons name="store" size={size} color={color} /> <MaterialIcons name="store" size={size} color={color} />
)} )}
/> />
<DrawerItem <DrawerItem
label="User Management" label="Logout"
onPress={() => router.push("/(drawer)/user-management" as any)} onPress={() => logout()}
icon={({ color, size }) => ( icon={({ color, size }) => (
<MaterialIcons name="people" size={size} color={color} /> <MaterialIcons name="logout" size={size} color={color} />
)} )}
/> />
<DrawerItem
label="Send Notifications"
onPress={() => router.push("/(drawer)/send-notifications" as any)}
icon={({ color, size }) => (
<MaterialIcons name="campaign" size={size} color={color} />
)}
/>
<DrawerItem
label="Logout"
onPress={() => logout()}
icon={({ color, size }) => (
<MaterialIcons name="logout" size={size} color={color} />
)}
/>
</DrawerContentScrollView> </DrawerContentScrollView>
); );
} }
@ -226,11 +212,11 @@ export default function Layout() {
<Drawer.Screen name="coupons" options={{ title: "Coupons" }} /> <Drawer.Screen name="coupons" options={{ title: "Coupons" }} />
<Drawer.Screen name="slots" options={{ title: "Slots" }} /> <Drawer.Screen name="slots" options={{ title: "Slots" }} />
<Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} /> <Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} />
<Drawer.Screen name="stores" options={{ title: "Stores" }} /> <Drawer.Screen name="stores" options={{ title: "Stores" }} />
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} /> <Drawer.Screen name="address-management" options={{ title: "Address Management" }} />
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} /> <Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
<Drawer.Screen name="user-management" options={{ title: "User Management" }} /> <Drawer.Screen name="order-details/[id]" options={{ title: "Order Details" }} />
<Drawer.Screen name="send-notifications" options={{ title: "Send Notifications" }} /> <Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
</Drawer> </Drawer>
); );
} }

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

@ -1,58 +1,25 @@
import React, { useState, useCallback, useMemo } from "react"; import React, { useState } from "react";
import { View, TouchableOpacity, Alert, ActivityIndicator } from "react-native"; import { View, Text, TouchableOpacity, Alert } from "react-native";
import { MaterialIcons } from "@expo/vector-icons"; import { tw, ConfirmationDialog, MyText, MyFlatList, useMarkDataFetchers, usePagination, ImageViewerURI } from "common-ui";
import { useRouter } from "expo-router";
import { tw, ConfirmationDialog, MyText, MyFlatList, useMarkDataFetchers, ImageViewerURI } from "common-ui";
import { trpc } from "@/src/trpc-client"; import { trpc } from "@/src/trpc-client";
export default function Complaints() { export default function Complaints() {
const router = useRouter(); const { currentPage, pageSize, PaginationComponent } = usePagination(5); // 5 complaints per page for testing
const [dialogOpen, setDialogOpen] = useState(false); const { data, isLoading, error, refetch } = trpc.admin.complaint.getAll.useQuery({
const [selectedComplaintId, setSelectedComplaintId] = useState<number | null>(null); page: currentPage,
limit: pageSize,
const { });
data, const resolveComplaint = trpc.admin.complaint.resolve.useMutation();
isLoading,
isError,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
refetch,
} = trpc.admin.complaint.getAll.useInfiniteQuery(
{ limit: 20 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
useMarkDataFetchers(() => { useMarkDataFetchers(() => {
refetch(); refetch();
}); });
const resolveComplaint = trpc.admin.complaint.resolve.useMutation(); const [dialogOpen, setDialogOpen] = useState(false);
const [selectedComplaintId, setSelectedComplaintId] = useState<number | null>(null);
const complaints = useMemo(() => { const complaints = data?.complaints || [];
const allComplaints = data?.pages.flatMap((page) => page.complaints) || []; const totalCount = data?.totalCount || 0;
return allComplaints.filter(
(complaint, index, self) =>
index === self.findIndex((c) => c.id === complaint.id)
);
}, [data]);
const handleLoadMore = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const handleUserPress = useCallback((userId: number) => {
router.push(`/(drawer)/user-management/${userId}`);
}, [router]);
const handleOrderPress = useCallback((orderId: number) => {
router.push(`/(drawer)/manage-orders/order-details/${orderId}`);
}, [router]);
const handleMarkResolved = (id: number) => { const handleMarkResolved = (id: number) => {
setSelectedComplaintId(id); setSelectedComplaintId(id);
@ -83,158 +50,109 @@ export default function Complaints() {
); );
}; };
if (isLoading) { if (isLoading) {
return ( return (
<View style={tw`flex-1 justify-center items-center bg-gray-50`}> <View style={tw`flex-1 justify-center items-center`}>
<ActivityIndicator size="large" color="#3B82F6" /> <MyText style={tw`text-gray-600`}>Loading complaints...</MyText>
<MyText style={tw`text-gray-500 mt-4`}>Loading complaints...</MyText> </View>
</View> );
); }
}
if (isError) { if (error) {
return ( return (
<View style={tw`flex-1 justify-center items-center bg-gray-50 p-6`}> <View style={tw`flex-1 justify-center items-center`}>
<MaterialIcons name="error-outline" size={48} color="#EF4444" /> <MyText style={tw`text-red-600`}>Error loading complaints</MyText>
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Error</MyText> </View>
<MyText style={tw`text-gray-500 text-center mt-2 mb-6`}> );
{error?.message || "Failed to load complaints"} }
</MyText>
<TouchableOpacity
onPress={() => refetch()}
style={tw`bg-blue-600 px-6 py-3 rounded-full`}
>
<MyText style={tw`text-white font-semibold`}>Retry</MyText>
</TouchableOpacity>
</View>
);
}
return ( return (
<View style={tw`flex-1 bg-gray-50`}> <View style={tw`flex-1`}>
<MyFlatList <MyFlatList
style={tw`flex-1`} style={tw`flex-1 bg-white`}
contentContainerStyle={tw`px-4 py-4`} contentContainerStyle={tw`px-4 pb-6`}
data={complaints} data={complaints}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
onEndReached={handleLoadMore} renderItem={({ item }) => (
onEndReachedThreshold={0.5} <View style={tw`bg-white p-4 mb-4 rounded-2xl shadow-lg`}>
renderItem={({ item }) => ( <MyText style={tw`text-lg font-bold mb-2 text-gray-800`}>Complaint #{item.id}</MyText>
<View style={tw`bg-white p-4 mb-4 rounded-2xl shadow-sm border border-gray-100`}> <MyText style={tw`text-base mb-2 text-gray-700`}>{item.text}</MyText>
<View style={tw`flex-row justify-between items-start mb-2`}>
<MyText style={tw`text-lg font-bold text-gray-900`}>
Complaint #{item.id}
</MyText>
<View
style={tw`px-2.5 py-1 rounded-full ${
item.status === "resolved"
? "bg-green-100 border border-green-200"
: "bg-amber-100 border border-amber-200"
}`}
>
<MyText
style={tw`text-xs font-semibold ${
item.status === "resolved" ? "text-green-700" : "text-amber-700"
}`}
>
{item.status === "resolved" ? "Resolved" : "Pending"}
</MyText>
</View>
</View>
<MyText style={tw`text-base text-gray-700 mb-3 leading-5`}> {item.images && item.images.length > 0 && (
{item.text} <View style={tw`mt-3 mb-3`}>
</MyText> <MyText style={tw`text-sm font-semibold text-gray-700 mb-2`}>Attached Images:</MyText>
<View style={tw`flex-row flex-wrap gap-2`}>
{item.images && item.images.length > 0 && ( {item.images.map((imageUri: string, index: number) => (
<View style={tw`mb-3`}> <ImageViewerURI
<MyText style={tw`text-sm font-semibold text-gray-700 mb-2`}> key={index}
Attached Images: uri={imageUri}
</MyText> style={tw`w-16 h-16 rounded-lg border border-gray-200`}
<View style={tw`flex-row flex-wrap gap-2`}> />
{item.images.map((imageUri: string, index: number) => ( ))}
<ImageViewerURI </View>
key={index}
uri={imageUri}
style={tw`w-16 h-16 rounded-lg border border-gray-200`}
/>
))}
</View> </View>
</View>
)}
<View style={tw`flex-row items-center gap-2 mb-3`}>
<MaterialIcons name="person" size={14} color="#6B7280" />
<TouchableOpacity
onPress={() => item.userId && handleUserPress(item.userId)}
>
<MyText style={tw`text-sm text-blue-600 underline`}>
{item.userName || item.userMobile || "Unknown User"}
</MyText>
</TouchableOpacity>
{item.orderId && (
<>
<MyText style={tw`text-sm text-gray-400`}>|</MyText>
<MaterialIcons name="shopping-bag" size={14} color="#6B7280" />
<TouchableOpacity
onPress={() => item.orderId && handleOrderPress(item.orderId)}
>
<MyText style={tw`text-sm text-blue-600 underline`}>
Order #{item.orderId}
</MyText>
</TouchableOpacity>
</>
)} )}
</View>
{item.status === "pending" && ( <View style={tw`flex-row items-center mb-2`}>
<TouchableOpacity <TouchableOpacity
onPress={() => handleMarkResolved(item.id)} onPress={() =>
style={tw`bg-blue-500 py-3 rounded-xl items-center shadow-sm mt-2`} Alert.alert("User Page", "User page coming soon")
> }
<MyText style={tw`text-white font-semibold`}> >
Resolve Complaint <MyText style={tw`text-sm text-blue-600 underline`}>
</MyText> {item.userName}
</TouchableOpacity> </MyText>
)} </TouchableOpacity>
<MyText style={tw`text-sm text-gray-600 mx-2`}>|</MyText>
{item.orderId && (
<TouchableOpacity
onPress={() =>
Alert.alert("Order Page", "Order page coming soon")
}
>
<MyText style={tw`text-sm text-blue-600 underline`}>
Order #{item.orderId}
</MyText>
</TouchableOpacity>
)}
</View>
<MyText
style={tw`text-sm ${
item.status === "resolved" ? "text-green-600" : "text-red-600"
}`}
>
Status: {item.status}
</MyText>
{item.status === "pending" && (
<TouchableOpacity
onPress={() => handleMarkResolved(item.id)}
style={tw`mt-2 bg-blue-500 p-3 rounded-lg shadow-md`}
>
<MyText style={tw`text-white text-center font-semibold`}>Mark as Resolved</MyText>
</TouchableOpacity>
)}
</View> </View>
)} )}
ListEmptyComponent={ ListEmptyComponent={
<View style={tw`flex-1 justify-center items-center py-20`}> <View style={tw`flex-1 justify-center items-center py-10`}>
<View style={tw`bg-white p-6 rounded-full shadow-sm mb-4`}> <MyText style={tw`text-gray-500 text-center`}>No complaints found</MyText>
<MaterialIcons name="inbox" size={48} color="#D1D5DB" /> </View>
</View> }
<MyText style={tw`text-gray-900 text-lg font-bold`}> />
No complaints <PaginationComponent totalCount={totalCount} />
</MyText> <ConfirmationDialog
<MyText style={tw`text-gray-500 text-center mt-2`}> open={dialogOpen}
All complaints will appear here positiveAction={handleConfirmResolve}
</MyText> commentNeeded={true}
</View> negativeAction={() => {
} setDialogOpen(false);
ListFooterComponent={ setSelectedComplaintId(null);
isFetchingNextPage ? ( }}
<View style={tw`py-4 items-center flex-row justify-center`}> title="Mark as Resolved"
<ActivityIndicator size="small" color="#3B82F6" /> message="Add admin notes for this resolution:"
<MyText style={tw`text-gray-500 ml-2`}>Loading more...</MyText> confirmText="Resolve"
</View> cancelText="Cancel"
) : null />
} </View>
/>
<ConfirmationDialog
open={dialogOpen}
positiveAction={handleConfirmResolve}
commentNeeded={true}
negativeAction={() => {
setDialogOpen(false);
setSelectedComplaintId(null);
}}
title="Resolve Complaint"
message="Add admin notes for this resolution:"
confirmText="Resolve"
cancelText="Cancel"
isLoading={resolveComplaint.isPending}
/>
</View>
); );
} }

View file

@ -9,20 +9,6 @@ export default function CreateCoupon() {
const router = useRouter(); const router = useRouter();
const createCoupon = trpc.admin.coupon.create.useMutation(); const createCoupon = trpc.admin.coupon.create.useMutation();
const createReservedCoupon = trpc.admin.coupon.createReservedCoupon.useMutation(); const createReservedCoupon = trpc.admin.coupon.createReservedCoupon.useMutation();
const { refetch: refetchCoupons } = trpc.admin.coupon.getAll.useInfiniteQuery(
{ limit: 20, search: '' },
{
enabled: false,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
)
const { refetch: refetchReservedCoupons } = trpc.admin.coupon.getReservedCoupons.useInfiniteQuery(
{ limit: 20, search: '' },
{
enabled: false,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
)
const handleCreateCoupon = (values: any) => { const handleCreateCoupon = (values: any) => {
console.log('Form values:', values); // Debug log console.log('Form values:', values); // Debug log
@ -41,9 +27,7 @@ export default function CreateCoupon() {
if (isLoading) return; // Prevent double submission if (isLoading) return; // Prevent double submission
mutation.mutate(payload, { mutation.mutate(payload, {
onSuccess: async () => { onSuccess: () => {
await refetchCoupons()
await refetchReservedCoupons()
Alert.alert('Success', `${isReservedCoupon ? 'Reserved coupon' : 'Coupon'} created successfully`, [ Alert.alert('Success', `${isReservedCoupon ? 'Reserved coupon' : 'Coupon'} created successfully`, [
{ text: 'OK', onPress: () => router.back() } { text: 'OK', onPress: () => router.back() }
]); ]);
@ -62,4 +46,4 @@ export default function CreateCoupon() {
/> />
</AppContainer> </AppContainer>
); );
} }

View file

@ -12,21 +12,7 @@ export default function EditCoupon() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const couponId = parseInt(id as string); const couponId = parseInt(id as string);
const { data: coupon, isLoading, refetch } = trpc.admin.coupon.getById.useQuery({ id: couponId }); const { data: coupon, isLoading } = trpc.admin.coupon.getById.useQuery({ id: couponId });
const { refetch: refetchCoupons } = trpc.admin.coupon.getAll.useInfiniteQuery(
{ limit: 20, search: '' },
{
enabled: false,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
)
const { refetch: refetchReservedCoupons } = trpc.admin.coupon.getReservedCoupons.useInfiniteQuery(
{ limit: 20, search: '' },
{
enabled: false,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
)
const updateCoupon = trpc.admin.coupon.update.useMutation(); const updateCoupon = trpc.admin.coupon.update.useMutation();
const handleUpdateCoupon = (values: CreateCouponPayload & { isReservedCoupon?: boolean }) => { const handleUpdateCoupon = (values: CreateCouponPayload & { isReservedCoupon?: boolean }) => {
@ -38,10 +24,7 @@ export default function EditCoupon() {
delete updates.targetUsers; delete updates.targetUsers;
updateCoupon.mutate({ id: couponId, updates }, { updateCoupon.mutate({ id: couponId, updates }, {
onSuccess: async () => { onSuccess: () => {
await refetch()
await refetchCoupons()
await refetchReservedCoupons()
Alert.alert('Success', 'Coupon updated successfully', [ Alert.alert('Success', 'Coupon updated successfully', [
{ text: 'OK', onPress: () => router.back() } { text: 'OK', onPress: () => router.back() }
]); ]);
@ -97,4 +80,4 @@ export default function EditCoupon() {
/> />
</AppContainer> </AppContainer>
); );
} }

View file

@ -17,13 +17,6 @@ export default function Layout() {
headerShown: false, headerShown: false,
}} }}
/> />
<Stack.Screen
name="all-items-order"
options={{
title: "All Items Order",
headerShown: false,
}}
/>
</Stack> </Stack>
); );
} }

View file

@ -1,391 +0,0 @@
import React, { useState, useEffect, useCallback } from "react";
import {
View,
Alert,
ActivityIndicator,
Dimensions,
StyleSheet,
} from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";
import { Image } from "expo-image";
import DraggableFlatList, {
ScaleDecorator,
} from "react-native-draggable-flatlist";
import {
AppContainer,
MyText,
tw,
MyTouchableOpacity,
} from "common-ui";
import { useRouter } from "expo-router";
import { trpc } from "../../../src/trpc-client";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { useQueryClient } from "@tanstack/react-query";
const { width: screenWidth } = Dimensions.get("window");
// Item takes full width minus padding
const itemWidth = screenWidth - 48; // 24px padding each side
const itemHeight = 80;
interface Product {
id: number;
name: string;
images: string[];
isOutOfStock: boolean;
}
interface ProductItemProps {
item: Product;
drag: () => void;
isActive: boolean;
}
const ProductItem: React.FC<ProductItemProps> = ({
item,
drag,
isActive,
}) => {
return (
<ScaleDecorator>
<TouchableOpacity
onLongPress={drag}
activeOpacity={1}
style={[
styles.item,
isActive && styles.activeItem,
item.isOutOfStock && styles.outOfStock,
]}
>
{/* Drag Handle */}
<View style={styles.dragHandle}>
<MaterialIcons
name="drag-indicator"
size={24}
color={isActive ? "#3b82f6" : "#9ca3af"}
/>
</View>
{/* Product Image */}
{item.images?.[0] ? (
<Image
source={{ uri: item.images[0] }}
style={styles.image}
resizeMode="cover"
/>
) : (
<View style={styles.placeholderImage}>
<MaterialIcons name="image" size={24} color="#9ca3af" />
</View>
)}
{/* Product Info */}
<View style={styles.info}>
<MyText style={styles.name} numberOfLines={1}>
{item.name.length > 30 ? item.name.substring(0, 30) + '...' : item.name}
</MyText>
{item.isOutOfStock && (
<MaterialIcons name="remove-circle" size={16} color="#dc2626" />
)}
</View>
</TouchableOpacity>
</ScaleDecorator>
);
};
export default function AllItemsOrder() {
const router = useRouter();
const queryClient = useQueryClient();
const [products, setProducts] = useState<Product[]>([]);
const [hasChanges, setHasChanges] = useState(false);
// Get current order from constants
const { data: constants, isLoading: isLoadingConstants, error: constantsError } = trpc.admin.const.getConstants.useQuery();
const { data: allProducts, isLoading: isLoadingProducts, error: productsError } = trpc.common.product.getAllProductsSummary.useQuery({});
const updateConstants = trpc.admin.const.updateConstants.useMutation();
// Initialize products from constants
useEffect(() => {
if (allProducts?.products) {
const allItemsOrderConstant = constants?.find(c => c.key === 'allItemsOrder');
let orderedIds: number[] = [];
if (allItemsOrderConstant) {
const value = allItemsOrderConstant.value;
if (Array.isArray(value)) {
orderedIds = value.map((id: any) => parseInt(id));
} else if (typeof value === 'string') {
orderedIds = value.split(',').map((id: string) => parseInt(id.trim())).filter(id => !isNaN(id));
}
}
// Create product map for quick lookup
const productMap = new Map(allProducts.products.map(p => [p.id, p]));
// Sort products based on order, products not in order go to end
const sortedProducts: Product[] = [];
// First add products in the specified order
for (const id of orderedIds) {
const product = productMap.get(id);
if (product) {
sortedProducts.push({
id: product.id,
name: product.name,
images: product.images || [],
isOutOfStock: product.isOutOfStock || false,
});
productMap.delete(id);
}
}
// Then add remaining products (not in order yet)
for (const product of productMap.values()) {
sortedProducts.push({
id: product.id,
name: product.name,
images: product.images || [],
isOutOfStock: product.isOutOfStock || false,
});
}
setProducts(sortedProducts);
}
}, [constants, allProducts]);
const handleDragEnd = useCallback(({ data }: { data: Product[] }) => {
setProducts(data);
setHasChanges(true);
}, []);
const renderItem = useCallback(({ item, drag, isActive }: { item: Product; drag: () => void; isActive: boolean }) => {
return (
<ProductItem
item={item}
drag={drag}
isActive={isActive}
/>
);
}, []);
const handleSave = () => {
const productIds = products.map(p => p.id);
updateConstants.mutate(
{
constants: [{
key: 'allItemsOrder',
value: productIds
}]
},
{
onSuccess: () => {
setHasChanges(false);
Alert.alert('Success', 'All items order updated successfully!');
queryClient.invalidateQueries({ queryKey: ['const.getConstants'] });
},
onError: (error) => {
Alert.alert('Error', 'Failed to update items order. Please try again.');
console.error('Update all items order error:', error);
}
}
);
};
// Show loading state while data is being fetched
if (isLoadingConstants || isLoadingProducts) {
return (
<AppContainer>
<View style={tw`flex-1 bg-gray-50`}>
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center justify-between`}>
<TouchableOpacity
onPress={() => router.back()}
style={tw`p-2 -ml-4`}
>
<MaterialIcons name="chevron-left" size={24} color="#374151" />
</TouchableOpacity>
<MyText style={tw`text-xl font-bold text-gray-900`}>All Items Order</MyText>
<View style={tw`w-16`} />
</View>
<View style={tw`flex-1 justify-center items-center p-8`}>
<ActivityIndicator size="large" color="#3b82f6" />
<MyText style={tw`text-gray-500 mt-4 text-center`}>
{isLoadingConstants ? 'Loading order...' : 'Loading products...'}
</MyText>
</View>
</View>
</AppContainer>
);
}
// Show error state if queries failed
if (constantsError || productsError) {
return (
<AppContainer>
<View style={tw`flex-1 bg-gray-50`}>
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center justify-between`}>
<TouchableOpacity
onPress={() => router.back()}
style={tw`p-2 -ml-4`}
>
<MaterialIcons name="chevron-left" size={24} color="#374151" />
</TouchableOpacity>
<MyText style={tw`text-xl font-bold text-gray-900`}>All Items Order</MyText>
<View style={tw`w-16`} />
</View>
<View style={tw`flex-1 justify-center items-center p-8`}>
<MaterialIcons name="error-outline" size={64} color="#ef4444" />
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Error</MyText>
<MyText style={tw`text-gray-500 mt-2 text-center`}>
{constantsError ? 'Failed to load order' : 'Failed to load products'}
</MyText>
<TouchableOpacity
onPress={() => router.back()}
style={tw`mt-6 bg-blue-600 px-6 py-3 rounded-full`}
>
<MyText style={tw`text-white font-semibold`}>Go Back</MyText>
</TouchableOpacity>
</View>
</View>
</AppContainer>
);
}
return (
<View style={tw`flex-1 bg-gray-50`}>
{/* Header */}
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center justify-between`}>
<MyTouchableOpacity
onPress={() => router.back()}
style={tw`p-2 -ml-4`}
>
<MaterialIcons name="chevron-left" size={24} color="#374151" />
</MyTouchableOpacity>
<MyText style={tw`text-xl font-bold text-gray-900`}>All Items Order</MyText>
<MyTouchableOpacity
onPress={handleSave}
disabled={!hasChanges || updateConstants.isPending}
style={tw`px-4 py-2 rounded-lg ${
hasChanges && !updateConstants.isPending
? 'bg-blue-600'
: 'bg-gray-300'
}`}
>
<MyText style={tw`${
hasChanges && !updateConstants.isPending
? 'text-white'
: 'text-gray-500'
} font-semibold`}>
{updateConstants.isPending ? 'Saving...' : 'Save'}
</MyText>
</MyTouchableOpacity>
</View>
{/* Content */}
{products.length === 0 ? (
<View style={tw`flex-1 justify-center items-center p-8`}>
<MaterialIcons name="inventory" size={64} color="#e5e7eb" />
<MyText style={tw`text-gray-500 mt-4 text-center text-lg`}>
No products available
</MyText>
</View>
) : (
<View style={tw`flex-1`}>
<View style={tw`bg-blue-50 px-4 py-2 mb-2 mt-2 mx-4 rounded-lg`}>
<MyText style={tw`text-blue-700 text-xs text-center`}>
Long press and drag to reorder {products.length} items
</MyText>
</View>
<View style={tw`flex-1 px-3`}>
<DraggableFlatList
data={products}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
onDragEnd={handleDragEnd}
showsVerticalScrollIndicator={true}
contentContainerStyle={{ paddingBottom: 20 }}
containerStyle={tw`flex-1`}
keyboardShouldPersistTaps="handled"
// Enable auto-scroll during drag
activationDistance={10}
/>
</View>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
item: {
width: itemWidth,
height: 60,
backgroundColor: 'white',
borderRadius: 8,
borderWidth: 1,
borderColor: '#e5e7eb',
padding: 10,
flexDirection: 'row',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
marginVertical: 4,
},
activeItem: {
shadowColor: '#3b82f6',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
borderColor: '#3b82f6',
transform: [{ scale: 1.02 }],
},
outOfStock: {
opacity: 0.6,
},
dragHandle: {
marginRight: 8,
padding: 2,
},
image: {
width: 30,
height: 30,
borderRadius: 6,
marginRight: 10,
},
placeholderImage: {
width: 30,
height: 30,
borderRadius: 6,
backgroundColor: '#f3f4f6',
marginRight: 10,
alignItems: 'center',
justifyContent: 'center',
},
info: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
name: {
fontSize: 13,
color: '#111827',
fontWeight: '500',
flex: 1,
marginRight: 4,
},
orderNumber: {
fontSize: 11,
color: '#9ca3af',
marginLeft: 8,
},
});

View file

@ -6,7 +6,14 @@ import { trpc } from '../../../src/trpc-client';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
type ConstantFormData = Record<string, any> interface ConstantFormData {
constants: ConstantItem[];
}
interface ConstantItem {
key: string;
value: any;
}
const CONST_LABELS: Record<string, string> = { const CONST_LABELS: Record<string, string> = {
minRegularOrderValue: 'Minimum Regular Order Value', minRegularOrderValue: 'Minimum Regular Order Value',
@ -24,51 +31,27 @@ const CONST_LABELS: Record<string, string> = {
playStoreUrl: 'Play Store URL', playStoreUrl: 'Play Store URL',
appStoreUrl: 'App Store URL', appStoreUrl: 'App Store URL',
popularItems: 'Popular Items', popularItems: 'Popular Items',
allItemsOrder: 'All Items Order',
isFlashDeliveryEnabled: 'Enable Flash Delivery', isFlashDeliveryEnabled: 'Enable Flash Delivery',
supportMobile: 'Support Mobile', supportMobile: 'Support Mobile',
supportEmail: 'Support Email', supportEmail: 'Support Email',
}; };
const CONST_VISIBILITY: Record<string, boolean> = {
minRegularOrderValue: true,
freeDeliveryThreshold: true,
deliveryCharge: true,
flashFreeDeliveryThreshold: true,
flashDeliveryCharge: true,
platformFeePercent: true,
taxRate: false,
minOrderAmountForCoupon: true,
maxCouponDiscount: false,
flashDeliverySlotId: true,
readableOrderId: false,
versionNum: true,
playStoreUrl: true,
appStoreUrl: true,
popularItems: true,
allItemsOrder: true,
isFlashDeliveryEnabled: true,
supportMobile: true,
supportEmail: true,
tester: false,
};
interface ConstantInputProps { interface ConstantInputProps {
constantKey: string; constant: ConstantItem;
value: any;
setFieldValue: (field: string, value: any) => void; setFieldValue: (field: string, value: any) => void;
index: number;
router: any; router: any;
} }
const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFieldValue, router }) => { const ConstantInput: React.FC<ConstantInputProps> = ({ constant, setFieldValue, index, router }) => {
const fieldName = constantKey const fieldName = `constants.${index}.value`;
// Special handling for popularItems - show navigation button instead of input // Special handling for popularItems - show navigation button instead of input
if (constantKey === 'popularItems') { if (constant.key === 'popularItems') {
return ( return (
<View> <View>
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}> <MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
{CONST_LABELS[constantKey] || constantKey} {CONST_LABELS[constant.key] || constant.key}
</MyText> </MyText>
<MyTouchableOpacity <MyTouchableOpacity
onPress={() => router.push('/(drawer)/customize-app/popular-items')} onPress={() => router.push('/(drawer)/customize-app/popular-items')}
@ -76,7 +59,7 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
> >
<MaterialIcons name="edit" size={20} color="#3b82f6" style={tw`mr-2`} /> <MaterialIcons name="edit" size={20} color="#3b82f6" style={tw`mr-2`} />
<MyText style={tw`text-blue-700 font-medium`}> <MyText style={tw`text-blue-700 font-medium`}>
Manage Popular Items ({Array.isArray(value) ? value.length : 0} items) Manage Popular Items ({Array.isArray(constant.value) ? constant.value.length : 0} items)
</MyText> </MyText>
<MaterialIcons name="chevron-right" size={20} color="#3b82f6" style={tw`ml-2`} /> <MaterialIcons name="chevron-right" size={20} color="#3b82f6" style={tw`ml-2`} />
</MyTouchableOpacity> </MyTouchableOpacity>
@ -84,43 +67,21 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
); );
} }
// Special handling for allItemsOrder - show navigation button instead of input
if (constantKey === 'allItemsOrder') {
return (
<View>
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
{CONST_LABELS[constantKey] || constantKey}
</MyText>
<MyTouchableOpacity
onPress={() => router.push('/(drawer)/customize-app/all-items-order')}
style={tw`bg-green-50 border-2 border-dashed border-green-200 p-4 rounded-lg flex-row items-center justify-center`}
>
<MaterialIcons name="reorder" size={20} color="#16a34a" style={tw`mr-2`} />
<MyText style={tw`text-green-700 font-medium`}>
Manage All Visible Items ({Array.isArray(value) ? value.length : 0} items)
</MyText>
<MaterialIcons name="chevron-right" size={20} color="#16a34a" style={tw`ml-2`} />
</MyTouchableOpacity>
</View>
);
}
// Handle boolean values - show checkbox // Handle boolean values - show checkbox
if (typeof value === 'boolean') { if (typeof constant.value === 'boolean') {
return ( return (
<View> <View>
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}> <MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
{CONST_LABELS[constantKey] || constantKey} {CONST_LABELS[constant.key] || constant.key}
</MyText> </MyText>
<View style={tw`flex-row items-center`}> <View style={tw`flex-row items-center`}>
<Checkbox <Checkbox
checked={value} checked={constant.value}
onPress={() => setFieldValue(fieldName, !value)} onPress={() => setFieldValue(fieldName, !constant.value)}
size={28} size={28}
/> />
<MyText style={tw`ml-3 text-gray-700`}> <MyText style={tw`ml-3 text-gray-700`}>
{value ? 'Enabled' : 'Disabled'} {constant.value ? 'Enabled' : 'Disabled'}
</MyText> </MyText>
</View> </View>
</View> </View>
@ -128,11 +89,11 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
} }
// Handle different value types // Handle different value types
if (typeof value === 'number') { if (typeof constant.value === 'number') {
return ( return (
<MyTextInput <MyTextInput
topLabel={CONST_LABELS[constantKey] || constantKey} topLabel={CONST_LABELS[constant.key] || constant.key}
value={value.toString()} value={constant.value.toString()}
onChangeText={(value) => { onChangeText={(value) => {
const numValue = parseFloat(value); const numValue = parseFloat(value);
setFieldValue(fieldName, isNaN(numValue) ? 0 : numValue); setFieldValue(fieldName, isNaN(numValue) ? 0 : numValue);
@ -143,11 +104,11 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
); );
} }
if (Array.isArray(value)) { if (Array.isArray(constant.value)) {
return ( return (
<MyTextInput <MyTextInput
topLabel={CONST_LABELS[constantKey] || constantKey} topLabel={CONST_LABELS[constant.key] || constant.key}
value={value.join(', ')} value={constant.value.join(', ')}
onChangeText={(value) => { onChangeText={(value) => {
const arrayValue = value.split(',').map(s => s.trim()).filter(s => s.length > 0); const arrayValue = value.split(',').map(s => s.trim()).filter(s => s.length > 0);
setFieldValue(fieldName, arrayValue); setFieldValue(fieldName, arrayValue);
@ -160,12 +121,9 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constantKey, value, setFi
// Default to string // Default to string
return ( return (
<MyTextInput <MyTextInput
topLabel={CONST_LABELS[constantKey] || constantKey} topLabel={CONST_LABELS[constant.key] || constant.key}
// value={value === null || value === undefined ? '' : String(value)} value={String(constant.value)}
value={value} onChangeText={(value) => setFieldValue(fieldName, value)}
onChangeText={(value) => {
setFieldValue(fieldName, value)
}}
placeholder="Enter value" placeholder="Enter value"
/> />
); );
@ -176,16 +134,12 @@ export default function CustomizeApp() {
const { data: constants, isLoading: isLoadingConstants, refetch } = trpc.admin.const.getConstants.useQuery(); const { data: constants, isLoading: isLoadingConstants, refetch } = trpc.admin.const.getConstants.useQuery();
const { mutate: updateConstants, isPending: isUpdating } = trpc.admin.const.updateConstants.useMutation(); const { mutate: updateConstants, isPending: isUpdating } = trpc.admin.const.updateConstants.useMutation();
const handleSubmit = (values: ConstantFormData) => { const handleSubmit = (values: ConstantFormData) => {
// Filter out constants that haven't changed // Filter out constants that haven't changed
const changedConstants = (constants || []).filter((constant) => { const changedConstants = values.constants.filter((constant, index) => {
const nextValue = values[constant.key] const original = constants?.[index];
return JSON.stringify(nextValue) !== JSON.stringify(constant.value) return original && JSON.stringify(constant.value) !== JSON.stringify(original.value);
}).map((constant) => ({ });
key: constant.key,
value: values[constant.key],
}))
if (changedConstants.length === 0) { if (changedConstants.length === 0) {
Alert.alert('No Changes', 'No constants were modified.'); Alert.alert('No Changes', 'No constants were modified.');
@ -223,10 +177,9 @@ export default function CustomizeApp() {
); );
} }
const initialValues: ConstantFormData = constants.reduce((acc, constant) => { const initialValues: ConstantFormData = {
acc[constant.key] = constant.value ?? '' constants: constants.map(c => ({ key: c.key, value: c.value ?? '' } as ConstantItem)),
return acc };
}, {} as ConstantFormData)
@ -241,22 +194,11 @@ export default function CustomizeApp() {
<Formik initialValues={initialValues} onSubmit={handleSubmit}> <Formik initialValues={initialValues} onSubmit={handleSubmit}>
{({ handleSubmit, values, setFieldValue }) => ( {({ handleSubmit, values, setFieldValue }) => (
<View> <View>
{constants.map((constant) => { {values.constants.map((constant, index) => (
if (!CONST_VISIBILITY[constant.key]) { <View key={constant.key} style={tw`mb-4`}>
return null <ConstantInput constant={constant} setFieldValue={setFieldValue} index={index} router={router} />
} </View>
))}
return (
<View key={constant.key} style={tw`mb-4`}>
<ConstantInput
constantKey={constant.key}
value={values[constant.key]}
setFieldValue={setFieldValue}
router={router}
/>
</View>
)
})}
<MyTouchableOpacity <MyTouchableOpacity
onPress={() => handleSubmit()} onPress={() => handleSubmit()}
@ -273,4 +215,4 @@ export default function CustomizeApp() {
</View> </View>
</AppContainer> </AppContainer>
); );
} }

View file

@ -1,11 +1,10 @@
import React, { useState, useEffect, useMemo } from "react"; import React, { useState, useEffect, useMemo } from "react";
import { import {
View, View,
TouchableOpacity,
Alert, Alert,
ActivityIndicator, ActivityIndicator,
ScrollView,
} from "react-native"; } from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";
import { Image } from "expo-image"; import { Image } from "expo-image";
import DraggableFlatList, { import DraggableFlatList, {
RenderItemParams, RenderItemParams,
@ -17,7 +16,6 @@ import {
tw, tw,
BottomDialog, BottomDialog,
BottomDropdown, BottomDropdown,
MyTouchableOpacity,
} from "common-ui"; } from "common-ui";
import ProductsSelector from "../../../components/ProductsSelector"; import ProductsSelector from "../../../components/ProductsSelector";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
@ -29,8 +27,8 @@ interface PopularProduct {
id: number; id: number;
name: string; name: string;
shortDescription: string | null; shortDescription: string | null;
price: number; price: string;
marketPrice: number | null; marketPrice: string | null;
unit: string; unit: string;
incrementStep: number; incrementStep: number;
productQuantity: number; productQuantity: number;
@ -121,7 +119,7 @@ export default function CustomizePopularItems() {
const [popularProducts, setPopularProducts] = useState<PopularProduct[]>([]); const [popularProducts, setPopularProducts] = useState<PopularProduct[]>([]);
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const [showAddDialog, setShowAddDialog] = useState(false); const [showAddDialog, setShowAddDialog] = useState(false);
const [selectedProductIds, setSelectedProductIds] = useState<number[]>([]); const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
// Get current popular items from constants // Get current popular items from constants
const { data: constants, isLoading: isLoadingConstants, error: constantsError } = trpc.admin.const.getConstants.useQuery(); const { data: constants, isLoading: isLoadingConstants, error: constantsError } = trpc.admin.const.getConstants.useQuery();
@ -184,20 +182,14 @@ export default function CustomizePopularItems() {
}; };
const handleAddProduct = () => { const handleAddProduct = () => {
if (selectedProductIds.length > 0) { if (selectedProductId) {
const newProducts = selectedProductIds const product = allProducts?.products.find(p => p.id === selectedProductId);
.map(id => allProducts?.products.find(p => p.id === id)) if (product && !popularProducts.find(p => p.id === product.id)) {
.filter((product): product is NonNullable<typeof product> => setPopularProducts(prev => [...prev, product as PopularProduct]);
product !== undefined && !popularProducts.find(p => p.id === product.id)
);
if (newProducts.length > 0) {
setPopularProducts(prev => [...prev, ...newProducts as PopularProduct[]]);
setHasChanges(true); setHasChanges(true);
setSelectedProductId(null);
setShowAddDialog(false);
} }
setSelectedProductIds([]);
setShowAddDialog(false);
} }
}; };
@ -301,19 +293,20 @@ export default function CustomizePopularItems() {
} }
return ( return (
<View style={[tw`flex-1 bg-gray-50 relative`]}> <AppContainer>
<View style={tw`flex-1 bg-gray-50`}>
{/* Header */} {/* Header */}
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center justify-between`}> <View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center justify-between`}>
<MyTouchableOpacity <TouchableOpacity
onPress={() => router.back()} onPress={() => router.back()}
style={tw`p-2 -ml-4`} style={tw`p-2 -ml-4`}
> >
<MaterialIcons name="chevron-left" size={24} color="#374151" /> <MaterialIcons name="chevron-left" size={24} color="#374151" />
</MyTouchableOpacity> </TouchableOpacity>
<MyText style={tw`text-xl font-bold text-gray-900`}>Popular Items</MyText> <MyText style={tw`text-xl font-bold text-gray-900`}>Popular Items</MyText>
<MyTouchableOpacity <TouchableOpacity
onPress={handleSave} onPress={handleSave}
disabled={!hasChanges || updateConstants.isPending} disabled={!hasChanges || updateConstants.isPending}
style={tw`px-4 py-2 rounded-lg ${ style={tw`px-4 py-2 rounded-lg ${
@ -329,7 +322,7 @@ export default function CustomizePopularItems() {
} font-semibold`}> } font-semibold`}>
{updateConstants.isPending ? 'Saving...' : 'Save'} {updateConstants.isPending ? 'Saving...' : 'Save'}
</MyText> </MyText>
</MyTouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Content */} {/* Content */}
@ -363,41 +356,35 @@ export default function CustomizePopularItems() {
)} )}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
showsVerticalScrollIndicator={true} showsVerticalScrollIndicator={false}
scrollEnabled={true} contentContainerStyle={tw`pb-8`}
contentContainerStyle={{ paddingBottom: 80 }}
containerStyle={tw`flex-1`}
keyboardShouldPersistTaps="handled"
/> />
</View> </View>
)} )}
{/* FAB for Add Product - Fixed position */} {/* FAB for Add Product */}
<View style={tw`absolute bottom-12 right-6 z-50`}> <View style={tw`absolute bottom-4 right-4`}>
<MyTouchableOpacity <TouchableOpacity
onPress={() => setShowAddDialog(true)} onPress={() => setShowAddDialog(true)}
style={tw`bg-blue-600 p-4 rounded-full shadow-lg elevation-5`} style={tw`bg-blue-600 p-4 rounded-full shadow-lg`}
> >
<MaterialIcons name="add" size={24} color="white" /> <MaterialIcons name="add" size={24} color="white" />
</MyTouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Add Product Dialog */} {/* Add Product Dialog */}
<BottomDialog <BottomDialog
open={showAddDialog} open={showAddDialog}
onClose={() => { onClose={() => setShowAddDialog(false)}
setShowAddDialog(false);
setSelectedProductIds([]);
}}
> >
<View style={tw`pb-8 pt-2 px-4`}> <View style={tw`pb-8 pt-2 px-4`}>
<View style={tw`items-center mb-6`}> <View style={tw`items-center mb-6`}>
<View style={tw`w-12 h-1.5 bg-gray-200 rounded-full mb-4`} /> <View style={tw`w-12 h-1.5 bg-gray-200 rounded-full mb-4`} />
<MyText style={tw`text-lg font-bold text-gray-900`}> <MyText style={tw`text-lg font-bold text-gray-900`}>
Add Popular Items Add Popular Item
</MyText> </MyText>
<MyText style={tw`text-sm text-gray-500`}> <MyText style={tw`text-sm text-gray-500`}>
Select products to add to popular items Select a product to add to popular items
</MyText> </MyText>
</View> </View>
@ -411,43 +398,41 @@ export default function CustomizePopularItems() {
) : ( ) : (
<> <>
<ProductsSelector <ProductsSelector
value={selectedProductIds} value={selectedProductId || 0}
onChange={(val) => setSelectedProductIds(val as number[])} onChange={(val) => setSelectedProductId(val as number)}
multiple={true} multiple={false}
label="Select Products" label="Select Product"
placeholder="Choose products..." placeholder="Choose a product..."
labelFormat={(product) => `${product.name} - ₹${product.price}`} labelFormat={(product) => `${product.name} - ₹${product.price}`}
/> />
<View style={tw`flex-row gap-3 mt-6`}> <View style={tw`flex-row gap-3 mt-6`}>
<MyTouchableOpacity <TouchableOpacity
onPress={() => setShowAddDialog(false)} onPress={() => setShowAddDialog(false)}
style={tw`flex-1 bg-gray-100 p-3 rounded-lg`} style={tw`flex-1 bg-gray-100 p-3 rounded-lg`}
> >
<MyText style={tw`text-gray-700 text-center font-semibold`}> <MyText style={tw`text-gray-700 text-center font-semibold`}>
Cancel Cancel
</MyText> </MyText>
</MyTouchableOpacity> </TouchableOpacity>
<MyTouchableOpacity <TouchableOpacity
onPress={handleAddProduct} onPress={handleAddProduct}
disabled={selectedProductIds.length === 0} disabled={!selectedProductId}
style={tw`flex-1 ${ style={tw`flex-1 ${
selectedProductIds.length > 0 ? 'bg-blue-600' : 'bg-gray-300' selectedProductId ? 'bg-blue-600' : 'bg-gray-300'
} p-3 rounded-lg`} } p-3 rounded-lg`}
> >
<MyText style={tw`text-white text-center font-semibold`}> <MyText style={tw`text-white text-center font-semibold`}>
{selectedProductIds.length > 0 Add Product
? `Add ${selectedProductIds.length} Product${selectedProductIds.length > 1 ? 's' : ''}`
: 'Add Products'}
</MyText> </MyText>
</MyTouchableOpacity> </TouchableOpacity>
</View> </View>
</> </>
)} )}
</View> </View>
</BottomDialog> </BottomDialog>
</View> </View>
</AppContainer>
); );
} }

View file

@ -21,9 +21,6 @@ export default function CreateBanner() {
}; };
const createBannerMutation = trpc.admin.banner.createBanner.useMutation(); const createBannerMutation = trpc.admin.banner.createBanner.useMutation();
const { refetch: refetchBanners } = trpc.admin.banner.getBanners.useQuery(undefined, {
enabled: false,
});
const handleSubmit = async (values: BannerFormData, imageUrl?: string) => { const handleSubmit = async (values: BannerFormData, imageUrl?: string) => {
if (!imageUrl) { if (!imageUrl) {
@ -42,7 +39,6 @@ export default function CreateBanner() {
redirectUrl: values.redirectUrl || undefined, redirectUrl: values.redirectUrl || undefined,
}); });
await refetchBanners()
Alert.alert('Success', 'Banner created successfully', [ Alert.alert('Success', 'Banner created successfully', [
{ {
text: 'OK', text: 'OK',
@ -83,4 +79,4 @@ export default function CreateBanner() {
</View> </View>
</AppContainer> </AppContainer>
); );
} }

View file

@ -31,9 +31,6 @@ export default function EditBanner() {
const {data: bannerData } = trpc.admin.banner.getBanner.useQuery({ const {data: bannerData } = trpc.admin.banner.getBanner.useQuery({
id: parseInt(bannerId) id: parseInt(bannerId)
}); });
const { refetch: refetchBanners } = trpc.admin.banner.getBanners.useQuery(undefined, {
enabled: false,
});
const [banner, setBanner] = useState<typeof bannerData>(undefined); const [banner, setBanner] = useState<typeof bannerData>(undefined);
@ -103,7 +100,6 @@ export default function EditBanner() {
redirectUrl: values.redirectUrl || undefined, redirectUrl: values.redirectUrl || undefined,
}); });
await refetchBanners()
Alert.alert('Success', 'Banner updated successfully', [ Alert.alert('Success', 'Banner updated successfully', [
{ {
text: 'OK', text: 'OK',
@ -164,4 +160,4 @@ export default function EditBanner() {
</View> </View>
</AppContainer> </AppContainer>
); );
} }

View file

@ -464,4 +464,4 @@ export default function DashboardBanners() {
</View> </View>
</AppContainer> </AppContainer>
); );
} }

View file

@ -13,12 +13,11 @@ interface MenuItem {
icon: string; icon: string;
description?: string; description?: string;
route: string; route: string;
category: 'quick' | 'products' | 'orders' | 'marketing' | 'settings' | 'users'; category: 'quick' | 'products' | 'orders' | 'marketing' | 'settings';
iconColor?: string; iconColor?: string;
iconBg?: string; iconBg?: string;
badgeCount?: number; badgeCount?: number;
onPress?: () => void; onPress?: () => void;
testID?: string;
} }
interface MenuItemComponentProps { interface MenuItemComponentProps {
@ -74,7 +73,7 @@ export default function Dashboard() {
const menuItems: MenuItem[] = [ const menuItems: MenuItem[] = [
{ {
title: 'Manage Orderss', title: 'Manage Orders',
icon: 'shopping-bag', icon: 'shopping-bag',
description: 'View and manage customer orders', description: 'View and manage customer orders',
route: '/(drawer)/manage-orders', route: '/(drawer)/manage-orders',
@ -101,7 +100,6 @@ export default function Dashboard() {
category: 'quick', category: 'quick',
iconColor: '#06B6D4', iconColor: '#06B6D4',
iconBg: '#CFFAFE', iconBg: '#CFFAFE',
testID: 'delivery-slots-menu-item',
}, },
{ {
title: 'Add Product', title: 'Add Product',
@ -176,34 +174,25 @@ export default function Dashboard() {
iconColor: '#F97316', iconColor: '#F97316',
iconBg: '#FFEDD5', iconBg: '#FFEDD5',
}, },
{ {
title: 'App Constants', title: 'Address Management',
icon: 'settings-applications', icon: 'location-on',
description: 'Customize app settings', description: 'Manage service areas',
route: '/(drawer)/customize-app', route: '/(drawer)/address-management',
category: 'settings', category: 'settings',
iconColor: '#7C3AED', iconColor: '#EAB308',
iconBg: '#F3E8FF', iconBg: '#FEF9C3',
}, },
{ {
title: 'User Management', title: 'App Constants',
icon: 'people', icon: 'settings-applications',
description: 'View and manage all users', description: 'Customize app settings',
route: '/(drawer)/user-management', route: '/(drawer)/customize-app',
category: 'users', category: 'settings',
iconColor: '#0EA5E9', iconColor: '#7C3AED',
iconBg: '#E0F2FE', iconBg: '#F3E8FF',
}, },
{ ];
title: 'Send Notifications',
icon: 'campaign',
description: 'Send push notifications to users',
route: '/(drawer)/send-notifications',
category: 'users',
iconColor: '#8B5CF6',
iconBg: '#F3E8FF',
},
];
const quickActions = menuItems.filter(item => item.category === 'quick'); const quickActions = menuItems.filter(item => item.category === 'quick');
@ -211,7 +200,6 @@ export default function Dashboard() {
{ key: 'orders', title: 'Orders', icon: 'receipt-long' }, { key: 'orders', title: 'Orders', icon: 'receipt-long' },
{ key: 'products', title: 'Products & Inventory', icon: 'inventory' }, { key: 'products', title: 'Products & Inventory', icon: 'inventory' },
{ key: 'marketing', title: 'Marketing & Promotions', icon: 'campaign' }, { key: 'marketing', title: 'Marketing & Promotions', icon: 'campaign' },
{ key: 'users', title: 'User Management', icon: 'people' },
{ key: 'settings', title: 'Settings & Configuration', icon: 'settings' }, { key: 'settings', title: 'Settings & Configuration', icon: 'settings' },
]; ];
@ -238,8 +226,6 @@ export default function Dashboard() {
{quickActions.map((item) => ( {quickActions.map((item) => (
<Pressable <Pressable
key={`quick-${item.route}`} key={`quick-${item.route}`}
testID={item.testID}
accessibilityLabel={item.testID}
onPress={() => item.onPress ? item.onPress() : router.push(item.route as any)} onPress={() => item.onPress ? item.onPress() : router.push(item.route as any)}
style={({ pressed }) => [ style={({ pressed }) => [
tw`bg-white rounded-xl p-3 shadow-sm border border-gray-100 items-center`, tw`bg-white rounded-xl p-3 shadow-sm border border-gray-100 items-center`,
@ -285,4 +271,4 @@ export default function Dashboard() {
</ScrollView> </ScrollView>
</View> </View>
); );
} }

View file

@ -6,7 +6,6 @@ export default function Layout() {
<Stack.Screen name="index" options={{ title: 'Manage Orders' }} /> <Stack.Screen name="index" options={{ title: 'Manage Orders' }} />
<Stack.Screen name="delivery-sequences" options={{ title: 'Delivery Sequences' }} /> <Stack.Screen name="delivery-sequences" options={{ title: 'Delivery Sequences' }} />
<Stack.Screen name="orders" options={{ title: 'Orders' }} /> <Stack.Screen name="orders" options={{ title: 'Orders' }} />
<Stack.Screen name="order-details" options={{ title: 'Order Details' }} />
</Stack> </Stack>
); );
} }

View file

@ -726,7 +726,7 @@ export default function DeliverySequences() {
}} }}
onViewDetails={() => { onViewDetails={() => {
if (selectedOrder) { if (selectedOrder) {
router.push(`/manage-orders/order-details/${selectedOrder.id}`); router.push(`/order-details/${selectedOrder.id}`);
} }
setShowOrderMenu(false); setShowOrderMenu(false);
}} }}

View file

@ -18,7 +18,7 @@ export default function ManageOrders() {
useCallback(() => { useCallback(() => {
const target = getNavigationTarget(); const target = getNavigationTarget();
if (target) { if (target) {
router.push(target as any); router.replace(target as any);
} }
}, [router, getNavigationTarget]) }, [router, getNavigationTarget])
); );

View file

@ -56,11 +56,7 @@ interface OrderType {
orderId: string; orderId: string;
readableId: number; readableId: number;
customerName: string | null; customerName: string | null;
customerMobile?: string | null;
address: string; address: string;
addressId: number;
latitude: number | null;
longitude: number | null;
totalAmount: number; totalAmount: number;
deliveryCharge: number; deliveryCharge: number;
items: { items: {
@ -86,7 +82,6 @@ interface OrderType {
discountAmount?: number; discountAmount?: number;
adminNotes?: string | null; adminNotes?: string | null;
userNotes?: string | null; userNotes?: string | null;
userNegativityScore?: number;
} }
const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }) => { const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }) => {
@ -105,12 +100,12 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
const updateItemPackagingMutation = trpc.admin.order.updateOrderItemPackaging.useMutation(); const updateItemPackagingMutation = trpc.admin.order.updateOrderItemPackaging.useMutation();
const handleOrderPress = () => { const handleOrderPress = () => {
router.push(`/manage-orders/order-details/${order.orderId}` as any); router.push(`/order-details/${order.orderId}` as any);
}; };
const handleMenuOption = () => { const handleMenuOption = () => {
setMenuOpen(false); setMenuOpen(false);
router.push(`/manage-orders/order-details/${order.orderId}` as any); router.push(`/order-details/${order.orderId}` as any);
}; };
const handleMarkPackaged = (isPackaged: boolean) => { const handleMarkPackaged = (isPackaged: boolean) => {
@ -173,8 +168,8 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
<View style={tw`flex-row justify-between items-start`}> <View style={tw`flex-row justify-between items-start`}>
<View style={tw`flex-1`}> <View style={tw`flex-1`}>
<View style={tw`flex-row items-center mb-1`}> <View style={tw`flex-row items-center mb-1`}>
<MyText style={tw`font-bold text-lg mr-2 ${order.status === 'cancelled' ? 'text-red-600' : (order.userNegativityScore && order.userNegativityScore > 0 ? 'text-yellow-600' : 'text-gray-900')}`}> <MyText style={tw`font-bold text-lg mr-2 ${order.status === 'cancelled' ? 'text-red-600' : 'text-gray-900'}`}>
{order.customerName || order.customerMobile || 'Unknown Customer'} {order.customerName || 'Unknown Customer'}
</MyText> </MyText>
<View style={tw`bg-gray-200 px-2 py-0.5 rounded mr-2`}> <View style={tw`bg-gray-200 px-2 py-0.5 rounded mr-2`}>
<MyText style={tw`text-xs font-medium text-gray-600`}>#{order.readableId}</MyText> <MyText style={tw`text-xs font-medium text-gray-600`}>#{order.readableId}</MyText>
@ -191,12 +186,6 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
<MyText style={tw`text-xs text-gray-500 ml-1`}> <MyText style={tw`text-xs text-gray-500 ml-1`}>
{dayjs(order.createdAt).format('MMM D, h:mm A')} {dayjs(order.createdAt).format('MMM D, h:mm A')}
</MyText> </MyText>
{order.userNegativityScore && order.userNegativityScore > 0 && (
<View style={tw`flex-row items-center ml-2`}>
<MaterialIcons name="warning" size={14} color="#CA8A04" />
<MyText style={tw`text-xs text-yellow-600 font-semibold ml-1`}>Negative Customer</MyText>
</View>
)}
</View> </View>
</View> </View>
@ -370,11 +359,11 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
isDelivered: order.isDelivered, isDelivered: order.isDelivered,
isFlashDelivery: order.isFlashDelivery, isFlashDelivery: order.isFlashDelivery,
address: order.address, address: order.address,
addressId: order.addressId, addressId: 0,
adminNotes: order.adminNotes, adminNotes: order.adminNotes,
userNotes: order.userNotes, userNotes: order.userNotes,
latitude: order.latitude, latitude: null,
longitude: order.longitude, longitude: null,
status: order.status, status: order.status,
}} }}
onViewDetails={handleMenuOption} onViewDetails={handleMenuOption}
@ -388,7 +377,7 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
setMenuOpen(false); setMenuOpen(false);
setCancelDialogOpen(true); setCancelDialogOpen(true);
}} }}
onAttachLocation={() => refetch()} onAttachLocation={() => {}}
onWhatsApp={() => {}} onWhatsApp={() => {}}
onDial={() => {}} onDial={() => {}}
/> />

View file

@ -13,7 +13,6 @@ import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import FontAwesome5 from "@expo/vector-icons/FontAwesome5"; import FontAwesome5 from "@expo/vector-icons/FontAwesome5";
import dayjs from "dayjs"; import dayjs from "dayjs";
import CancelOrderDialog from "@/components/CancelOrderDialog"; import CancelOrderDialog from "@/components/CancelOrderDialog";
import { UserIncidentsView } from "@/components/UserIncidentsView";
export default function OrderDetails() { export default function OrderDetails() {
const { id } = useLocalSearchParams<{ id: string }>(); const { id } = useLocalSearchParams<{ id: string }>();
@ -63,8 +62,7 @@ export default function OrderDetails() {
onSuccess: (result) => { onSuccess: (result) => {
Alert.alert( Alert.alert(
"Success", "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); setInitiateRefundDialogOpen(false);
}, },
@ -84,16 +82,6 @@ export default function OrderDetails() {
}, },
}); });
const removeDeliveryChargeMutation = trpc.admin.order.removeDeliveryCharge.useMutation({
onSuccess: () => {
Alert.alert("Success", "Delivery charge has been removed");
refetch();
},
onError: (error: any) => {
Alert.alert("Error", error.message || "Failed to remove delivery charge");
},
});
if (isLoading) { if (isLoading) {
return ( return (
<View style={tw`flex-1 justify-center items-center bg-gray-50`}> <View style={tw`flex-1 justify-center items-center bg-gray-50`}>
@ -279,23 +267,6 @@ export default function OrderDetails() {
</View> </View>
</View> </View>
{/* Cancellation Reason */}
{order.status === "cancelled" && order.cancelReason && (
<View
style={tw`bg-red-50 p-5 rounded-2xl border border-red-100 mb-4`}
>
<View style={tw`flex-row items-center mb-2`}>
<MaterialIcons name="cancel" size={18} color="#DC2626" />
<MyText style={tw`text-sm font-bold text-red-800 ml-2`}>
Cancellation Reason
</MyText>
</View>
<MyText style={tw`text-sm text-red-900 leading-5`}>
{order.cancelReason}
</MyText>
</View>
)}
{/* Order Progress (Simplified Timeline) */} {/* Order Progress (Simplified Timeline) */}
{order.status !== "cancelled" && ( {order.status !== "cancelled" && (
<View <View
@ -377,17 +348,12 @@ export default function OrderDetails() {
)} )}
{/* Customer Details */} {/* Customer Details */}
<TouchableOpacity <View
onPress={() => order.userId && router.push(`/(drawer)/user-management/${order.userId}`)}
activeOpacity={0.7}
style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`} style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}
> >
<View style={tw`flex-row items-center justify-between mb-4`}> <MyText style={tw`text-base font-bold text-gray-900 mb-4`}>
<MyText style={tw`text-base font-bold text-gray-900`}> Customer Details
Customer Details </MyText>
</MyText>
<MaterialIcons name="chevron-right" size={20} color="#6B7280" />
</View>
<View style={tw`flex-row items-center mb-4`}> <View style={tw`flex-row items-center mb-4`}>
<View <View
@ -397,7 +363,7 @@ export default function OrderDetails() {
</View> </View>
<View> <View>
<MyText style={tw`text-sm font-bold text-gray-900`}> <MyText style={tw`text-sm font-bold text-gray-900`}>
{order.customerName || 'Unknown User'} {order.customerName}
</MyText> </MyText>
<MyText style={tw`text-xs text-gray-500`}>Customer</MyText> <MyText style={tw`text-xs text-gray-500`}>Customer</MyText>
</View> </View>
@ -438,7 +404,7 @@ export default function OrderDetails() {
</View> </View>
</View> </View>
</View> </View>
</TouchableOpacity> </View>
{/* Order Items */} {/* Order Items */}
<View <View
@ -520,40 +486,6 @@ export default function OrderDetails() {
-{discountAmount} -{discountAmount}
</MyText> </MyText>
</View> </View>
)}
{order.deliveryCharge > 0 && (
<View style={tw`flex-row justify-between items-center mb-2`}>
<View style={tw`flex-row items-center`}>
<MyText style={tw`text-gray-600 font-medium`}>
Delivery Charge
</MyText>
<TouchableOpacity
onPress={() => {
Alert.alert(
'Remove Delivery Cost',
'Are you sure you want to remove the delivery cost from this order?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Remove',
style: 'destructive',
onPress: () => removeDeliveryChargeMutation.mutate({ orderId: order.id }),
},
]
);
}}
disabled={removeDeliveryChargeMutation.isPending}
style={tw`ml-2 px-2 py-1 bg-red-100 rounded-md`}
>
<MyText style={tw`text-xs font-bold text-red-600`}>
{removeDeliveryChargeMutation.isPending ? 'Removing...' : 'Remove'}
</MyText>
</TouchableOpacity>
</View>
<MyText style={tw`text-gray-600 font-medium`}>
{order.deliveryCharge}
</MyText>
</View>
)} )}
<View style={tw`flex-row justify-between items-center pt-2 border-t border-gray-200`}> <View style={tw`flex-row justify-between items-center pt-2 border-t border-gray-200`}>
<View style={tw`flex-row items-center`}> <View style={tw`flex-row items-center`}>
@ -612,14 +544,6 @@ export default function OrderDetails() {
</View> </View>
)} )}
{/* User Incidents Section */}
{order.userId && (
<UserIncidentsView
userId={order.userId}
orderId={order.id}
/>
)}
{/* Coupon Applied Section */} {/* Coupon Applied Section */}
{order.couponCode && ( {order.couponCode && (
<View <View

View file

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { View, Alert } from 'react-native'; import { View, Alert } from 'react-native';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { AppContainer, MyText, tw, type ImageUploaderNeoItem } from 'common-ui'; import { AppContainer, MyText, tw } from 'common-ui';
import TagForm from '@/src/components/TagForm'; import TagForm from '@/src/components/TagForm';
import { useCreateTag } from '@/src/api-hooks/tag.api';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
interface TagFormData { interface TagFormData {
tagName: string; tagName: string;
@ -15,51 +15,50 @@ interface TagFormData {
export default function AddTag() { export default function AddTag() {
const router = useRouter(); const router = useRouter();
const createTag = trpc.admin.product.createProductTag.useMutation(); const { mutate: createTag, isPending: isCreating } = useCreateTag();
const { refetch: refetchTags } = trpc.admin.product.getProductTags.useQuery(undefined, {
enabled: false,
});
const { data: storesData } = trpc.admin.store.getStores.useQuery(); const { data: storesData } = trpc.admin.store.getStores.useQuery();
const { upload, isUploading } = useUploadToObjectStorage();
const handleSubmit = async (values: TagFormData, images: ImageUploaderNeoItem[], _removedExisting: boolean) => { const handleSubmit = (values: TagFormData, image?: { uri?: string }) => {
try { const formData = new FormData();
let imageUrl: string | null | undefined;
let uploadUrls: string[] = []
const newImage = images.find((image) => image.mimeType !== null) // Add text fields
if (newImage) { formData.append('tagName', values.tagName);
const response = await fetch(newImage.imgUrl) if (values.tagDescription) {
const blob = await response.blob() formData.append('tagDescription', values.tagDescription);
const result = await upload({
images: [{ blob, mimeType: newImage.mimeType || 'image/jpeg' }],
contextString: 'tags',
})
imageUrl = result.keys[0]
uploadUrls = result.presignedUrls
}
await createTag.mutateAsync({
tagName: values.tagName,
tagDescription: values.tagDescription || undefined,
imageUrl,
isDashboardTag: values.isDashboardTag,
relatedStores: values.relatedStores,
uploadUrls,
})
await refetchTags()
Alert.alert('Success', 'Tag created successfully', [
{
text: 'OK',
onPress: () => router.back(),
},
])
} catch (error: any) {
const errorMessage = error.message || 'Failed to create tag'
Alert.alert('Error', errorMessage)
} }
} formData.append('isDashboardTag', values.isDashboardTag.toString());
// Add related stores
formData.append('relatedStores', JSON.stringify(values.relatedStores));
// Add image if uploaded
if (image?.uri) {
const filename = image.uri.split('/').pop() || 'image.jpg';
const match = /\.(\w+)$/.exec(filename);
const type = match ? `image/${match[1]}` : 'image/jpeg';
formData.append('image', {
uri: image.uri,
name: filename,
type,
} as any);
}
createTag(formData, {
onSuccess: (data) => {
Alert.alert('Success', 'Tag created successfully', [
{
text: 'OK',
onPress: () => router.back(),
},
]);
},
onError: (error: any) => {
const errorMessage = error.message || 'Failed to create tag';
Alert.alert('Error', errorMessage);
},
});
};
const initialValues: TagFormData = { const initialValues: TagFormData = {
tagName: '', tagName: '',
@ -77,10 +76,10 @@ export default function AddTag() {
mode="create" mode="create"
initialValues={initialValues} initialValues={initialValues}
onSubmit={handleSubmit} onSubmit={handleSubmit}
isLoading={createTag.isPending || isUploading} isLoading={isCreating}
stores={storesData?.stores.map((store: { id: number; name: string }) => ({ id: store.id, name: store.name })) || []} stores={storesData?.stores.map(store => ({ id: store.id, name: store.name })) || []}
/> />
</View> </View>
</AppContainer> </AppContainer>
); );
} }

View file

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { View, Alert } from 'react-native'; import { View, Alert } from 'react-native';
import { useRouter, useLocalSearchParams } from 'expo-router'; import { useRouter, useLocalSearchParams } from 'expo-router';
import { AppContainer, MyText, tw, type ImageUploaderNeoItem } from 'common-ui'; import { AppContainer, MyText, tw } from 'common-ui';
import TagForm from '@/src/components/TagForm'; import TagForm from '@/src/components/TagForm';
import { useGetTag, useUpdateTag } from '@/src/api-hooks/tag.api';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
interface TagFormData { interface TagFormData {
tagName: string; tagName: string;
@ -19,60 +19,53 @@ export default function EditTag() {
const { tagId } = useLocalSearchParams<{ tagId: string }>(); const { tagId } = useLocalSearchParams<{ tagId: string }>();
const tagIdNum = tagId ? parseInt(tagId) : null; const tagIdNum = tagId ? parseInt(tagId) : null;
const { data: tagData, isLoading: isLoadingTag, error: tagError } = trpc.admin.product.getProductTagById.useQuery( const { data: tagData, isLoading: isLoadingTag, error: tagError } = useGetTag(tagIdNum!);
{ id: tagIdNum || 0 }, const { mutate: updateTag, isPending: isUpdating } = useUpdateTag();
{ enabled: !!tagIdNum }
)
const { refetch: refetchTags } = trpc.admin.product.getProductTags.useQuery(undefined, {
enabled: false,
});
const updateTag = trpc.admin.product.updateProductTag.useMutation();
const { data: storesData } = trpc.admin.store.getStores.useQuery(); const { data: storesData } = trpc.admin.store.getStores.useQuery();
const { upload, isUploading } = useUploadToObjectStorage();
const handleSubmit = async (values: TagFormData, images: ImageUploaderNeoItem[], removedExisting: boolean) => { const handleSubmit = (values: TagFormData, image?: { uri?: string }) => {
if (!tagIdNum) return; if (!tagIdNum) return;
try { const formData = new FormData();
let imageUrl: string | null | undefined
let uploadUrls: string[] = []
const newImage = images.find((image) => image.mimeType !== null) // Add text fields
if (newImage) { formData.append('tagName', values.tagName);
const response = await fetch(newImage.imgUrl) if (values.tagDescription) {
const blob = await response.blob() formData.append('tagDescription', values.tagDescription);
const result = await upload({
images: [{ blob, mimeType: newImage.mimeType || 'image/jpeg' }],
contextString: 'tags',
})
imageUrl = result.keys[0]
uploadUrls = result.presignedUrls
} else if (removedExisting) {
imageUrl = null
}
await updateTag.mutateAsync({
id: tagIdNum,
tagName: values.tagName,
tagDescription: values.tagDescription || undefined,
imageUrl,
isDashboardTag: values.isDashboardTag,
relatedStores: values.relatedStores,
uploadUrls,
})
await refetchTags()
Alert.alert('Success', 'Tag updated successfully', [
{
text: 'OK',
onPress: () => router.back(),
},
])
} catch (error: any) {
const errorMessage = error.message || 'Failed to update tag'
Alert.alert('Error', errorMessage)
} }
} formData.append('isDashboardTag', values.isDashboardTag.toString());
// Add related stores
formData.append('relatedStores', JSON.stringify(values.relatedStores));
// Add image if uploaded
if (image?.uri) {
const filename = image.uri.split('/').pop() || 'image.jpg';
const match = /\.(\w+)$/.exec(filename);
const type = match ? `image/${match[1]}` : 'image/jpeg';
formData.append('image', {
uri: image.uri,
name: filename,
type,
} as any);
}
updateTag({ id: tagIdNum, formData }, {
onSuccess: (data) => {
Alert.alert('Success', 'Tag updated successfully', [
{
text: 'OK',
onPress: () => router.back(),
},
]);
},
onError: (error: any) => {
const errorMessage = error.message || 'Failed to update tag';
Alert.alert('Error', errorMessage);
},
});
};
if (isLoadingTag) { if (isLoadingTag) {
return ( return (
@ -99,7 +92,7 @@ export default function EditTag() {
tagName: tag.tagName, tagName: tag.tagName,
tagDescription: tag.tagDescription || '', tagDescription: tag.tagDescription || '',
isDashboardTag: tag.isDashboardTag, isDashboardTag: tag.isDashboardTag,
relatedStores: Array.isArray(tag.relatedStores) ? tag.relatedStores : [], relatedStores: tag.relatedStores || [],
existingImageUrl: tag.imageUrl || undefined, existingImageUrl: tag.imageUrl || undefined,
}; };
@ -113,10 +106,10 @@ export default function EditTag() {
initialValues={initialValues} initialValues={initialValues}
existingImageUrl={tag.imageUrl || undefined} existingImageUrl={tag.imageUrl || undefined}
onSubmit={handleSubmit} onSubmit={handleSubmit}
isLoading={updateTag.isPending || isUploading} isLoading={isUpdating}
stores={storesData?.stores.map((store: { id: number; name: string }) => ({ id: store.id, name: store.name })) || []} stores={storesData?.stores.map(store => ({ id: store.id, name: store.name })) || []}
/> />
</View> </View>
</AppContainer> </AppContainer>
); );
} }

View file

@ -5,20 +5,10 @@ import { useRouter } from 'expo-router';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { tw, MyText, useManualRefresh, useMarkDataFetchers, MyFlatList } from 'common-ui'; import { tw, MyText, useManualRefresh, useMarkDataFetchers, MyFlatList } from 'common-ui';
import { TagMenu } from '@/src/components/TagMenu'; import { TagMenu } from '@/src/components/TagMenu';
import { trpc } from '@/src/trpc-client'; import { useGetTags, Tag } from '@/src/api-hooks/tag.api';
interface TagItemData {
id: number;
tagName: string;
tagDescription: string | null;
imageUrl: string | null;
isDashboardTag: boolean;
relatedStores?: unknown;
createdAt: string | Date;
}
interface TagItemProps { interface TagItemProps {
item: TagItemData; item: Tag;
onDeleteSuccess: () => void; onDeleteSuccess: () => void;
} }
@ -70,7 +60,7 @@ const TagHeader: React.FC<TagHeaderProps> = ({ onAddNewTag }) => (
export default function ProductTags() { export default function ProductTags() {
const router = useRouter(); const router = useRouter();
const { data: tagsData, isLoading, error, refetch } = trpc.admin.product.getProductTags.useQuery(); const { data: tagsData, isLoading, error, refetch } = useGetTags();
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const tags = tagsData?.tags || []; const tags = tagsData?.tags || [];
@ -139,4 +129,4 @@ export default function ProductTags() {
/> />
</View> </View>
); );
} }

View file

@ -1,56 +1,62 @@
import React from 'react'; import React from 'react';
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import { AppContainer, ImageUploaderNeoPayload } from 'common-ui'; import { AppContainer } from 'common-ui';
import ProductForm from '@/src/components/ProductForm'; import ProductForm from '@/src/components/ProductForm';
import { trpc } from '@/src/trpc-client'; import { useCreateProduct, CreateProductPayload } from '@/src/api-hooks/product.api';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
export default function AddProduct() { export default function AddProduct() {
const createProduct = trpc.admin.product.createProduct.useMutation(); const { mutate: createProduct, isPending: isCreating } = useCreateProduct();
const { refetch: refetchProducts } = trpc.admin.product.getProducts.useQuery(undefined, {
enabled: false,
});
const { upload, isUploading } = useUploadToObjectStorage();
const handleSubmit = async (values: any, images: ImageUploaderNeoPayload[]) => { const handleSubmit = (values: any, images?: { uri?: string, mimeType?: string }[]) => {
try { const payload: CreateProductPayload = {
let uploadUrls: string[] = []; name: values.name,
shortDescription: values.shortDescription,
longDescription: values.longDescription,
unitId: parseInt(values.unitId),
storeId: parseInt(values.storeId),
price: parseFloat(values.price),
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
incrementStep: 1,
productQuantity: values.productQuantity || 1,
};
if (images.length > 0) { const formData = new FormData();
const blobs = await Promise.all( Object.entries(payload).forEach(([key, value]) => {
images.map(async (img) => { if (value !== undefined && value !== null) {
const response = await fetch(img.url); formData.append(key, value as string);
const blob = await response.blob();
return { blob, mimeType: img.mimeType || 'image/jpeg' };
})
);
const result = await upload({ images: blobs, contextString: 'product_info' });
uploadUrls = result.presignedUrls;
} }
});
await createProduct.mutateAsync({ // Append tag IDs
name: values.name, if (values.tagIds && values.tagIds.length > 0) {
shortDescription: values.shortDescription, values.tagIds.forEach((tagId: number) => {
longDescription: values.longDescription, formData.append('tagIds', tagId.toString());
unitId: parseInt(values.unitId),
storeId: parseInt(values.storeId),
price: parseFloat(values.price),
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
incrementStep: 1,
productQuantity: values.productQuantity || 1,
isSuspended: values.isSuspended || false,
isFlashAvailable: values.isFlashAvailable || false,
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : undefined,
uploadUrls,
tagIds: values.tagIds || [],
}); });
await refetchProducts();
Alert.alert('Success', 'Product created successfully!');
} catch (error: any) {
Alert.alert('Error', error.message || 'Failed to create product');
} }
// Append images
if (images) {
images.forEach((image, index) => {
if (image.uri) {
formData.append('images', {
uri: image.uri,
name: `image-${index}.jpg`,
// type: 'image/jpeg',
type: image.mimeType as any,
} as any);
}
});
}
createProduct(formData, {
onSuccess: (data) => {
Alert.alert('Success', 'Product created successfully!');
// Reset form or navigate
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to create product');
},
});
}; };
const initialValues = { const initialValues = {
@ -75,8 +81,9 @@ export default function AddProduct() {
mode="create" mode="create"
initialValues={initialValues} initialValues={initialValues}
onSubmit={handleSubmit} onSubmit={handleSubmit}
isLoading={createProduct.isPending || isUploading} isLoading={isCreating}
existingImages={[]}
/> />
</AppContainer> </AppContainer>
); );
} }

View file

@ -6,7 +6,6 @@ import { tw, AppContainer, MyText, useMarkDataFetchers, BottomDialog, ImageUploa
import { MaterialIcons, FontAwesome5, Ionicons, Feather, MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialIcons, FontAwesome5, Ionicons, Feather, MaterialCommunityIcons } from '@expo/vector-icons';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import usePickImage from 'common-ui/src/components/use-pick-image'; import usePickImage from 'common-ui/src/components/use-pick-image';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
import { Formik } from 'formik'; import { Formik } from 'formik';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { BlurView } from 'expo-blur'; import { BlurView } from 'expo-blur';
@ -24,9 +23,10 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
const [adminResponse, setAdminResponse] = useState(''); const [adminResponse, setAdminResponse] = useState('');
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]); const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]); const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
const [uploadUrls, setUploadUrls] = useState<string[]>([]);
const respondToReview = trpc.admin.product.respondToReview.useMutation(); const respondToReview = trpc.admin.product.respondToReview.useMutation();
const { upload } = useUploadToObjectStorage(); const generateUploadUrls = trpc.user.fileUpload.generateUploadUrls.useMutation();
const handleImagePick = usePickImage({ const handleImagePick = usePickImage({
setFile: async (assets: any) => { setFile: async (assets: any) => {
@ -62,16 +62,37 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
const handleSubmit = async (adminResponse: string) => { const handleSubmit = async (adminResponse: string) => {
try { try {
const { keys, presignedUrls } = await upload({ const mimeTypes = selectedImages.map(s => s.mimeType);
images: selectedImages, const { uploadUrls: generatedUrls } = await generateUploadUrls.mutateAsync({
contextString: 'review', contextString: 'review',
mimeTypes,
}); });
const keys = generatedUrls.map(url => {
const u = new URL(url);
const rawKey = u.pathname.replace(/^\/+/, "");
const decodedKey = decodeURIComponent(rawKey);
const parts = decodedKey.split('/');
parts.shift();
return parts.join('/');
});
setUploadUrls(generatedUrls);
for (let i = 0; i < generatedUrls.length; i++) {
const uploadUrl = generatedUrls[i];
const { blob, mimeType } = selectedImages[i];
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
body: blob,
headers: { 'Content-Type': mimeType },
});
if (!uploadResponse.ok) throw new Error(`Upload failed with status ${uploadResponse.status}`);
}
await respondToReview.mutateAsync({ await respondToReview.mutateAsync({
reviewId, reviewId,
adminResponse, adminResponse,
adminResponseImages: keys, adminResponseImages: keys,
uploadUrls: presignedUrls, uploadUrls: generatedUrls,
}); });
Alert.alert('Success', 'Response submitted'); Alert.alert('Success', 'Response submitted');
@ -79,7 +100,9 @@ const ReviewResponseForm: React.FC<ReviewResponseFormProps> = ({ reviewId, onClo
setAdminResponse(''); setAdminResponse('');
setSelectedImages([]); setSelectedImages([]);
setDisplayImages([]); setDisplayImages([]);
setUploadUrls([]);
} catch (error:any) { } catch (error:any) {
Alert.alert('Error', error.message || 'Failed to submit response.'); Alert.alert('Error', error.message || 'Failed to submit response.');
} }
}; };

View file

@ -1,74 +1,96 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { View, Alert } from 'react-native'; import { View, Text, Alert } from 'react-native';
import { useLocalSearchParams } from 'expo-router'; import { useLocalSearchParams } from 'expo-router';
import { AppContainer, useManualRefresh, MyText, tw, ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'common-ui'; import { AppContainer, useManualRefresh, MyText, tw } from 'common-ui';
import ProductForm, { ProductFormRef } from '@/src/components/ProductForm'; import ProductForm, { ProductFormRef } from '@/src/components/ProductForm';
import { useUpdateProduct } from '@/src/api-hooks/product.api';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
export default function EditProduct() { export default function EditProduct() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const productId = Number(id); const productId = Number(id);
const productFormRef = useRef<ProductFormRef>(null); const productFormRef = useRef<ProductFormRef>(null);
// const { data: product, isLoading: isFetching, refetch } = useGetProduct(productId);
const { data: product, isLoading: isFetching, refetch } = trpc.admin.product.getProductById.useQuery( const { data: product, isLoading: isFetching, refetch } = trpc.admin.product.getProductById.useQuery(
{ id: productId }, { id: productId },
{ enabled: !!productId } { enabled: !!productId }
); );
const { refetch: refetchProducts } = trpc.admin.product.getProducts.useQuery(undefined, { //
enabled: false, const { mutate: updateProduct, isPending: isUpdating } = useUpdateProduct();
});
const updateProduct = trpc.admin.product.updateProduct.useMutation();
const { upload, isUploading } = useUploadToObjectStorage();
useManualRefresh(() => refetch()); useManualRefresh(() => refetch());
const handleSubmit = async (values: any, images: ImageUploaderNeoPayload[], imagesToDelete: string[]) => { const handleSubmit = (values: any, newImages?: { uri?: string }[], imagesToDelete?: string[]) => {
try { const payload = {
// New images have mimeType !== null, existing images have mimeType === null
const newImages = images.filter(img => img.mimeType !== null);
let uploadUrls: string[] = [];
if (newImages.length > 0) {
const blobs = await Promise.all(
newImages.map(async (img) => {
const response = await fetch(img.url);
const blob = await response.blob();
return { blob, mimeType: img.mimeType || 'image/jpeg' };
})
);
const result = await upload({ images: blobs, contextString: 'product_info' });
uploadUrls = result.presignedUrls;
}
await updateProduct.mutateAsync({
id: productId,
name: values.name, name: values.name,
shortDescription: values.shortDescription, shortDescription: values.shortDescription,
longDescription: values.longDescription, longDescription: values.longDescription,
unitId: parseInt(values.unitId), unitId: parseInt(values.unitId),
storeId: parseInt(values.storeId), storeId: parseInt(values.storeId),
price: parseFloat(values.price), price: parseFloat(values.price),
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined, marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
incrementStep: 1, incrementStep: 1,
productQuantity: values.productQuantity || 1, productQuantity: values.productQuantity || 1,
isSuspended: values.isSuspended || false, deals: values.deals?.filter((deal: any) =>
isFlashAvailable: values.isFlashAvailable || false, deal.quantity && deal.price && deal.validTill
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : null, ).map((deal: any) => ({
uploadUrls, quantity: parseInt(deal.quantity),
imagesToDelete, price: parseFloat(deal.price),
tagIds: values.tagIds || [], validTill: deal.validTill instanceof Date
}); ? deal.validTill.toISOString().split('T')[0]
: deal.validTill, // Convert Date to YYYY-MM-DD string
})),
tagIds: values.tagIds,
};
await refetch(); console.log({payload})
await refetchProducts();
Alert.alert('Success', 'Product updated successfully!'); const formData = new FormData();
productFormRef.current?.clearImages(); Object.entries(payload).forEach(([key, value]) => {
} catch (error: any) { if (key === 'deals' && Array.isArray(value)) {
Alert.alert('Error', error.message || 'Failed to update product'); formData.append(key, JSON.stringify(value));
} else if (key === 'tagIds' && Array.isArray(value)) {
value.forEach(tagId => {
formData.append('tagIds', tagId.toString());
});
} else if (value !== undefined && value !== null) {
formData.append(key, value as string);
}
});
// Add new images
if (newImages && newImages.length > 0) {
newImages.forEach((image, index) => {
if (image.uri) {
const fileName = image.uri.split('/').pop() || `image_${index}.jpg`;
formData.append('images', {
uri: image.uri,
name: fileName,
type: 'image/jpeg',
} as any);
}
});
} }
// Add images to delete
if (imagesToDelete && imagesToDelete.length > 0) {
formData.append('imagesToDelete', JSON.stringify(imagesToDelete));
}
updateProduct(
{ id: productId, formData },
{
onSuccess: (data) => {
Alert.alert('Success', 'Product updated successfully!');
// Clear newly added images after successful update
productFormRef.current?.clearImages();
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to update product');
},
}
);
}; };
if (isFetching) { if (isFetching) {
@ -91,13 +113,7 @@ export default function EditProduct() {
); );
} }
const productData = product.product; const productData = product.product; // The API returns { product: Product }
const existingImages: ImageUploaderNeoItem[] = (productData.images || []).map((url) => ({
imgUrl: url,
mimeType: null,
}));
const existingImageKeys = productData.imageKeys || [];
const initialValues = { const initialValues = {
name: productData.name, name: productData.name,
@ -110,7 +126,7 @@ export default function EditProduct() {
deals: productData.deals?.map(deal => ({ deals: productData.deals?.map(deal => ({
quantity: deal.quantity, quantity: deal.quantity,
price: deal.price, price: deal.price,
validTill: deal.validTill ? new Date(deal.validTill) : null, validTill: deal.validTill ? new Date(deal.validTill) : null, // Convert to Date object
})) || [{ quantity: '', price: '', validTill: null }], })) || [{ quantity: '', price: '', validTill: null }],
tagIds: productData.tags?.map((tag: any) => tag.id) || [], tagIds: productData.tags?.map((tag: any) => tag.id) || [],
isSuspended: productData.isSuspended || false, isSuspended: productData.isSuspended || false,
@ -126,10 +142,9 @@ export default function EditProduct() {
mode="edit" mode="edit"
initialValues={initialValues} initialValues={initialValues}
onSubmit={handleSubmit} onSubmit={handleSubmit}
isLoading={updateProduct.isPending || isUploading} isLoading={isUpdating}
existingImages={existingImages} existingImages={productData.images || []}
existingImageKeys={existingImageKeys}
/> />
</AppContainer> </AppContainer>
); );
} }

View file

@ -6,7 +6,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { AppContainer, MyText, tw, MyButton, useManualRefresh, MyTextInput, SearchBar, useMarkDataFetchers } from 'common-ui'; import { AppContainer, MyText, tw, MyButton, useManualRefresh, MyTextInput, SearchBar, useMarkDataFetchers } from 'common-ui';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import type { AdminProduct } from '@packages/shared'; import { Product } from '@/src/api-hooks/product.api';
type FilterType = 'all' | 'in-stock' | 'out-of-stock'; type FilterType = 'all' | 'in-stock' | 'out-of-stock';
@ -54,7 +54,7 @@ export default function Products() {
// const handleToggleStock = (product: any) => { // const handleToggleStock = (product: any) => {
const handleToggleStock = (product: Pick<AdminProduct, 'id' | 'name' | 'isOutOfStock'>) => { const handleToggleStock = (product: Pick<Product, 'id' | 'name' | 'isOutOfStock'>) => {
const action = product.isOutOfStock ? 'mark as in stock' : 'mark as out of stock'; const action = product.isOutOfStock ? 'mark as in stock' : 'mark as out of stock';
Alert.alert( Alert.alert(
'Update Stock Status', 'Update Stock Status',

View file

@ -186,7 +186,7 @@ export default function RebalanceOrders() {
setDialogOpen={setDialogOpen} setDialogOpen={setDialogOpen}
/> />
)} )}
contentContainerStyle={tw`pb-24`} // Space for floating button contentContainerStyle={tw`pb-24 flex-1`} // Space for floating button
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
onRefresh={handleRefresh} onRefresh={handleRefresh}
refreshing={refreshing} refreshing={refreshing}

View file

@ -1,248 +0,0 @@
import React, { useState, useCallback } from 'react';
import {
View,
TouchableOpacity,
ActivityIndicator,
ScrollView,
Alert,
} from 'react-native';
import { useRouter } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import {
AppContainer,
MyText,
tw,
MyTextInput,
BottomDropdown,
ImageUploader,
} from 'common-ui';
import { trpc } from '@/src/trpc-client';
import usePickImage from 'common-ui/src/components/use-pick-image';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
interface User {
id: number;
name: string | null;
mobile: string | null;
isEligibleForNotif: boolean;
}
export default function SendNotifications() {
const router = useRouter();
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
const [title, setTitle] = useState('');
const [message, setMessage] = useState('');
const [selectedImage, setSelectedImage] = useState<{ blob: Blob; mimeType: string } | null>(null);
const [displayImage, setDisplayImage] = useState<{ uri?: string } | null>(null);
const [searchQuery, setSearchQuery] = useState('');
// Query users eligible for notifications
const { data: usersData, isLoading: isLoadingUsers } = trpc.admin.user.getUsersForNotification.useQuery({
search: searchQuery,
});
const { uploadSingle } = useUploadToObjectStorage();
// Send notification mutation
const sendNotification = trpc.admin.user.sendNotification.useMutation({
onSuccess: () => {
Alert.alert('Success', 'Notification sent successfully!');
// Reset form
setSelectedUserIds([]);
setTitle('');
setMessage('');
setSelectedImage(null);
setDisplayImage(null);
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to send notification');
},
});
const eligibleUsers = usersData?.users.filter((u: User) => u.isEligibleForNotif) || [];
const dropdownOptions = eligibleUsers.map((user: User) => ({
label: `${user.mobile || 'No Mobile'}${user.name ? ` - ${user.name}` : ''}`,
value: user.id,
}));
const handleImagePick = usePickImage({
setFile: async (assets: any) => {
if (!assets || (Array.isArray(assets) && assets.length === 0)) {
setSelectedImage(null);
setDisplayImage(null);
return;
}
const file = Array.isArray(assets) ? assets[0] : assets;
const response = await fetch(file.uri);
const blob = await response.blob();
setSelectedImage({ blob, mimeType: file.mimeType || 'image/jpeg' });
setDisplayImage({ uri: file.uri });
},
multiple: false,
});
const handleRemoveImage = () => {
setSelectedImage(null);
setDisplayImage(null);
};
const handleSend = async () => {
if (title.trim().length === 0) {
Alert.alert('Error', 'Please enter a title');
return;
}
if (message.trim().length === 0) {
Alert.alert('Error', 'Please enter a message');
return;
}
// Check if sending to all users
const isSendingToAll = selectedUserIds.length === 0;
if (isSendingToAll) {
const confirmed = await new Promise<boolean>((resolve) => {
Alert.alert(
'Send to All Users?',
'This will send the notification to all users with push tokens. Continue?',
[
{ text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
{ text: 'Send', style: 'default', onPress: () => resolve(true) },
]
);
});
if (!confirmed) return;
}
try {
let imageUrl: string | undefined;
// Upload image if selected
if (selectedImage) {
const { key } = await uploadSingle(selectedImage.blob, selectedImage.mimeType, 'notification');
imageUrl = key;
}
// Send notification
await sendNotification.mutateAsync({
userIds: selectedUserIds,
title: title.trim(),
text: message.trim(),
imageUrl,
});
} catch (error: any) {
Alert.alert('Error', error.message || 'Failed to send notification');
}
};
const getDisplayText = () => {
if (selectedUserIds.length === 0) return 'All Users';
if (selectedUserIds.length === 1) {
const user = eligibleUsers.find((u: User) => u.id === selectedUserIds[0]);
return user ? `${user.mobile}${user.name ? ` - ${user.name}` : ''}` : '1 user selected';
}
return `${selectedUserIds.length} users selected`;
};
if (isLoadingUsers) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center`}>
<ActivityIndicator size="large" color="#3b82f6" />
<MyText style={tw`text-gray-500 mt-4`}>Loading users...</MyText>
</View>
</AppContainer>
);
}
return (
<AppContainer>
<View style={tw`flex-1 bg-gray-50`}>
{/* Header */}
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center`}>
<TouchableOpacity
onPress={() => router.back()}
style={tw`p-2 -ml-4`}
>
<MaterialIcons name="chevron-left" size={24} color="#374151" />
</TouchableOpacity>
<MyText style={tw`text-xl font-bold text-gray-900 ml-2`}>Send Notifications</MyText>
</View>
<ScrollView style={tw`flex-1`} contentContainerStyle={tw`p-4`}>
{/* Title Input */}
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Title</MyText>
<MyTextInput
value={title}
onChangeText={setTitle}
placeholder="Enter notification title..."
style={tw`text-gray-900`}
/>
</View>
{/* Message Input */}
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Message</MyText>
<MyTextInput
value={message}
onChangeText={setMessage}
placeholder="Enter notification message..."
multiline
numberOfLines={4}
style={tw`text-gray-900`}
/>
</View>
{/* Image Upload - Hidden for now */}
{/* <View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Image (Optional)</MyText>
<ImageUploader
images={displayImage ? [displayImage] : []}
existingImageUrls={[]}
onAddImage={handleImagePick}
onRemoveImage={handleRemoveImage}
/>
</View> */}
{/* User Selection */}
<View style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-4 shadow-sm`}>
<MyText style={tw`text-base font-bold text-gray-900 mb-3`}>Select Users (Optional)</MyText>
<BottomDropdown
label="Select Users"
value={selectedUserIds}
options={dropdownOptions}
onValueChange={(value) => setSelectedUserIds(value as number[])}
multiple={true}
placeholder="Select users"
onSearch={(query) => setSearchQuery(query)}
/>
<MyText style={tw`text-gray-500 text-sm mt-2`}>
{getDisplayText()}
</MyText>
<MyText style={tw`text-blue-600 text-xs mt-1`}>
Leave empty to send to all users
</MyText>
</View>
{/* Submit Button */}
<TouchableOpacity
onPress={handleSend}
disabled={sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0}
style={tw`${
sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0
? 'bg-gray-300'
: 'bg-blue-600'
} rounded-xl py-4 items-center shadow-sm`}
>
<MyText style={tw`text-white font-bold text-base`}>
{sendNotification.isPending ? 'Sending...' : selectedUserIds.length === 0 ? 'Send to All Users' : 'Send Notification'}
</MyText>
</TouchableOpacity>
</ScrollView>
</View>
</AppContainer>
);
}

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { MaterialCommunityIcons, Entypo } from '@expo/vector-icons'; import { MaterialCommunityIcons, Entypo } from '@expo/vector-icons';
import { View, TouchableOpacity, FlatList, Alert, ActivityIndicator } from 'react-native'; import { View, TouchableOpacity, FlatList, Alert } from 'react-native';
import { AppContainer, MyText, tw, MyFlatList , BottomDialog, MyTouchableOpacity, Checkbox } from 'common-ui'; import { AppContainer, MyText, tw, MyFlatList , BottomDialog, MyTouchableOpacity } from 'common-ui';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -12,7 +12,6 @@ interface SlotItemProps {
router: any; router: any;
setDialogProducts: React.Dispatch<React.SetStateAction<any[]>>; setDialogProducts: React.Dispatch<React.SetStateAction<any[]>>;
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>; setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
refetch: () => void;
} }
const SlotItemComponent: React.FC<SlotItemProps> = ({ const SlotItemComponent: React.FC<SlotItemProps> = ({
@ -20,7 +19,6 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
router, router,
setDialogProducts, setDialogProducts,
setDialogOpen, setDialogOpen,
refetch,
}) => { }) => {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const slotProducts = slot.products?.map((p: any) => p.name).filter(Boolean) || []; const slotProducts = slot.products?.map((p: any) => p.name).filter(Boolean) || [];
@ -30,29 +28,6 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
const statusColor = isActive ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'; const statusColor = isActive ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700';
const statusText = isActive ? 'Active' : 'Inactive'; const statusText = isActive ? 'Active' : 'Inactive';
const updateSlotCapacity = trpc.admin.slots.updateSlotCapacity.useMutation();
const handleCapacityToggle = () => {
updateSlotCapacity.mutate(
{ slotId: slot.id, isCapacityFull: !slot.isCapacityFull },
{
onSuccess: () => {
setMenuOpen(false);
refetch();
Alert.alert(
'Success',
slot.isCapacityFull
? 'Slot capacity reset. It will now be visible to users.'
: 'Slot marked as full capacity. It will be hidden from users.'
);
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to update slot capacity');
},
}
);
};
return ( return (
<TouchableOpacity <TouchableOpacity
onPress={() => router.push(`/(drawer)/slots/slot-details?slotId=${slot.id}`)} onPress={() => router.push(`/(drawer)/slots/slot-details?slotId=${slot.id}`)}
@ -80,15 +55,10 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
<MyText style={tw`text-xs font-bold text-pink1`}>Edit</MyText> <MyText style={tw`text-xs font-bold text-pink1`}>Edit</MyText>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
<View style={tw`px-3 py-1 rounded-full ${statusColor.split(' ')[0]}`}> <View style={tw`px-3 py-1 rounded-full ${statusColor.split(' ')[0]}`}>
<MyText style={tw`text-xs font-bold ${statusColor.split(' ')[1]}`}>{statusText}</MyText> <MyText style={tw`text-xs font-bold ${statusColor.split(' ')[1]}`}>{statusText}</MyText>
</View> </View>
{slot.isCapacityFull && ( <TouchableOpacity
<View style={tw`px-2 py-1 rounded-full bg-red-500 ml-2`}>
<MyText style={tw`text-xs font-bold text-white`}>FULL</MyText>
</View>
)}
<TouchableOpacity
onPress={() => setMenuOpen(true)} onPress={() => setMenuOpen(true)}
style={tw`ml-2 p-1`} style={tw`ml-2 p-1`}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
@ -98,75 +68,33 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
</View> </View>
</View> </View>
{/* Replicate Menu Dialog */} {/* Replicate Menu Dialog */}
<BottomDialog open={menuOpen} onClose={() => setMenuOpen(false)}> <BottomDialog open={menuOpen} onClose={() => setMenuOpen(false)}>
<View style={tw`p-4`}> <View style={tw`p-4`}>
<MyText style={tw`text-lg font-bold mb-4`}>Slot #{slot.id} Actions</MyText> <MyText style={tw`text-lg font-bold mb-4`}>Slot #{slot.id} Actions</MyText>
{/* Capacity Toggle */}
<TouchableOpacity <TouchableOpacity
onPress={handleCapacityToggle} onPress={() => {
disabled={updateSlotCapacity.isPending} setMenuOpen(false);
router.push(`/slots/add?baseslot=${slot.id}` as any);
}}
style={tw`py-4 border-b border-gray-200`} style={tw`py-4 border-b border-gray-200`}
>
<View style={tw`flex-row items-center justify-between`}>
<View style={tw`flex-row items-center flex-1`}>
{updateSlotCapacity.isPending ? (
<ActivityIndicator size="small" color="#EF4444" style={tw`mr-3`} />
) : (
<MaterialCommunityIcons
name={slot.isCapacityFull ? "package-variant-closed" : "package-variant"}
size={20}
color={slot.isCapacityFull ? "#EF4444" : "#4B5563"}
style={tw`mr-3`}
/>
)}
<View>
<MyText style={tw`text-base text-gray-800`}>Mark as Full Capacity</MyText>
<MyText style={tw`text-xs text-gray-500 mt-0.5`}>
{slot.isCapacityFull
? "Slot is hidden from users"
: "Hidden from users when full"}
</MyText>
</View>
</View>
{updateSlotCapacity.isPending ? (
<ActivityIndicator size="small" color="#EF4444" />
) : (
<Checkbox
checked={slot.isCapacityFull}
onPress={handleCapacityToggle}
size={22}
fillColor="#EF4444"
checkColor="#FFFFFF"
/>
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setMenuOpen(false);
router.push(`/slots/add?baseslot=${slot.id}` as any);
}}
style={tw`py-4 border-b border-gray-200`}
>
<View style={tw`flex-row items-center`}>
<MaterialCommunityIcons name="content-copy" size={20} color="#4B5563" style={tw`mr-3`} />
<MyText style={tw`text-base text-gray-800`}>Replicate Slot</MyText>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setMenuOpen(false)}
style={tw`py-4 mt-2`}
> >
<View style={tw`flex-row items-center`}> <View style={tw`flex-row items-center`}>
<MaterialCommunityIcons name="close" size={20} color="#EF4444" style={tw`mr-3`} /> <MaterialCommunityIcons name="content-copy" size={20} color="#4B5563" style={tw`mr-3`} />
<MyText style={tw`text-base text-red-500`}>Cancel</MyText> <MyText style={tw`text-base text-gray-800`}>Replicate Slot</MyText>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</View> <TouchableOpacity
</BottomDialog> onPress={() => setMenuOpen(false)}
style={tw`py-4 mt-2`}
>
<View style={tw`flex-row items-center`}>
<MaterialCommunityIcons name="close" size={20} color="#EF4444" style={tw`mr-3`} />
<MyText style={tw`text-base text-red-500`}>Cancel</MyText>
</View>
</TouchableOpacity>
</View>
</BottomDialog>
{/* Divider */} {/* Divider */}
<View style={tw`h-[1px] bg-gray-100 mb-4`} /> <View style={tw`h-[1px] bg-gray-100 mb-4`} />
@ -265,7 +193,6 @@ export default function Slots() {
router={router} router={router}
setDialogProducts={setDialogProducts} setDialogProducts={setDialogProducts}
setDialogOpen={setDialogOpen} setDialogOpen={setDialogOpen}
refetch={refetch}
/> />
)} )}
contentContainerStyle={tw`p-4`} contentContainerStyle={tw`p-4`}
@ -275,8 +202,6 @@ export default function Slots() {
{/* FAB for Add New Slot */} {/* FAB for Add New Slot */}
<MyTouchableOpacity <MyTouchableOpacity
testID="add-slot-fab"
accessibilityLabel="add-slot-fab"
onPress={() => router.push('/slots/add' as any)} onPress={() => router.push('/slots/add' as any)}
activeOpacity={0.95} activeOpacity={0.95}
style={{ position: 'absolute', bottom: 32, right: 24, zIndex: 100 }} style={{ position: 'absolute', bottom: 32, right: 24, zIndex: 100 }}

View file

@ -9,14 +9,10 @@ export default function AddStore() {
const router = useRouter(); const router = useRouter();
const createStoreMutation = trpc.admin.store.createStore.useMutation(); const createStoreMutation = trpc.admin.store.createStore.useMutation();
const { refetch: refetchStores } = trpc.admin.store.getStores.useQuery(undefined, {
enabled: false,
});
const handleSubmit = (values: StoreFormData) => { const handleSubmit = (values: StoreFormData) => {
createStoreMutation.mutate(values, { createStoreMutation.mutate(values, {
onSuccess: async (data) => { onSuccess: (data) => {
await refetchStores();
Alert.alert('Success', data.message); Alert.alert('Success', data.message);
router.push('/stores' as any); // Navigate back to stores list router.push('/stores' as any); // Navigate back to stores list
}, },
@ -39,4 +35,4 @@ export default function AddStore() {
</View> </View>
</AppContainer> </AppContainer>
); );
} }

View file

@ -0,0 +1,158 @@
import React from 'react';
import { View, TouchableOpacity, Alert } from 'react-native';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { AppContainer, MyText, tw } from 'common-ui';
import { trpc } from '@/src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import dayjs from 'dayjs';
export default function UserDetails() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const { data: userData, isLoading, error, refetch } = trpc.admin.staffUser.getUserDetails.useQuery(
{ userId: id ? parseInt(id) : 0 },
{ enabled: !!id }
);
const updateSuspensionMutation = trpc.admin.staffUser.updateUserSuspension.useMutation({
onSuccess: () => {
refetch();
Alert.alert('Success', 'User suspension status updated');
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to update suspension');
},
});
const handleToggleSuspension = () => {
if (!userData) return;
const newStatus = !userData.isSuspended;
updateSuspensionMutation.mutate({
userId: userData.id,
isSuspended: newStatus,
});
};
if (isLoading) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center`}>
<MyText style={tw`text-gray-500`}>Loading user details...</MyText>
</View>
</AppContainer>
);
}
if (error || !userData) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center`}>
<MaterialIcons name="error-outline" size={48} color="#EF4444" />
<MyText style={tw`text-gray-900 text-xl font-bold mt-4 mb-2`}>
Error
</MyText>
<MyText style={tw`text-gray-500 text-center`}>
{error?.message || "Failed to load user details"}
</MyText>
<TouchableOpacity
onPress={() => router.back()}
style={tw`mt-6 bg-gray-900 px-6 py-3 rounded-xl`}
>
<MyText style={tw`text-white font-bold`}>Go Back</MyText>
</TouchableOpacity>
</View>
</AppContainer>
);
}
const user = userData;
return (
<AppContainer>
<View style={tw`flex-1 bg-gray-50`}>
{/* User Info */}
<View style={tw`p-4`}>
<View style={tw`bg-white rounded-2xl shadow-sm border border-gray-100 p-6 mb-4`}>
<View style={tw`flex-row items-center mb-6`}>
<View style={tw`w-16 h-16 bg-blue-100 rounded-full items-center justify-center mr-4`}>
<MaterialIcons name="person" size={32} color="#3B82F6" />
</View>
<View>
<MyText style={tw`text-2xl font-bold text-gray-900`}>{user.name || 'n/a'}</MyText>
<MyText style={tw`text-sm text-gray-500`}>User ID: {user.id}</MyText>
</View>
</View>
<View style={tw`space-y-4`}>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="phone" size={20} color="#6B7280" style={tw`mr-3 w-5`} />
<MyText style={tw`text-gray-700`}>{user.mobile}</MyText>
</View>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="email" size={20} color="#6B7280" style={tw`mr-3 w-5`} />
<MyText style={tw`text-gray-700`}>{user.email}</MyText>
</View>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="calendar-today" size={20} color="#6B7280" style={tw`mr-3 w-5`} />
<MyText style={tw`text-gray-700`}>
Added on {dayjs(user.addedOn).format('MMM DD, YYYY')}
</MyText>
</View>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="shopping-cart" size={20} color="#6B7280" style={tw`mr-3 w-5`} />
<MyText style={tw`text-gray-700`}>
{user.lastOrdered
? `Last ordered ${dayjs(user.lastOrdered).format('MMM DD, YYYY')}`
: 'No orders yet'
}
</MyText>
</View>
</View>
</View>
{/* Suspension Status */}
<View style={tw`bg-white rounded-2xl shadow-sm border border-gray-100 p-6 mb-4`}>
<View style={tw`flex-row items-center justify-between`}>
<View style={tw`flex-row items-center`}>
<MaterialIcons
name={user.isSuspended ? "block" : "check-circle"}
size={24}
color={user.isSuspended ? "#EF4444" : "#10B981"}
style={tw`mr-3`}
/>
<View>
<MyText style={tw`font-semibold text-gray-900`}>
{user.isSuspended ? 'Suspended' : 'Active'}
</MyText>
<MyText style={tw`text-sm text-gray-500`}>
{user.isSuspended ? 'User is suspended' : 'User is active'}
</MyText>
</View>
</View>
<TouchableOpacity
onPress={handleToggleSuspension}
disabled={updateSuspensionMutation.isPending}
style={tw`px-4 py-2 rounded-lg ${
user.isSuspended ? 'bg-green-500' : 'bg-red-500'
} ${updateSuspensionMutation.isPending ? 'opacity-50' : ''}`}
>
<MyText style={tw`text-white font-semibold text-sm`}>
{updateSuspensionMutation.isPending
? 'Updating...'
: user.isSuspended
? 'Revoke Suspension'
: 'Suspend User'
}
</MyText>
</TouchableOpacity>
</View>
</View>
</View>
</View>
</AppContainer>
);
}

View file

@ -1,258 +0,0 @@
import React, { useCallback } from 'react';
import {
View,
TouchableOpacity,
ActivityIndicator,
ScrollView,
} from 'react-native';
import { useRouter, useLocalSearchParams } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import {
AppContainer,
MyText,
tw,
Checkbox,
} from 'common-ui';
import { trpc } from '@/src/trpc-client';
import { formatDistanceToNow } from 'date-fns';
import dayjs from 'dayjs';
import { UserIncidentsView } from '@/components/UserIncidentsView';
interface Order {
id: number;
readableId: number;
totalAmount: string;
createdAt: string;
status: string;
isFlashDelivery: boolean;
itemCount: number;
}
interface OrderItemProps {
order: Order;
onPress: () => void;
}
const getStatusColor = (status: string) => {
switch (status) {
case 'delivered':
return 'text-green-600 bg-green-50 border-green-100';
case 'cancelled':
return 'text-red-600 bg-red-50 border-red-100';
default:
return 'text-yellow-600 bg-yellow-50 border-yellow-100';
}
};
const OrderItem: React.FC<OrderItemProps> = ({ order, onPress }) => {
const statusStyle = getStatusColor(order.status);
return (
<TouchableOpacity
onPress={onPress}
activeOpacity={0.7}
style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-3 shadow-sm`}
>
<View style={tw`flex-row items-center justify-between mb-3`}>
<View style={tw`flex-row items-center`}>
<MyText style={tw`text-lg font-bold text-gray-900`}>
#{order.readableId}
</MyText>
{order.isFlashDelivery && (
<View style={tw`ml-2 px-2 py-0.5 bg-amber-100 rounded-full border border-amber-200`}>
<MyText style={tw`text-[10px] font-black text-amber-700 uppercase`}></MyText>
</View>
)}
</View>
<View style={tw`px-3 py-1 rounded-full border ${statusStyle}`}>
<MyText style={tw`text-xs font-bold uppercase tracking-wider ${statusStyle.split(' ')[0]}`}>
{order.status}
</MyText>
</View>
</View>
<View style={tw`flex-row justify-between items-center`}>
<View>
<MyText style={tw`text-gray-500 text-sm mb-1`}>
{dayjs(order.createdAt).format('MMM DD, YYYY • h:mm A')}
</MyText>
<MyText style={tw`text-gray-400 text-xs`}>
{order.itemCount} {order.itemCount === 1 ? 'item' : 'items'}
</MyText>
</View>
<MyText style={tw`text-xl font-bold text-blue-600`}>
{order.totalAmount}
</MyText>
</View>
<View style={tw`mt-3 pt-3 border-t border-gray-100 flex-row items-center justify-center`}>
<MyText style={tw`text-blue-600 font-medium text-sm`}>
View Order Details
</MyText>
<MaterialIcons name="chevron-right" size={18} color="#3b82f6" />
</View>
</TouchableOpacity>
);
};
export default function UserDetails() {
const router = useRouter();
const { id } = useLocalSearchParams<{ id: string }>();
const userId = id ? parseInt(id) : 0;
const { data, isLoading, error, refetch } = trpc.admin.user.getUserDetails.useQuery(
{ userId },
{ enabled: !!userId }
);
const updateSuspension = trpc.admin.user.updateUserSuspension.useMutation({
onSuccess: () => {
refetch();
},
});
const handleOrderPress = useCallback((orderId: number) => {
router.push(`/(drawer)/manage-orders/order-details/${orderId}`);
}, [router]);
const handleSuspensionToggle = useCallback((isSuspended: boolean) => {
updateSuspension.mutate({ userId, isSuspended });
}, [userId, updateSuspension]);
if (isLoading) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center`}>
<ActivityIndicator size="large" color="#3b82f6" />
<MyText style={tw`text-gray-500 mt-4`}>Loading user details...</MyText>
</View>
</AppContainer>
);
}
if (error || !data) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center p-8`}>
<MaterialIcons name="error-outline" size={64} color="#ef4444" />
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Error</MyText>
<MyText style={tw`text-gray-500 mt-2 text-center`}>
{error?.message || 'Failed to load user details'}
</MyText>
<TouchableOpacity
onPress={() => refetch()}
style={tw`mt-6 bg-blue-600 px-6 py-3 rounded-full`}
>
<MyText style={tw`text-white font-semibold`}>Retry</MyText>
</TouchableOpacity>
</View>
</AppContainer>
);
}
const { user, orders } = data;
const displayName = user.name || 'Unnamed User';
return (
<AppContainer>
<View style={tw`flex-1 bg-gray-50`}>
{/* Header */}
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center`}>
<TouchableOpacity
onPress={() => router.back()}
style={tw`p-2 -ml-4`}
>
<MaterialIcons name="chevron-left" size={24} color="#374151" />
</TouchableOpacity>
<MyText style={tw`text-xl font-bold text-gray-900 ml-2`}>User Details</MyText>
</View>
<ScrollView showsVerticalScrollIndicator={false}>
{/* User Info Card */}
<View style={tw`bg-white p-5 m-4 rounded-2xl shadow-sm border border-gray-100`}>
<View style={tw`flex-row items-center mb-4`}>
<View style={tw`w-12 h-12 bg-blue-50 rounded-full items-center justify-center mr-4`}>
<MaterialIcons name="person" size={24} color="#3B82F6" />
</View>
<View style={tw`flex-1`}>
<View style={tw`flex-row items-center`}>
<MyText style={tw`text-gray-900 font-bold text-lg mb-0.5`}>
{user.mobile || 'No Mobile'}
</MyText>
{user.isSuspended && (
<View style={tw`ml-2 px-2 py-0.5 bg-red-100 rounded-full border border-red-200`}>
<MyText style={tw`text-[10px] font-bold text-red-700 uppercase`}>Suspended</MyText>
</View>
)}
</View>
<MyText style={tw`text-gray-500`}>
{displayName}
</MyText>
</View>
</View>
<View style={tw`bg-gray-50 p-3 rounded-xl mb-4`}>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="access-time" size={18} color="#6B7280" />
<MyText style={tw`ml-2 text-gray-600`}>
Registered {formatDistanceToNow(new Date(user.createdAt), { addSuffix: true })}
</MyText>
</View>
</View>
{/* Suspension Toggle */}
<View style={tw`flex-row items-center justify-between pt-4 border-t border-gray-100`}>
<View style={tw`flex-1`}>
<MyText style={tw`text-gray-900 font-bold text-base mb-1`}>
Suspend User
</MyText>
<MyText style={tw`text-gray-500 text-sm`}>
Prevent user from placing orders
</MyText>
</View>
{updateSuspension.isPending ? (
<ActivityIndicator size="small" color="#3b82f6" />
) : (
<Checkbox
checked={user.isSuspended}
onPress={() => handleSuspensionToggle(!user.isSuspended)}
size={28}
/>
)}
</View>
</View>
{/* User Incidents Section */}
<UserIncidentsView userId={userId} orderId={null} />
{/* Orders Section */}
<View style={tw`px-4 pb-8`}>
<View style={tw`flex-row items-center justify-between mb-4`}>
<MyText style={tw`text-lg font-bold text-gray-900`}>Order History</MyText>
<MyText style={tw`text-gray-500 text-sm`}>
{orders.length} {orders.length === 1 ? 'order' : 'orders'}
</MyText>
</View>
{orders.length === 0 ? (
<View style={tw`bg-white rounded-xl border border-gray-100 p-8 items-center`}>
<MaterialIcons name="shopping-bag" size={48} color="#e5e7eb" />
<MyText style={tw`text-gray-500 mt-4 text-center`}>
No orders yet
</MyText>
</View>
) : (
orders.map((order) => (
<OrderItem
key={order.id}
order={order}
onPress={() => handleOrderPress(order.id)}
/>
))
)}
</View>
</ScrollView>
</View>
</AppContainer>
);
}

View file

@ -1,268 +0,0 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import {
View,
TouchableOpacity,
Alert,
RefreshControl,
ActivityIndicator,
} from 'react-native';
import { useRouter } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import {
AppContainer,
MyText,
tw,
SearchBar,
MyFlatList,
} from 'common-ui';
import { trpc } from '@/src/trpc-client';
import { formatDistanceToNow } from 'date-fns';
interface User {
id: number;
name: string | null;
mobile: string | null;
createdAt: string;
totalOrders: number;
lastOrderDate: string | null;
isSuspended: boolean;
}
interface UserItemProps {
user: User;
index: number;
onPress: () => void;
}
const UserItem: React.FC<UserItemProps> = ({ user, index, onPress }) => {
const displayName = user.name || 'Unnamed User';
const hasOrders = user.totalOrders > 0;
const lastOrderText = user.lastOrderDate
? formatDistanceToNow(new Date(user.lastOrderDate), { addSuffix: true })
: 'Never';
return (
<TouchableOpacity
onPress={onPress}
activeOpacity={0.7}
style={tw`bg-white rounded-xl border border-gray-100 p-4 mb-3 shadow-sm`}
>
<View style={tw`flex-row items-center justify-between`}>
{/* Left: Index number */}
<View style={tw`w-8 h-8 rounded-full bg-gray-100 items-center justify-center mr-3`}>
<MyText style={tw`text-gray-600 text-xs font-bold`}>{index + 1}</MyText>
</View>
{/* Middle: User Info */}
<View style={tw`flex-1`}>
{/* Mobile number - primary identifier */}
<View style={tw`flex-row items-center mb-0.5`}>
<MyText style={tw`text-gray-900 font-bold text-base`}>
{user.mobile || 'No Mobile'}
</MyText>
{user.isSuspended && (
<View style={tw`ml-2 px-2 py-0.5 bg-red-100 rounded-full border border-red-200`}>
<MyText style={tw`text-[10px] font-bold text-red-700 uppercase`}>Suspended</MyText>
</View>
)}
</View>
{/* Name */}
<MyText style={tw`text-gray-500 text-sm mb-1`}>
{displayName}
</MyText>
{/* Registration date */}
<MyText style={tw`text-gray-400 text-xs`}>
Registered: {formatDistanceToNow(new Date(user.createdAt), { addSuffix: true })}
</MyText>
</View>
{/* Right: Order Stats */}
<View style={tw`items-end`}>
{/* Total Orders */}
<View style={tw`flex-row items-center mb-1`}>
<MaterialIcons name="shopping-bag" size={14} color={hasOrders ? '#10B981' : '#9CA3AF'} />
<MyText style={tw`${hasOrders ? 'text-green-600' : 'text-gray-400'} font-bold text-sm ml-1`}>
{user.totalOrders} orders
</MyText>
</View>
{/* Last Order */}
<MyText style={tw`text-gray-400 text-xs`}>
Last: {lastOrderText}
</MyText>
</View>
</View>
</TouchableOpacity>
);
};
interface ListHeaderProps {
searchTerm: string;
onSearchChange: (text: string) => void;
userCount: number;
}
const ListHeader: React.FC<ListHeaderProps> = ({ searchTerm, onSearchChange, userCount }) => (
<View>
{/* Search Bar */}
<View style={tw`px-4 py-3 bg-white border-b border-gray-100`}>
<SearchBar
value={searchTerm}
onChangeText={onSearchChange}
placeholder="Search by mobile number..."
/>
</View>
{/* Stats Summary */}
<View style={tw`px-4 py-2 bg-gray-50`}>
<MyText style={tw`text-gray-500 text-sm`}>
Showing {userCount} users
</MyText>
</View>
</View>
);
const ListFooter: React.FC<{ isFetching: boolean }> = ({ isFetching }) => {
if (!isFetching) return null;
return (
<View style={tw`py-4 items-center`}>
<ActivityIndicator size="small" color="#3b82f6" />
</View>
);
};
export default function UserManagement() {
const router = useRouter();
const [searchTerm, setSearchTerm] = useState('');
const [refreshing, setRefreshing] = useState(false);
const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
// Infinite scroll query
const {
data,
isLoading,
isError,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
refetch,
} = trpc.admin.user.getAllUsers.useInfiniteQuery(
{
limit: 30,
search: searchTerm || undefined,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
useEffect(() => {
if (data?.pages?.length) {
setHasLoadedOnce(true);
}
}, [data]);
// Flatten all pages and remove duplicates
const users = useMemo(() => {
const allUsers = data?.pages.flatMap((page) => page.users) || [];
// Remove duplicates based on user id
const uniqueUsers = allUsers.filter((user, index, self) =>
index === self.findIndex((u) => u.id === user.id)
);
return uniqueUsers;
}, [data]);
const handleRefresh = useCallback(async () => {
setRefreshing(true);
await refetch();
setRefreshing(false);
}, [refetch]);
const handleLoadMore = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const handleUserPress = useCallback((userId: number) => {
router.push(`/(drawer)/user-management/${userId}`);
}, [router]);
const renderUserItem = useCallback(({ item, index }: { item: User; index: number }) => {
return <UserItem user={item} index={index} onPress={() => handleUserPress(item.id)} />;
}, [handleUserPress]);
if (isLoading && !hasLoadedOnce) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center`}>
<ActivityIndicator size="large" color="#3b82f6" />
<MyText style={tw`text-gray-500 mt-4`}>Loading users...</MyText>
</View>
</AppContainer>
);
}
if (isError) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center p-8`}>
<MaterialIcons name="error-outline" size={64} color="#ef4444" />
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Error</MyText>
<MyText style={tw`text-gray-500 mt-2 text-center`}>
{error?.message || 'Failed to load users'}
</MyText>
<TouchableOpacity
onPress={() => refetch()}
style={tw`mt-6 bg-blue-600 px-6 py-3 rounded-full`}
>
<MyText style={tw`text-white font-semibold`}>Retry</MyText>
</TouchableOpacity>
</View>
</AppContainer>
);
}
return (
<View style={tw`flex-1 bg-gray-50`}>
{/* Users List */}
<MyFlatList
data={users}
renderItem={renderUserItem}
keyExtractor={(item) => item.id.toString()}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.5}
ListHeaderComponent={
<ListHeader
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
userCount={users.length}
/>
}
ListFooterComponent={<ListFooter isFetching={isFetchingNextPage} />}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
contentContainerStyle={tw`pb-8`}
stickyHeaderIndices={[0]}
ListEmptyComponent={
<View style={tw`flex-1 justify-center items-center py-12`}>
<MaterialIcons name="people-outline" size={64} color="#e5e7eb" />
<MyText style={tw`text-gray-500 mt-4 text-center text-lg`}>
No users found
</MyText>
{searchTerm && (
<MyText style={tw`text-gray-400 mt-1 text-center`}>
Try adjusting your search
</MyText>
)}
</View>
}
/>
</View>
);
}

View file

@ -54,7 +54,7 @@ export default function Users() {
const users = data?.pages.flatMap(page => page.users) || []; const users = data?.pages.flatMap(page => page.users) || [];
const handleUserPress = (userId: string) => { const handleUserPress = (userId: string) => {
router.push(`/(drawer)/user-management/${userId}`); router.push(`/user-details/${userId}`);
}; };
return ( return (

View file

@ -11,14 +11,10 @@ export default function Layout() {
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<trpc.Provider client={trpcClient} queryClient={queryClient}> <trpc.Provider client={trpcClient} queryClient={queryClient}>
<StaffAuthProvider> <StaffAuthProvider>
<SafeAreaView <SafeAreaView edges={['left', 'right', 'bottom']} style={{ flex: 1, backgroundColor: '#fff' }}>
edges={['left', 'right', 'bottom']} <RefreshProvider queryClient={queryClient}>
style={{ flex: 1, backgroundColor: '#fff' }} <Stack screenOptions={{ headerShown: false }} />
testID="app-root" </RefreshProvider>
>
<RefreshProvider queryClient={queryClient}>
<Stack screenOptions={{ headerShown: false }} />
</RefreshProvider>
</SafeAreaView> </SafeAreaView>
</StaffAuthProvider> </StaffAuthProvider>
</trpc.Provider> </trpc.Provider>

View file

@ -26,6 +26,7 @@ export default function LoginScreen() {
} }
}; };
console.log('from the login page')
return ( return (
@ -51,8 +52,6 @@ export default function LoginScreen() {
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
style={{ marginBottom: 20 }} style={{ marginBottom: 20 }}
testID="login-name-input"
accessibilityLabel="login-name-input"
/> />
<MyTextInput <MyTextInput
@ -64,8 +63,6 @@ export default function LoginScreen() {
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
style={{ marginBottom: 30 }} style={{ marginBottom: 30 }}
testID="login-password-input"
accessibilityLabel="login-password-input"
/> />
{loginError && ( {loginError && (
@ -87,10 +84,8 @@ export default function LoginScreen() {
disabled={isLoggingIn} disabled={isLoggingIn}
fullWidth fullWidth
style={{ marginBottom: 20 }} style={{ marginBottom: 20 }}
testID="login-button"
accessibilityLabel="login-button"
/> />
</View> </View>
</AppContainer> </AppContainer>
); );
} }

View file

@ -7,7 +7,6 @@ import { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
import ProductsSelector from './ProductsSelector'; import ProductsSelector from './ProductsSelector';
import { trpc } from '../src/trpc-client'; import { trpc } from '../src/trpc-client';
import usePickImage from 'common-ui/src/components/use-pick-image'; import usePickImage from 'common-ui/src/components/use-pick-image';
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStore';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
export interface BannerFormData { export interface BannerFormData {
@ -53,10 +52,10 @@ export default function BannerForm({
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]); const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]); const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
const { uploadSingle } = useUploadToObjectStorage(); const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
// Fetch products for dropdown // Fetch products for dropdown
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery(); const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
const products = productsData?.products || []; const products = productsData?.products || [];
@ -98,11 +97,33 @@ export default function BannerForm({
let imageUrl: string | undefined; let imageUrl: string | undefined;
if (selectedImages.length > 0) { if (selectedImages.length > 0) {
// Generate upload URLs
const mimeTypes = selectedImages.map(s => s.mimeType);
const { uploadUrls } = await generateUploadUrls.mutateAsync({
contextString: 'store', // Using 'store' for now
mimeTypes,
});
// Upload image
const uploadUrl = uploadUrls[0];
const { blob, mimeType } = selectedImages[0]; const { blob, mimeType } = selectedImages[0];
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store');
imageUrl = presignedUrl; const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
body: blob,
headers: {
'Content-Type': mimeType,
},
});
if (!uploadResponse.ok) {
throw new Error(`Upload failed with status ${uploadResponse.status}`);
}
imageUrl = uploadUrl;
} }
// Call onSubmit with form values and imageUrl
await onSubmit(values, imageUrl); await onSubmit(values, imageUrl);
} catch (error) { } catch (error) {
console.error('Upload error:', error); console.error('Upload error:', error);
@ -235,4 +256,4 @@ export default function BannerForm({
)} )}
</Formik> </Formik>
); );
} }

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>
);
};

View file

@ -103,18 +103,6 @@ export function OrderOptionsMenu({
} }
}; };
const handleOpenInMaps = () => {
if (order.latitude && order.longitude) {
const url = `https://www.google.com/maps/search/?api=1&query=${order.latitude},${order.longitude}`;
Linking.openURL(url);
} else {
Alert.alert('No location coordinates available');
}
};
const hasCoordinates = order.latitude !== null && order.latitude !== undefined &&
order.longitude !== null && order.longitude !== undefined;
return ( return (
<BottomDialog open={open} onClose={onClose}> <BottomDialog open={open} onClose={onClose}>
<View style={{ maxHeight: SCREEN_HEIGHT * 0.7 }}> <View style={{ maxHeight: SCREEN_HEIGHT * 0.7 }}>
@ -269,29 +257,6 @@ export function OrderOptionsMenu({
<MaterialIcons name="chevron-right" size={24} color="#9ca3af" style={tw`ml-auto`} /> <MaterialIcons name="chevron-right" size={24} color="#9ca3af" style={tw`ml-auto`} />
</TouchableOpacity> </TouchableOpacity>
{hasCoordinates && (
<TouchableOpacity
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}
onPress={() => {
handleOpenInMaps();
onClose();
}}
>
<View style={tw`w-10 h-10 rounded-full bg-blue-50 items-center justify-center mr-4`}>
<MaterialIcons name="map" size={20} color="#2563eb" />
</View>
<View>
<MyText style={tw`font-semibold text-gray-800 text-base`}>
Open in Maps
</MyText>
<MyText style={tw`text-gray-500 text-xs`}>
View delivery location on Google Maps
</MyText>
</View>
<MaterialIcons name="chevron-right" size={24} color="#9ca3af" style={tw`ml-auto`} />
</TouchableOpacity>
)}
<TouchableOpacity <TouchableOpacity
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`} style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}
onPress={() => { onPress={() => {

View file

@ -147,7 +147,6 @@ export default function ProductsSelector({
{showGroups && groups.length > 0 && ( {showGroups && groups.length > 0 && (
<View style={tw`mb-4`}> <View style={tw`mb-4`}>
<BottomDropdown <BottomDropdown
testID="product-groups-dropdown"
label="Select Product Groups" label="Select Product Groups"
options={groupOptions} options={groupOptions}
value={selectedGroupIds.map(id => id.toString())} value={selectedGroupIds.map(id => id.toString())}

View file

@ -73,11 +73,6 @@ export default function SlotForm({
return; return;
} }
if (values.freezeTime > values.deliveryTime) {
Alert.alert('Error', 'Freeze time must be before or equal to delivery time');
return;
}
const slotData = { const slotData = {
deliveryTime: values.deliveryTime.toISOString(), deliveryTime: values.deliveryTime.toISOString(),
freezeTime: values.freezeTime.toISOString(), freezeTime: values.freezeTime.toISOString(),
@ -148,22 +143,12 @@ export default function SlotForm({
<View style={tw`mb-4`}> <View style={tw`mb-4`}>
<Text style={tw`text-lg font-semibold mb-2`}>Delivery Date & Time</Text> <Text style={tw`text-lg font-semibold mb-2`}>Delivery Date & Time</Text>
<DateTimePickerMod <DateTimePickerMod value={values.deliveryTime} setValue={(value) => setFieldValue('deliveryTime', value)} />
dateTestID="delivery-date-picker"
timeTestID="delivery-time-picker"
value={values.deliveryTime}
setValue={(value) => setFieldValue('deliveryTime', value)}
/>
</View> </View>
<View style={tw`mb-4`}> <View style={tw`mb-4`}>
<Text style={tw`text-lg font-semibold mb-2`}>Freeze Date & Time</Text> <Text style={tw`text-lg font-semibold mb-2`}>Freeze Date & Time</Text>
<DateTimePickerMod <DateTimePickerMod value={values.freezeTime} setValue={(value) => setFieldValue('freezeTime', value)} />
dateTestID="freeze-date-picker"
timeTestID="freeze-time-picker"
value={values.freezeTime}
setValue={(value) => setFieldValue('freezeTime', value)}
/>
</View> </View>
<View style={tw`mb-4`}> <View style={tw`mb-4`}>
@ -230,8 +215,6 @@ export default function SlotForm({
</FieldArray> </FieldArray>
<TouchableOpacity <TouchableOpacity
testID="create-slot-button"
accessibilityLabel="create-slot-button"
onPress={() => handleSubmit()} onPress={() => handleSubmit()}
disabled={isPending} disabled={isPending}
style={tw`${isPending ? 'bg-pink2' : 'bg-pink1'} p-3 rounded-lg items-center mt-6 pb-4`} style={tw`${isPending ? 'bg-pink2' : 'bg-pink1'} p-3 rounded-lg items-center mt-6 pb-4`}

View file

@ -6,7 +6,6 @@ import { MyTextInput, BottomDropdown, MyText, tw, ImageUploader } from 'common-u
import ProductsSelector from './ProductsSelector'; import ProductsSelector from './ProductsSelector';
import { trpc } from '../src/trpc-client'; import { trpc } from '../src/trpc-client';
import usePickImage from 'common-ui/src/components/use-pick-image'; import usePickImage from 'common-ui/src/components/use-pick-image';
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStore';
export interface StoreFormData { export interface StoreFormData {
name: string; name: string;
@ -60,19 +59,14 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
}); });
}, [initialValues, initialSelectedProducts]); }, [initialValues, initialSelectedProducts]);
const existingImageUrls = useMemo( const staffOptions = staffData?.staff.map(staff => ({
() => (formInitialValues.imageUrl ? [formInitialValues.imageUrl] : []),
[formInitialValues.imageUrl]
)
const staffOptions = staffData?.staff.map((staff: { id: number; name: string }) => ({
label: staff.name, label: staff.name,
value: staff.id, value: staff.id,
})) || []; })) || [];
const { uploadSingle, isUploading } = useUploadToObjectStorage(); const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
const handleImagePick = usePickImage({ const handleImagePick = usePickImage({
setFile: async (assets: any) => { setFile: async (assets: any) => {
@ -119,11 +113,39 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
let imageUrl: string | undefined; let imageUrl: string | undefined;
if (selectedImages.length > 0) { if (selectedImages.length > 0) {
const { blob, mimeType } = selectedImages[0]; // Generate upload URLs
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store'); const mimeTypes = selectedImages.map(s => s.mimeType);
imageUrl = presignedUrl; const { uploadUrls } = await generateUploadUrls.mutateAsync({
contextString: 'store',
mimeTypes,
});
// Upload images
for (let i = 0; i < uploadUrls.length; i++) {
const uploadUrl = uploadUrls[i];
const { blob, mimeType } = selectedImages[i];
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
body: blob,
headers: {
'Content-Type': mimeType,
},
});
if (!uploadResponse.ok) {
throw new Error(`Upload failed with status ${uploadResponse.status}`);
}
}
// Extract key from first upload URL
// const u = new URL(uploadUrls[0]);
// const rawKey = u.pathname.replace(/^\/+/, "");
// imageUrl = decodeURIComponent(rawKey);
imageUrl = uploadUrls[0];
} }
// Submit form with imageUrl
onSubmit({ ...values, imageUrl }); onSubmit({ ...values, imageUrl });
} catch (error) { } catch (error) {
console.error('Upload error:', error); console.error('Upload error:', error);
@ -173,25 +195,20 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
<MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText> <MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText>
<ImageUploader <ImageUploader
images={displayImages} images={displayImages}
existingImageUrls={existingImageUrls} existingImageUrls={formInitialValues.imageUrl ? [formInitialValues.imageUrl] : []}
onAddImage={handleImagePick} onAddImage={handleImagePick}
onRemoveImage={handleRemoveImage} onRemoveImage={handleRemoveImage}
onRemoveExistingImage={() => onRemoveExistingImage={() => setFormInitialValues({ ...formInitialValues, imageUrl: undefined })}
setFormInitialValues((prev) => ({
...prev,
imageUrl: undefined,
}))
}
allowMultiple={false} allowMultiple={false}
/> />
</View> </View>
<TouchableOpacity <TouchableOpacity
onPress={submit} onPress={submit}
disabled={isLoading || isUploading} disabled={isLoading || generateUploadUrls.isPending}
style={tw`px-4 py-2 rounded-lg shadow-lg items-center mt-2 ${isLoading || isUploading ? 'bg-gray-400' : 'bg-blue-500'}`} style={tw`px-4 py-2 rounded-lg shadow-lg items-center mt-2 ${isLoading || generateUploadUrls.isPending ? 'bg-gray-400' : 'bg-blue-500'}`}
> >
<MyText style={tw`text-white text-lg font-bold`}> <MyText style={tw`text-white text-lg font-bold`}>
{isUploading ? 'Uploading...' : isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Store' : 'Update Store')} {generateUploadUrls.isPending ? 'Uploading...' : isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Store' : 'Update Store')}
</MyText> </MyText>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -203,4 +220,4 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
StoreForm.displayName = 'StoreForm'; StoreForm.displayName = 'StoreForm';
export default StoreForm; export default StoreForm;

View file

@ -1,109 +0,0 @@
import React, { useState } from 'react';
import { View, TouchableOpacity } from 'react-native';
import { MyText, tw, BottomDialog, MyTextInput } from 'common-ui';
import { trpc } from '@/src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { Alert } from 'react-native';
interface UserIncidentDialogProps {
orderId: number;
userId: number;
open: boolean;
onClose: () => void;
onSuccess?: () => void;
}
export default function UserIncidentDialog({ orderId, userId, open, onClose, onSuccess }: UserIncidentDialogProps) {
const [adminComment, setAdminComment] = useState('');
const [negativityScore, setNegativityScore] = useState('');
const addIncidentMutation = trpc.admin.user.addUserIncident.useMutation({
onSuccess: () => {
Alert.alert('Success', 'Incident added successfully');
setAdminComment('');
setNegativityScore('');
onClose();
onSuccess?.();
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to add incident');
},
});
const handleAddIncident = () => {
const score = negativityScore ? parseInt(negativityScore) : undefined;
if (!adminComment.trim() && !negativityScore) {
Alert.alert('Error', 'Please enter a comment or negativity score');
return;
}
addIncidentMutation.mutate({
userId,
orderId,
adminComment: adminComment || undefined,
negativityScore: score,
});
};
return (
<BottomDialog open={open} onClose={onClose}>
<View style={tw`p-6`}>
<View style={tw`items-center mb-6`}>
<View style={tw`w-12 h-12 bg-amber-100 rounded-full items-center justify-center mb-3`}>
<MaterialIcons name="warning" size={24} color="#D97706" />
</View>
<MyText style={tw`text-xl font-bold text-gray-900 text-center`}>
Add User Incident
</MyText>
<MyText style={tw`text-gray-500 text-center mt-2 text-sm leading-5`}>
Record an incident for this user. This will be visible in their profile.
</MyText>
</View>
<MyTextInput
topLabel="Admin Comment"
value={adminComment}
onChangeText={setAdminComment}
placeholder="Enter details about the incident..."
multiline
style={tw`h-24`}
/>
<MyTextInput
topLabel="Negativity Score (Optional)"
value={negativityScore}
onChangeText={setNegativityScore}
placeholder="0"
keyboardType="numeric"
style={tw`mt-4`}
/>
<View style={tw`bg-amber-50 p-4 rounded-xl border border-amber-100 mb-6 mt-4 flex-row items-start`}>
<MaterialIcons name="info-outline" size={20} color="#D97706" style={tw`mt-0.5`} />
<MyText style={tw`text-sm text-amber-800 ml-2 flex-1 leading-5`}>
Higher negativity scores indicate more serious incidents (e.g., repeated cancellations, abusive behavior).
</MyText>
</View>
<View style={tw`flex-row gap-3`}>
<TouchableOpacity
style={tw`flex-1 bg-gray-100 py-3.5 rounded-xl items-center`}
onPress={onClose}
>
<MyText style={tw`text-gray-700 font-bold`}>Cancel</MyText>
</TouchableOpacity>
<TouchableOpacity
style={tw`flex-1 bg-amber-500 py-3.5 rounded-xl items-center shadow-sm ${addIncidentMutation.isPending ? 'opacity-50' : ''}`}
onPress={handleAddIncident}
disabled={addIncidentMutation.isPending}
>
<MyText style={tw`text-white font-bold`}>
{addIncidentMutation.isPending ? 'Adding...' : 'Add Incident'}
</MyText>
</TouchableOpacity>
</View>
</View>
</BottomDialog>
);
}

View file

@ -1,206 +0,0 @@
import React, { useState } from 'react';
import { View, TouchableOpacity, Alert } from 'react-native';
import { MyText, tw, BottomDialog, MyTextInput } from 'common-ui';
import { trpc } from '@/src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import dayjs from 'dayjs';
function UserIncidentDialog({
userId,
orderId,
open,
onClose,
onSuccess
}: {
userId: number;
orderId: number | null;
open: boolean;
onClose: () => void;
onSuccess?: () => void
}) {
const [adminComment, setAdminComment] = useState('');
const [negativityScore, setNegativityScore] = useState('');
const addIncidentMutation = trpc.admin.user.addUserIncident.useMutation({
onSuccess: () => {
Alert.alert('Success', 'Incident added successfully');
setAdminComment('');
setNegativityScore('');
onClose();
onSuccess?.();
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to add incident');
},
});
const handleAddIncident = () => {
const score = negativityScore ? parseInt(negativityScore) : undefined;
if (!adminComment.trim() && !negativityScore) {
Alert.alert('Error', 'Please enter a comment or negativity score');
return;
}
addIncidentMutation.mutate({
userId,
orderId: orderId || undefined,
adminComment: adminComment || undefined,
negativityScore: score,
});
};
return (
<BottomDialog open={open} onClose={onClose}>
<View style={tw`p-6`}>
<View style={tw`items-center mb-6`}>
<View style={tw`w-12 h-12 bg-amber-100 rounded-full items-center justify-center mb-3`}>
<MaterialIcons name="warning" size={24} color="#D97706" />
</View>
<MyText style={tw`text-xl font-bold text-gray-900 text-center`}>
Add User Incident
</MyText>
<MyText style={tw`text-gray-500 text-center mt-2 text-sm leading-5`}>
Record an incident for this user. This will be visible in their profile.
</MyText>
</View>
<MyTextInput
topLabel="Admin Comment"
value={adminComment}
onChangeText={setAdminComment}
placeholder="Enter details about the incident..."
multiline
style={tw`h-24`}
/>
<MyTextInput
topLabel="Negativity Score (Optional)"
value={negativityScore}
onChangeText={setNegativityScore}
placeholder="0"
keyboardType="numeric"
style={tw`mt-4`}
/>
<View style={tw`bg-amber-50 p-4 rounded-xl border border-amber-100 mb-6 mt-4 flex-row items-start`}>
<MaterialIcons name="info-outline" size={20} color="#D97706" style={tw`mt-0.5`} />
<MyText style={tw`text-sm text-amber-800 ml-2 flex-1 leading-5`}>
Higher negativity scores indicate more serious incidents (e.g., repeated cancellations, abusive behavior).
</MyText>
</View>
<View style={tw`flex-row gap-3`}>
<TouchableOpacity
style={tw`flex-1 bg-gray-100 py-3.5 rounded-xl items-center`}
onPress={onClose}
>
<MyText style={tw`text-gray-700 font-bold`}>Cancel</MyText>
</TouchableOpacity>
<TouchableOpacity
style={tw`flex-1 bg-amber-500 py-3.5 rounded-xl items-center shadow-sm ${addIncidentMutation.isPending ? 'opacity-50' : ''}`}
onPress={handleAddIncident}
disabled={addIncidentMutation.isPending}
>
<MyText style={tw`text-white font-bold`}>
{addIncidentMutation.isPending ? 'Adding...' : 'Add Incident'}
</MyText>
</TouchableOpacity>
</View>
</View>
</BottomDialog>
);
}
export function UserIncidentsView({ userId, orderId }: { userId: number; orderId: number | null }) {
const [incidentDialogOpen, setIncidentDialogOpen] = useState(false);
const { data: incidentsData, refetch: refetchIncidents } = trpc.admin.user.getUserIncidents.useQuery(
{ userId },
{ enabled: !!userId }
);
return (
<>
<View style={tw`bg-amber-50 p-5 rounded-2xl shadow-sm mb-4 border border-amber-100`}>
<View style={tw`flex-row items-center justify-between mb-4`}>
<MyText style={tw`text-base font-bold text-amber-900`}>
User Incidents
</MyText>
<TouchableOpacity
onPress={() => setIncidentDialogOpen(true)}
style={tw`flex-row items-center bg-amber-200 px-3 py-1.5 rounded-lg`}
>
<MaterialIcons name="add" size={16} color="#D97706" />
<MyText style={tw`text-xs font-bold text-amber-800 ml-1`}>Add Incident</MyText>
</TouchableOpacity>
</View>
{incidentsData?.incidents && incidentsData.incidents.length > 0 ? (
<View style={tw`space-y-3`}>
{incidentsData.incidents.map((incident: any, index: number) => (
<View
key={incident.id}
style={tw`bg-white p-4 rounded-xl border border-amber-200 ${index === incidentsData.incidents.length - 1 ? 'mb-0' : 'mb-3'}`}
>
<View style={tw`flex-row justify-between items-start mb-2`}>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="event" size={14} color="#6B7280" />
<MyText style={tw`text-xs text-gray-600 ml-1`}>
{dayjs(incident.dateAdded).format('MMM DD, YYYY • h:mm A')}
</MyText>
</View>
{incident.negativityScore && (
<View style={tw`px-2 py-1 bg-red-100 rounded-md`}>
<MyText style={tw`text-xs font-bold text-red-700`}>
Score: {incident.negativityScore}
</MyText>
</View>
)}
</View>
{incident.adminComment && (
<View style={tw`mt-2`}>
<MyText style={tw`text-sm text-gray-900 leading-5`}>
{incident.adminComment}
</MyText>
</View>
)}
<View style={tw`flex-row items-center mt-2 pt-2 border-t border-gray-100`}>
<MaterialIcons name="person" size={12} color="#9CA3AF" />
<MyText style={tw`text-xs text-gray-500 ml-1`}>
Added by {incident.addedBy}
</MyText>
{incident.orderId && (
<>
<MaterialIcons name="shopping-cart" size={12} color="#9CA3AF" style={tw`ml-3`} />
<MyText style={tw`text-xs text-gray-500 ml-1`}>
Order #{incident.orderId}
</MyText>
</>
)}
</View>
</View>
))}
</View>
) : (
<View style={tw`items-center py-6`}>
<MaterialIcons name="check-circle-outline" size={32} color="#D97706" />
<MyText style={tw`text-sm text-amber-700 mt-2`}>
No incidents recorded for this user
</MyText>
</View>
)}
</View>
<UserIncidentDialog
orderId={orderId}
userId={userId}
open={incidentDialogOpen}
onClose={() => setIncidentDialogOpen(false)}
onSuccess={refetchIncidents}
/>
</>
);
}

View file

@ -63,6 +63,7 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
}, },
onSubmit: async (values) => { onSubmit: async (values) => {
try { try {
console.log({values})
const submitData = { const submitData = {
snippetCode: values.snippetCode, snippetCode: values.snippetCode,

View file

@ -1,12 +0,0 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
rootDir: '..',
testMatch: ['<rootDir>/e2e/**/*.test.js'],
testTimeout: 120000,
maxWorkers: 1,
globalSetup: 'detox/runners/jest/globalSetup',
globalTeardown: 'detox/runners/jest/globalTeardown',
reporters: ['detox/runners/jest/reporter'],
testEnvironment: 'detox/runners/jest/testEnvironment',
verbose: true,
};

View file

@ -1,23 +0,0 @@
describe('Example', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should have welcome screen', async () => {
await expect(element(by.id('welcome'))).toBeVisible();
});
it('should show hello screen after tap', async () => {
await element(by.id('hello_button')).tap();
await expect(element(by.text('Hello!!!'))).toBeVisible();
});
it('should show world screen after tap', async () => {
await element(by.id('world_button')).tap();
await expect(element(by.text('World!!!'))).toBeVisible();
});
});

View file

@ -5,8 +5,8 @@
}, },
"build": { "build": {
"development": { "development": {
"distribution": "internal", "developmentClient": true,
"channel": "development" "distribution": "internal"
}, },
"preview": { "preview": {
"distribution": "internal", "distribution": "internal",

View file

@ -1,118 +0,0 @@
import { useState } from 'react';
import { trpc } from '../src/trpc-client';
type ContextString = 'review' | 'product_info' | 'notification' | 'store' | 'complaint' | 'profile' | 'tags';
interface UploadInput {
blob: Blob;
mimeType: string;
}
interface UploadBatchInput {
images: UploadInput[];
contextString: ContextString;
}
interface UploadResult {
keys: string[];
presignedUrls: string[];
}
export function useUploadToObjectStorage() {
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [progress, setProgress] = useState<{ completed: number; total: number } | null>(null);
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
const upload = async (input: UploadBatchInput): Promise<UploadResult> => {
setIsUploading(true);
setError(null);
setProgress({ completed: 0, total: input.images.length });
try {
const { images, contextString } = input;
if (images.length === 0) {
return { keys: [], presignedUrls: [] };
}
// 1. Get presigned URLs from backend (one call for all images)
const mimeTypes = images.map(img => img.mimeType);
const { uploadUrls } = await generateUploadUrls.mutateAsync({
contextString,
mimeTypes,
});
if (uploadUrls.length !== images.length) {
throw new Error(`Expected ${images.length} URLs, got ${uploadUrls.length}`);
}
// 2. Upload all images in parallel
const uploadPromises = images.map(async (image, index) => {
const presignedUrl = uploadUrls[index];
const { blob, mimeType } = image;
const response = await fetch(presignedUrl, {
method: 'PUT',
body: blob,
headers: { 'Content-Type': mimeType },
});
if (!response.ok) {
throw new Error(`Upload ${index + 1} failed with status ${response.status}`);
}
// Update progress
setProgress(prev => prev ? { ...prev, completed: prev.completed + 1 } : null);
return {
key: extractKeyFromPresignedUrl(presignedUrl),
presignedUrl,
};
});
// Use Promise.all - if any fails, entire batch fails
const results = await Promise.all(uploadPromises);
return {
keys: results.map(r => r.key),
presignedUrls: results.map(r => r.presignedUrl),
};
} catch (err) {
const uploadError = err instanceof Error ? err : new Error('Upload failed');
setError(uploadError);
throw uploadError;
} finally {
setIsUploading(false);
setProgress(null);
}
};
const uploadSingle = async (blob: Blob, mimeType: string, contextString: ContextString): Promise<{ key: string; presignedUrl: string }> => {
const result = await upload({
images: [{ blob, mimeType }],
contextString,
});
return {
key: result.keys[0],
presignedUrl: result.presignedUrls[0],
};
};
return {
upload,
uploadSingle,
isUploading,
error,
progress,
isPending: generateUploadUrls.isPending
};
}
function extractKeyFromPresignedUrl(url: string): string {
const u = new URL(url);
let rawKey = u.pathname.replace(/^\/+/, '');
rawKey = rawKey.split('/').slice(1).join('/'); // make meatfarmer/product-images/asdf as product-images/asdf
return decodeURIComponent(rawKey);
}

Binary file not shown.

View file

@ -24,7 +24,6 @@
"@trpc/react-query": "^11.6.0", "@trpc/react-query": "^11.6.0",
"axios": "^1.11.0", "axios": "^1.11.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"date-fns": "^4.1.0",
"dayjs": "^1.11.18", "dayjs": "^1.11.18",
"expo": "~53.0.22", "expo": "~53.0.22",
"expo-blur": "~14.1.5", "expo-blur": "~14.1.5",

View file

@ -0,0 +1,111 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import axios from '../../services/axios-admin-ui';
// Types
export interface CreateProductPayload {
name: string;
shortDescription?: string;
longDescription?: string;
unitId: number;
storeId: number;
price: number;
marketPrice?: number;
incrementStep?: number;
productQuantity?: number;
isOutOfStock?: boolean;
deals?: {
quantity: number;
price: number;
validTill: string;
}[];
}
export interface UpdateProductPayload {
name: string;
shortDescription?: string;
longDescription?: string;
unitId: number;
storeId: number;
price: number;
marketPrice?: number;
incrementStep?: number;
productQuantity?: number;
isOutOfStock?: boolean;
deals?: {
quantity: number;
price: number;
validTill: string;
}[];
}
export interface Product {
id: number;
name: string;
shortDescription?: string | null;
longDescription?: string;
unitId: number;
storeId: number;
price: number;
marketPrice?: number;
productQuantity?: number;
isOutOfStock?: boolean;
images?: string[];
createdAt: string;
unit?: {
id: number;
shortNotation: string;
fullName: string;
};
deals?: {
id: number;
quantity: string;
price: string;
validTill: string;
}[];
}
export interface CreateProductResponse {
product: Product;
deals?: any[];
message: string;
}
// API functions
const createProductApi = async (formData: FormData): Promise<CreateProductResponse> => {
const response = await axios.post('/av/products', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
};
const updateProductApi = async ({ id, formData }: { id: number; formData: FormData }): Promise<CreateProductResponse> => {
const response = await axios.put(`/av/products/${id}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
};
// Hooks
export const useCreateProduct = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createProductApi,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
};
export const useUpdateProduct = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateProductApi,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
};

View file

@ -0,0 +1,119 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import axios from '../../services/axios-admin-ui';
// Types
export interface CreateTagPayload {
tagName: string;
tagDescription?: string;
imageUrl?: string;
isDashboardTag: boolean;
relatedStores?: number[];
}
export interface UpdateTagPayload {
tagName: string;
tagDescription?: string;
imageUrl?: string;
isDashboardTag: boolean;
relatedStores?: number[];
}
export interface Tag {
id: number;
tagName: string;
tagDescription: string | null;
imageUrl: string | null;
isDashboardTag: boolean;
relatedStores: number[];
createdAt?: string;
}
export interface CreateTagResponse {
tag: Tag;
message: string;
}
export interface GetTagsResponse {
tags: Tag[];
message: string;
}
// API functions
const createTagApi = async (formData: FormData): Promise<CreateTagResponse> => {
const response = await axios.post('/av/product-tags', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
};
const updateTagApi = async ({ id, formData }: { id: number; formData: FormData }): Promise<CreateTagResponse> => {
const response = await axios.put(`/av/product-tags/${id}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
};
const deleteTagApi = async (id: number): Promise<{ message: string }> => {
const response = await axios.delete(`/av/product-tags/${id}`);
return response.data;
};
const getTagsApi = async (): Promise<GetTagsResponse> => {
const response = await axios.get('/av/product-tags');
return response.data;
};
const getTagApi = async (id: number): Promise<{ tag: Tag }> => {
const response = await axios.get(`/av/product-tags/${id}`);
return response.data;
};
// Hooks
export const useCreateTag = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createTagApi,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tags'] });
},
});
};
export const useUpdateTag = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateTagApi,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tags'] });
},
});
};
export const useDeleteTag = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteTagApi,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tags'] });
},
});
};
export const useGetTags = () => {
return useQuery({
queryKey: ['tags'],
queryFn: getTagsApi,
});
};
export const useGetTag = (id: number) => {
return useQuery({
queryKey: ['tags', id],
queryFn: () => getTagApi(id),
enabled: !!id,
});
};

View file

@ -1,10 +1,13 @@
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle, useMemo } from 'react'; import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react';
import { View, TouchableOpacity } from 'react-native'; import { View, TouchableOpacity } from 'react-native';
import { Image } from 'expo-image';
import { Formik, FieldArray } from 'formik'; import { Formik, FieldArray } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { MyTextInput, BottomDropdown, MyText, useTheme, DatePicker, tw, useFocusCallback, Checkbox, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'common-ui'; import { MyTextInput, BottomDropdown, MyText, ImageUploader, ImageGalleryWithDelete, useTheme, DatePicker, tw, useFocusCallback, Checkbox } from 'common-ui';
import usePickImage from 'common-ui/src/components/use-pick-image';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { trpc } from '../trpc-client'; import { trpc } from '../trpc-client';
import { useGetTags } from '../api-hooks/tag.api';
interface ProductFormData { interface ProductFormData {
name: string; name: string;
@ -35,10 +38,9 @@ export interface ProductFormRef {
interface ProductFormProps { interface ProductFormProps {
mode: 'create' | 'edit'; mode: 'create' | 'edit';
initialValues: ProductFormData; initialValues: ProductFormData;
onSubmit: (values: ProductFormData, images: ImageUploaderNeoPayload[], imagesToDelete: string[]) => void; onSubmit: (values: ProductFormData, images?: { uri?: string }[], imagesToDelete?: string[]) => void;
isLoading: boolean; isLoading: boolean;
existingImages?: ImageUploaderNeoItem[]; existingImages?: string[];
existingImageKeys?: string[];
} }
const unitOptions = [ const unitOptions = [
@ -48,22 +50,18 @@ const unitOptions = [
{ label: 'Unit Piece', value: 4 }, { label: 'Unit Piece', value: 4 },
]; ];
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
mode, mode,
initialValues, initialValues,
onSubmit, onSubmit,
isLoading, isLoading,
existingImages:existingImagesRaw, existingImages = []
existingImageKeys = [],
}, ref) => { }, ref) => {
const { theme } = useTheme(); const { theme } = useTheme();
const [images, setImages] = useState<ImageUploaderNeoItem[]>([]); const [images, setImages] = useState<{ uri?: string }[]>([]);
const [existingImagesState, setExistingImagesState] = useState<string[]>(existingImages);
const existingImages = existingImagesRaw || []
// Sync images state when existingImages prop changes (e.g., when async query data arrives)
useEffect(() => {
setImages(existingImages);
}, [existingImagesRaw]);
const { data: storesData } = trpc.common.getStoresSummary.useQuery(); const { data: storesData } = trpc.common.getStoresSummary.useQuery();
const storeOptions = storesData?.stores.map(store => ({ const storeOptions = storesData?.stores.map(store => ({
@ -71,50 +69,44 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
value: store.id, value: store.id,
})) || []; })) || [];
const { data: tagsData } = trpc.admin.product.getProductTags.useQuery(); const { data: tagsData } = useGetTags();
const tagOptions = tagsData?.tags.map(tag => ({ const tagOptions = tagsData?.tags.map(tag => ({
label: tag.tagName, label: tag.tagName,
value: tag.id.toString(), value: tag.id.toString(),
})) || []; })) || [];
// Build signed URL -> S3 key mapping for existing images // Initialize existing images state when existingImages prop changes
const signedUrlToKey = useMemo(() => { useEffect(() => {
const map: Record<string, string> = {}; console.log('changing existing imaes statte')
existingImages.forEach((img, i) => {
if (existingImageKeys[i]) { setExistingImagesState(existingImages);
map[img.imgUrl] = existingImageKeys[i]; }, [existingImages]);
}
}); const pickImage = usePickImage({
return map; setFile: (files) => setImages(prev => [...prev, ...files]),
}, [existingImages, existingImageKeys]); multiple: true,
});
// Calculate which existing images were deleted
const deletedImages = existingImages.filter(img => !existingImagesState.includes(img));
return ( return (
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
onSubmit={(values) => { onSubmit={(values) => onSubmit(values, images, deletedImages)}
// New images have mimeType set, existing images have mimeType === null
const newImages = images.filter(img => img.mimeType !== null);
const deletedImageKeys = existingImages
.filter(existing => !images.some(current => current.imgUrl === existing.imgUrl))
.map(deleted => signedUrlToKey[deleted.imgUrl])
.filter(Boolean);
onSubmit(
values,
newImages.map(img => ({ url: img.imgUrl, mimeType: img.mimeType })),
deletedImageKeys,
);
}}
enableReinitialize enableReinitialize
> >
{({ handleChange, handleSubmit, values, setFieldValue, resetForm }) => { {({ handleChange, handleSubmit, values, setFieldValue, resetForm }) => {
// Clear form when screen comes into focus
const clearForm = useCallback(() => { const clearForm = useCallback(() => {
setImages([]); setImages([]);
setExistingImagesState([]);
resetForm(); resetForm();
}, [resetForm]); }, [resetForm]);
useFocusCallback(clearForm); useFocusCallback(clearForm);
// Update ref with current clearForm function
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
clearImages: clearForm, clearImages: clearForm,
}), [clearForm]); }), [clearForm]);
@ -149,18 +141,44 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
/> />
<ImageUploaderNeo {mode === 'create' && (
images={images} <ImageUploader
onImageAdd={(payloads) => setImages(prev => [...prev, ...payloads.map(p => ({ imgUrl: p.url, mimeType: p.mimeType }))])} images={images}
onImageRemove={(payload) => setImages(prev => prev.filter(img => img.imgUrl !== payload.url))} onAddImage={pickImage}
allowMultiple={true} onRemoveImage={(uri) => setImages(prev => prev.filter(img => img.uri !== uri))}
/> />
)}
{mode === 'edit' && existingImagesState.length > 0 && (
<View style={{ marginBottom: 16 }}>
<MyText style={tw`text-lg font-bold mb-2 text-gray-800`}>Current Images</MyText>
<ImageGalleryWithDelete
imageUrls={existingImagesState}
setImageUrls={setExistingImagesState}
imageHeight={100}
imageWidth={100}
columns={3}
/>
</View>
)}
{mode === 'edit' && (
<View style={{ marginBottom: 16 }}>
<MyText style={tw`text-lg font-bold mb-2 text-gray-800`}>Add New Images</MyText>
<ImageUploader
images={images}
onAddImage={pickImage}
onRemoveImage={(uri) => setImages(prev => prev.filter(img => img.uri !== uri))}
/>
</View>
)}
<BottomDropdown <BottomDropdown
topLabel='Unit' topLabel='Unit'
label="Unit" label="Unit"
value={values.unitId} value={values.unitId}
options={unitOptions} options={unitOptions}
// onValueChange={(value) => handleChange('unitId')(value+'')}
onValueChange={(value) => setFieldValue('unitId', value)} onValueChange={(value) => setFieldValue('unitId', value)}
placeholder="Select unit" placeholder="Select unit"
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
@ -170,7 +188,18 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
placeholder="Enter product quantity" placeholder="Enter product quantity"
keyboardType="numeric" keyboardType="numeric"
value={values.productQuantity.toString()} value={values.productQuantity.toString()}
onChangeText={(text) => setFieldValue('productQuantity', text)} onChangeText={(text) => {
// if(text)
// setFieldValue('productQuantity', text);
// else
setFieldValue('productQuantity', text);
// if (text === '' || text === null || text === undefined) {
// setFieldValue('productQuantity', 1);
// } else {
// const num = parseFloat(text);
// setFieldValue('productQuantity', isNaN(num) ? 1 : num);
// }
}}
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
/> />
<BottomDropdown <BottomDropdown
@ -209,6 +238,8 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
/> />
<View style={tw`flex-row items-center mb-4`}> <View style={tw`flex-row items-center mb-4`}>
<Checkbox <Checkbox
checked={values.isSuspended} checked={values.isSuspended}
@ -223,7 +254,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
checked={values.isFlashAvailable} checked={values.isFlashAvailable}
onPress={() => { onPress={() => {
setFieldValue('isFlashAvailable', !values.isFlashAvailable); setFieldValue('isFlashAvailable', !values.isFlashAvailable);
if (values.isFlashAvailable) setFieldValue('flashPrice', ''); if (values.isFlashAvailable) setFieldValue('flashPrice', ''); // Clear price when disabled
}} }}
style={tw`mr-3`} style={tw`mr-3`}
/> />
@ -241,6 +272,87 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
/> />
)} )}
{/* <FieldArray name="deals">
{({ push, remove, form }) => (
<View style={{ marginBottom: 16 }}>
<View style={tw`flex-row items-center mb-4`}>
<MaterialIcons name="local-offer" size={20} color="#3B82F6" />
<MyText style={tw`text-lg font-bold text-gray-800 ml-2`}>
Special Package Deals
</MyText>
<MyText style={tw`text-sm text-gray-500 ml-1`}>(Optional)</MyText>
</View>
{(form.values.deals || []).map((deal: any, index: number) => (
<View key={index} style={tw`bg-white p-4 rounded-2xl shadow-lg mb-4 border border-gray-100`}>
<View style={tw`mb-3`}>
<View style={tw`flex-row items-end gap-3 mb-3`}>
<View style={tw`flex-1`}>
<MyTextInput
topLabel="Quantity"
placeholder="Enter quantity"
keyboardType="numeric"
value={deal.quantity || ''}
onChangeText={form.handleChange(`deals.${index}.quantity`)}
fullWidth={false}
/>
</View>
<View style={tw`flex-1`}>
<MyTextInput
topLabel="Price"
placeholder="Enter price"
keyboardType="numeric"
value={deal.price || ''}
onChangeText={form.handleChange(`deals.${index}.price`)}
fullWidth={false}
/>
</View>
</View>
<View style={tw`flex-row items-end gap-3`}>
<View style={tw`flex-1`}>
<DatePicker
value={deal.validTill}
setValue={(date) => form.setFieldValue(`deals.${index}.validTill`, date)}
showLabel={true}
placeholder="Valid Till"
/>
</View>
<View style={tw`flex-1`}>
<TouchableOpacity
onPress={() => remove(index)}
style={tw`bg-red-500 p-3 rounded-lg shadow-md flex-row items-center justify-center`}
>
<MaterialIcons name="delete" size={16} color="white" />
<MyText style={tw`text-white font-semibold ml-1`}>Remove</MyText>
</TouchableOpacity>
</View>
</View>
</View>
</View>
))}
{(form.values.deals || []).length === 0 && (
<View style={tw`bg-gray-50 p-6 rounded-2xl border-2 border-dashed border-gray-300 items-center mb-4`}>
<MaterialIcons name="local-offer" size={32} color="#9CA3AF" />
<MyText style={tw`text-gray-500 text-center mt-2`}>
No package deals added yet
</MyText>
<MyText style={tw`text-gray-400 text-sm text-center mt-1`}>
Add special pricing for bulk purchases
</MyText>
</View>
)}
<TouchableOpacity
onPress={() => push({ quantity: '', price: '', validTill: null })}
style={tw`bg-green-500 px-4 py-2 rounded-lg shadow-lg flex-row items-center justify-center mt-4`}
>
<MaterialIcons name="add" size={20} color="white" />
<MyText style={tw`text-white font-bold text-lg ml-2`}>Add Package Deal</MyText>
</TouchableOpacity>
</View>
)}
</FieldArray> */}
<TouchableOpacity <TouchableOpacity
onPress={submit} onPress={submit}
disabled={isLoading} disabled={isLoading}
@ -259,4 +371,4 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
ProductForm.displayName = 'ProductForm'; ProductForm.displayName = 'ProductForm';
export default ProductForm; export default ProductForm;

View file

@ -1,8 +1,10 @@
import React, { useState, useEffect, forwardRef, useCallback } from 'react'; import React, { useState, useEffect, forwardRef, useCallback } from 'react';
import { View, TouchableOpacity } from 'react-native'; import { View, TouchableOpacity } from 'react-native';
import { Image } from 'expo-image';
import { Formik } from 'formik'; import { Formik } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { MyTextInput, MyText, Checkbox, ImageUploaderNeo, tw, useFocusCallback, BottomDropdown, type ImageUploaderNeoItem, type ImageUploaderNeoPayload } from 'common-ui'; import { MyTextInput, MyText, Checkbox, ImageUploader, tw, useFocusCallback, BottomDropdown } from 'common-ui';
import usePickImage from 'common-ui/src/components/use-pick-image';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
interface StoreOption { interface StoreOption {
@ -21,7 +23,7 @@ interface TagFormProps {
mode: 'create' | 'edit'; mode: 'create' | 'edit';
initialValues: TagFormData; initialValues: TagFormData;
existingImageUrl?: string; existingImageUrl?: string;
onSubmit: (values: TagFormData, images: ImageUploaderNeoItem[], removedExisting: boolean) => void; onSubmit: (values: TagFormData, image?: { uri?: string }) => void;
isLoading: boolean; isLoading: boolean;
stores?: StoreOption[]; stores?: StoreOption[];
} }
@ -29,28 +31,27 @@ interface TagFormProps {
const TagForm = forwardRef<any, TagFormProps>(({ const TagForm = forwardRef<any, TagFormProps>(({
mode, mode,
initialValues, initialValues,
existingImageUrl: existingImageUrlRaw, existingImageUrl = '',
onSubmit, onSubmit,
isLoading, isLoading,
stores: storesRaw, stores = [],
}, ref) => { }, ref) => {
const [images, setImages] = useState<ImageUploaderNeoItem[]>([]) const [image, setImage] = useState<{ uri?: string } | null>(null);
const [removedExisting, setRemovedExisting] = useState(false)
const [isDashboardTagChecked, setIsDashboardTagChecked] = useState<boolean>(Boolean(initialValues.isDashboardTag)); const [isDashboardTagChecked, setIsDashboardTagChecked] = useState<boolean>(Boolean(initialValues.isDashboardTag));
const existingImageUrl = existingImageUrlRaw || ''
const stores = storesRaw || []
// Update checkbox when initial values change // Update checkbox when initial values change
useEffect(() => { useEffect(() => {
setIsDashboardTagChecked(Boolean(initialValues.isDashboardTag)); setIsDashboardTagChecked(Boolean(initialValues.isDashboardTag));
if (existingImageUrl) { existingImageUrl && setImage({uri:existingImageUrl})
setImages([{ imgUrl: existingImageUrl, mimeType: null }]) }, [initialValues.isDashboardTag]);
} else {
setImages([]) const pickImage = usePickImage({
} setFile: (files) => {
setRemovedExisting(false)
}, [existingImageUrlRaw, initialValues.isDashboardTag]); setImage(files || null)
},
multiple: false,
});
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
@ -66,17 +67,17 @@ const TagForm = forwardRef<any, TagFormProps>(({
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={(values) => onSubmit(values, images, removedExisting)} onSubmit={(values) => onSubmit(values, image || undefined)}
enableReinitialize enableReinitialize
> >
{({ handleChange, handleSubmit, values, setFieldValue, errors, touched, setFieldValue: formikSetFieldValue, resetForm }) => { {({ handleChange, handleSubmit, values, setFieldValue, errors, touched, setFieldValue: formikSetFieldValue, resetForm }) => {
// Clear form when screen comes into focus // Clear form when screen comes into focus
const clearForm = useCallback(() => { const clearForm = useCallback(() => {
setImages([]) setImage(null);
setRemovedExisting(false)
setIsDashboardTagChecked(false); setIsDashboardTagChecked(false);
resetForm(); resetForm();
}, [resetForm]); }, [resetForm]);
useFocusCallback(clearForm); useFocusCallback(clearForm);
@ -107,21 +108,10 @@ const TagForm = forwardRef<any, TagFormProps>(({
</MyText> </MyText>
<ImageUploaderNeo <ImageUploader
images={images} images={image ? [image] : []}
onImageAdd={(payload: ImageUploaderNeoPayload[]) => { onAddImage={pickImage}
setImages((prev) => [...prev, ...payload.map((img) => ({ onRemoveImage={() => setImage(null)}
imgUrl: img.url,
mimeType: img.mimeType,
}))])
}}
onImageRemove={(payload) => {
if (payload.mimeType === null) {
setRemovedExisting(true)
}
setImages((prev) => prev.filter((item) => item.imgUrl !== payload.url))
}}
allowMultiple={false}
/> />
</View> </View>
@ -177,4 +167,4 @@ const TagForm = forwardRef<any, TagFormProps>(({
TagForm.displayName = 'TagForm'; TagForm.displayName = 'TagForm';
export default TagForm; export default TagForm;

View file

@ -3,7 +3,7 @@ import { View, TouchableOpacity, Alert } from 'react-native';
import { Entypo } from '@expo/vector-icons'; import { Entypo } from '@expo/vector-icons';
import { MyText, tw, BottomDialog } from 'common-ui'; import { MyText, tw, BottomDialog } from 'common-ui';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { trpc } from '@/src/trpc-client'; import { useDeleteTag } from '../api-hooks/tag.api';
export interface TagMenuProps { export interface TagMenuProps {
tagId: number; tagId: number;
@ -22,7 +22,7 @@ export const TagMenu: React.FC<TagMenuProps> = ({
}) => { }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const deleteTag = trpc.admin.product.deleteProductTag.useMutation(); const { mutate: deleteTag, isPending: isDeleting } = useDeleteTag();
const handleOpenMenu = () => { const handleOpenMenu = () => {
setIsOpen(true); setIsOpen(true);
@ -54,7 +54,7 @@ export const TagMenu: React.FC<TagMenuProps> = ({
}; };
const performDelete = () => { const performDelete = () => {
deleteTag.mutate({ id: tagId }, { deleteTag(tagId, {
onSuccess: () => { onSuccess: () => {
Alert.alert('Success', 'Tag deleted successfully'); Alert.alert('Success', 'Tag deleted successfully');
onDeleteSuccess?.(); onDeleteSuccess?.();
@ -63,7 +63,7 @@ export const TagMenu: React.FC<TagMenuProps> = ({
const errorMessage = error.message || 'Failed to delete tag'; const errorMessage = error.message || 'Failed to delete tag';
Alert.alert('Error', errorMessage); Alert.alert('Error', errorMessage);
}, },
}) });
}; };
const options = [ const options = [
@ -116,4 +116,4 @@ export const TagMenu: React.FC<TagMenuProps> = ({
</BottomDialog> </BottomDialog>
</> </>
); );
}; };

View file

@ -1,6 +1,6 @@
import { createTRPCProxyClient, httpBatchLink, TRPCClientError } from '@trpc/client'; import { createTRPCProxyClient, httpBatchLink, TRPCClientError } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query'; import { createTRPCReact } from '@trpc/react-query';
import { AppRouter } from '@backend/trpc/router' import {AppRouter} from '../../backend/src/trpc/router'
import { BASE_API_URL } from 'common-ui'; import { BASE_API_URL } from 'common-ui';
import { getJWT } from '@/hooks/useJWT'; import { getJWT } from '@/hooks/useJWT';
import { FORCE_LOGOUT_EVENT } from 'common-ui/src/lib/const-strs'; import { FORCE_LOGOUT_EVENT } from 'common-ui/src/lib/const-strs';

View file

@ -4,11 +4,7 @@
"strict": true, "strict": true,
"paths": { "paths": {
"@/*": [ "@/*": [
"./*", "./*"
"../backend/*"
],
"@backend/*": [
"../backend/src/*"
], ],
"shared-types": ["../shared-types"], "shared-types": ["../shared-types"],
"common-ui": ["../../packages/ui"], "common-ui": ["../../packages/ui"],

8
packages/db_helper_postgres/.env → apps/backend/.env Normal file → Executable file
View file

@ -1,5 +1,6 @@
ENV_MODE=PROD ENV_MODE=PROD
DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy 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=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner
PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/ PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090 PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
@ -20,11 +21,6 @@ S3_URL=https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com
S3_BUCKET_NAME=meatfarmer S3_BUCKET_NAME=meatfarmer
EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK- EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK-
JWT_SECRET=my_meatfarmer_jwt_secret_key JWT_SECRET=my_meatfarmer_jwt_secret_key
ASSETS_DOMAIN=https://assets.freshyo.in/
API_CACHE_KEY=api-cache-dev
# CLOUDFLARE_API_TOKEN=I8Vp4E9TX58E8qEDeH0nTFDS2d2zXNYiXvbs4Ckj
CLOUDFLARE_API_TOKEN=N7jAg5X-RUj_fVfMW6zbfJ8qIYc81TSIKKlbZ6oh
CLOUDFLARE_ZONE_ID=edefbf750bfc3ff26ccd11e8e28dc8d7
# REDIS_URL=redis://default:redis_shafi_password@5.223.55.14:6379 # REDIS_URL=redis://default:redis_shafi_password@5.223.55.14:6379
REDIS_URL=redis://default:redis_shafi_password@57.128.212.174:6379 REDIS_URL=redis://default:redis_shafi_password@57.128.212.174:6379
APP_URL=http://localhost:4000 APP_URL=http://localhost:4000

View file

@ -1,42 +0,0 @@
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
PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
PHONE_PE_CLIENT_VERSION=1
PHONE_PE_CLIENT_SECRET=MTU1MmIzOTgtM2Q0Mi00N2M5LTkyMWUtNzBiMjdmYzVmZWUy
PHONE_PE_MERCHANT_ID=M23F2IGP34ZAR
# S3_REGION=ap-hyderabad-1
# S3_REGION=sgp
# S3_ACCESS_KEY_ID=52932a33abce40b38b559dadccab640f
# S3_SECRET_ACCESS_KEY=d287998b696d4a1c912e727f6394e53b
# S3_URL=https://s3.sgp.io.cloud.ovh.net/
# S3_BUCKET_NAME=theobjectstore
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
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
# 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
RAZORPAY_KEY=rzp_test_RdCBBUJ56NLaJK
RAZORPAY_SECRET=namEwKBE1ypWxH0QDVg6fWOe
OTP_SENDER_AUTH_TOKEN=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJDLTM5OENEMkJDRTM0MjQ4OCIsImlhdCI6MTc0Nzg0MTEwMywiZXhwIjoxOTA1NTIxMTAzfQ.IV64ofVKjcwveIanxu_P2XlACtPeA9sJQ74uM53osDeyUXsFv0rwkCl6NNBIX93s_wnh4MKITLbcF_ClwmFQ0A
MIN_ORDER_VALUE=300
DELIVERY_CHARGE=20
# Telegram Configuration
TELEGRAM_BOT_TOKEN=8410461852:AAGXQCwRPFbndqwTgLJh8kYxST4Z0vgh72U
TELEGRAM_CHAT_IDS=5147760058
# TELEGRAM_BOT_TOKEN=8410461852:AAGXQCwRPFbndqwTgLJh8kYxST4Z0vgh72U
# TELEGRAM_CHAT_IDS=-5075171894

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
This is a demo file.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

View file

@ -1,13 +0,0 @@
{
"type": "service_account",
"project_id": "freshyo-cefb2",
"private_key_id": "dcdb3d9edb6505567db69bbd24e447df78c82dc7",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDE3TSKEL9CF7yP\nUiSIvQC024yQGrERz1wtErH5Xff4pie1LSL1pXJHTpyWojp6dotkmCpxM36XjS+O\nO5pnJhSVwiYgvSxeO3EL6oNQwLP36pxQ7YwmoaFx9Jipau+OK+VY8Y/eMx4cWUJH\n7WeUDGwwJlMKE6CpEsbbBiAY5bF9wwe7v1YlkAnMm5ZZcujCqW1aShWKXuYoUMoP\n6egEiclCdQrHZ5IQCHRWruFTAOBuJ7v0A/9WM1gi7UM3VU3/8ccswP8DDoCrgrmh\nerUhFAEFMEjsns0B8SmwQ8v3GH5/SG0SCDwJniPFPnzdSxksaEB51OTaBcROJlED\nkwZZ+2u/AgMBAAECggEAPYL2vysjb6XWC5w5gSY5Ocmd/orwh+WYYhcE2CuV5zIX\nlyM+2K106zXzdJfFGO3AeVKYdF2IMRdy5AjYomFCLlcHLdSeL+V32abRmCJWOWEr\nrZfD4nA/b0ljiBA7QNuTYnq8HswvHOGA9dOGuTo2dccLzEq8uQd+bgJYdh8TGf2R\nqOpYHdRUJqDl+EuCDqLLqnq4l8E981GN78iVVL4DnYFE/3wb7tmuONww2+grq7ou\ntDtPQf4yNE2Vfx+5JnMsvU+J+iF/4vCI/9Oyg0keW/C8q9rbDdmeyefGRGWHtCCI\nH1wMzYTn2xw7EBH9O1NHDzTWkSTUfeo2dnaR0loVsQKBgQDnuwk180u1QXoDLUzj\ngd86CRnP/zqijdt+Y2oZnT+uHJrHCJbYNt3bdKRUEBU5KEcMdzMaeR8A1YEYK2oD\nd8M42nsOn22VymT0fIqwrHsf9e5mgGV+novqw848aEmgTIEmBnSYKc3Xa3px3wge\nJWLKlX/+y2uvI9En8u1FGQ0wcQKBgQDZe1uLqd6koPh/+VSAx2OtjCDgCAvlYoUh\nwIH3tFab/p41DyR+VDx6z18MACsSmyiewV4xUBmu1o+H/iiOxPXQvf7QchY+fYYb\nzOMJXM4ddcGUdLF8CPapbIFcLKemQIb0PIlrCQQeXq2E74JacP8kdqNmCQ8J/GZF\nMPapRTt3LwKBgQCU5jLJ7tZD1pnO9snEGkxUn0ptw0Nq9hoGwVyIrukfOJQfth4v\nOjoebHm25kqs2nukv+cfaJqKT6ZO4H6TUd4oZwLRZ5HjwRRToL8BPSM0azNPu8r7\nrGadaEnZuO0uSlpmE5nRuHLiq9YW20f9DurG339KOm2sMSiRMeBSGQHHkQKBgGTZ\nFQxgiwOgOVtujMbirtAtKJl6YbnOw5lxIVNx5q+TlF1aVjvWZ+0y+Aoikdag6Gcl\nl74aPK6chBY1vyzlHG/diqmyHaqAno2JpsYSqOl0T3291weDSI4r6JiLhHpNdccP\nw1FE7wn+MUxxm+rAdy+7a+3GyZiB2BLBr7+ygO61AoGBALySZ9m4hgX6uZtJvv3R\nrl8AWoG65NHCZ4694aEGTJDVDlPByV+Sd5iBOQ5dvhgA12Py2uj5ZHQXbuo0IGfJ\ngH8AZMIKX9UrhbE5BWYncg2ZR8uvKow8w36mLNnQhGZ71IZ9MXbWbpEK8CbCEvzZ\nyw0rKVgrrSRihW3stnl16Zs5\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-fbsvc@freshyo-cefb2.iam.gserviceaccount.com",
"client_id": "117456013812283364643",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40freshyo-cefb2.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View file

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