Compare commits
No commits in common. "main" and "before-notif" have entirely different histories.
main
...
before-not
224 changed files with 2016 additions and 31365 deletions
|
|
@ -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.
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"devices": []
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"dependencies": "c63a16a85154f1ea03750b1df53dcdee0200585f",
|
|
||||||
"devDependencies": "0a1ec1c6df1c9d5100926df058dd0824b1293819"
|
|
||||||
}
|
|
||||||
|
|
@ -50,4 +50,4 @@ react-native. They are available in the common-ui as MyText, MyTextInput, MyTouc
|
||||||
## Important Notes
|
## Important Notes
|
||||||
- **Do not run build, compile, or migration commands** - These should be handled manually by developers
|
- **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
|
- 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
|
- Don't do anything with git. Don't do git add or git commit. That will be managed entirely by the user
|
||||||
8
app.json
8
app.json
|
|
@ -1,7 +1,3 @@
|
||||||
{
|
{
|
||||||
"expo": {
|
"expo": {}
|
||||||
"ios": {
|
}
|
||||||
"bundleIdentifier": "com.mohammedshafiuddin54.meat-farmer-monorepo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"dependencies": "4650ceb7c30aaa4d5fd17b9577e186af7a84b50d",
|
"dependencies": "091948e86692e0cce7744b6b0543448538c3125a",
|
||||||
"devDependencies": "b3b38265f32b99a8299270a292f38ca26288d53d"
|
"devDependencies": "b3b38265f32b99a8299270a292f38ca26288d53d"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6
apps/admin-ui/.expo/types/router.d.ts
vendored
6
apps/admin-ui/.expo/types/router.d.ts
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB |
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -227,11 +213,10 @@ export default function Layout() {
|
||||||
<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="address-management" options={{ title: "Address Management" }} />
|
<Drawer.Screen name="address-management" options={{ title: "Address Management" }} />
|
||||||
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
|
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
|
||||||
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
|
<Drawer.Screen name="order-details/[id]" options={{ title: "Order Details" }} />
|
||||||
<Drawer.Screen name="user-management" options={{ title: "User Management" }} />
|
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
|
||||||
<Drawer.Screen name="send-notifications" options={{ title: "Send Notifications" }} />
|
</Drawer>
|
||||||
</Drawer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -31,7 +31,6 @@ 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',
|
||||||
|
|
@ -49,7 +48,6 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constant, setFieldValue,
|
||||||
|
|
||||||
// Special handling for popularItems - show navigation button instead of input
|
// Special handling for popularItems - show navigation button instead of input
|
||||||
if (constant.key === 'popularItems') {
|
if (constant.key === 'popularItems') {
|
||||||
console.log('key is allItemsOrder')
|
|
||||||
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`}>
|
||||||
|
|
@ -69,28 +67,6 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constant, setFieldValue,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for allItemsOrder - show navigation button instead of input
|
|
||||||
if (constant.key === 'allItemsOrder') {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
|
||||||
{CONST_LABELS[constant.key] || constant.key}
|
|
||||||
</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(constant.value) ? constant.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 constant.value === 'boolean') {
|
if (typeof constant.value === 'boolean') {
|
||||||
return (
|
return (
|
||||||
|
|
@ -158,7 +134,6 @@ 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 = values.constants.filter((constant, index) => {
|
const changedConstants = values.constants.filter((constant, index) => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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',
|
||||||
|
|
@ -185,34 +183,16 @@ export default function Dashboard() {
|
||||||
iconColor: '#EAB308',
|
iconColor: '#EAB308',
|
||||||
iconBg: '#FEF9C3',
|
iconBg: '#FEF9C3',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'App Constants',
|
title: 'App Constants',
|
||||||
icon: 'settings-applications',
|
icon: 'settings-applications',
|
||||||
description: 'Customize app settings',
|
description: 'Customize app settings',
|
||||||
route: '/(drawer)/customize-app',
|
route: '/(drawer)/customize-app',
|
||||||
category: 'settings',
|
category: 'settings',
|
||||||
iconColor: '#7C3AED',
|
iconColor: '#7C3AED',
|
||||||
iconBg: '#F3E8FF',
|
iconBg: '#F3E8FF',
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
title: 'User Management',
|
|
||||||
icon: 'people',
|
|
||||||
description: 'View and manage all users',
|
|
||||||
route: '/(drawer)/user-management',
|
|
||||||
category: 'users',
|
|
||||||
iconColor: '#0EA5E9',
|
|
||||||
iconBg: '#E0F2FE',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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');
|
||||||
|
|
||||||
|
|
@ -220,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' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -247,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`,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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={() => {}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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 }>();
|
||||||
|
|
@ -83,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`}>
|
||||||
|
|
@ -278,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
|
||||||
|
|
@ -376,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
|
||||||
|
|
@ -396,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>
|
||||||
|
|
@ -437,7 +404,7 @@ export default function OrderDetails() {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
|
|
||||||
{/* Order Items */}
|
{/* Order Items */}
|
||||||
<View
|
<View
|
||||||
|
|
@ -519,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`}>
|
||||||
|
|
@ -611,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
|
||||||
|
|
@ -44,6 +44,7 @@ export default function EditProduct() {
|
||||||
tagIds: values.tagIds,
|
tagIds: values.tagIds,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log({payload})
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
Object.entries(payload).forEach(([key, value]) => {
|
Object.entries(payload).forEach(([key, value]) => {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -1,274 +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';
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: number;
|
|
||||||
name: string | null;
|
|
||||||
mobile: string | null;
|
|
||||||
isEligibleForNotif: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractKeyFromUrl = (url: string): string => {
|
|
||||||
const u = new URL(url);
|
|
||||||
const rawKey = u.pathname.replace(/^\/+/, '');
|
|
||||||
return decodeURIComponent(rawKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate upload URLs mutation
|
|
||||||
const generateUploadUrls = trpc.user.fileUpload.generateUploadUrls.useMutation();
|
|
||||||
|
|
||||||
// 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 { uploadUrls } = await generateUploadUrls.mutateAsync({
|
|
||||||
contextString: 'notification',
|
|
||||||
mimeTypes: [selectedImage.mimeType],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (uploadUrls.length > 0) {
|
|
||||||
const uploadUrl = uploadUrls[0];
|
|
||||||
imageUrl = extractKeyFromUrl(uploadUrl);
|
|
||||||
|
|
||||||
// Upload image
|
|
||||||
const uploadResponse = await fetch(uploadUrl, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: selectedImage.blob,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': selectedImage.mimeType,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!uploadResponse.ok) {
|
|
||||||
throw new Error(`Upload failed with status ${uploadResponse.status}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
158
apps/admin-ui/app/(drawer)/user-details/[id]/index.tsx
Normal file
158
apps/admin-ui/app/(drawer)/user-details/[id]/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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={() => {
|
||||||
|
|
|
||||||
|
|
@ -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())}
|
||||||
|
|
|
||||||
|
|
@ -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`}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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"],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
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
|
||||||
PHONE_PE_CLIENT_VERSION=1
|
PHONE_PE_CLIENT_VERSION=1
|
||||||
|
|
@ -20,7 +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/
|
|
||||||
# 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
CREATE TABLE "mf"."user_notifications" (
|
|
||||||
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "mf"."user_notifications_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
|
||||||
"image_url" varchar(500),
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"body" text NOT NULL,
|
|
||||||
"applicable_users" jsonb
|
|
||||||
);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE "mf"."user_notifications" ADD COLUMN "title" varchar(255) NOT NULL;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE "mf"."delivery_slot_info" ADD COLUMN "is_capacity_full" boolean DEFAULT false NOT NULL;
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
CREATE TABLE "mf"."unlogged_user_tokens" (
|
|
||||||
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "mf"."unlogged_user_tokens_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
|
||||||
"token" varchar(500) NOT NULL,
|
|
||||||
"added_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"last_verified" timestamp,
|
|
||||||
CONSTRAINT "unlogged_user_tokens_token_unique" UNIQUE("token")
|
|
||||||
);
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
CREATE TABLE "mf"."user_incidents" (
|
|
||||||
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "mf"."user_incidents_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
|
||||||
"user_id" integer NOT NULL,
|
|
||||||
"order_id" integer,
|
|
||||||
"date_added" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"admin_comment" text,
|
|
||||||
"added_by" integer,
|
|
||||||
"negativity_score" integer
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "mf"."user_incidents" ADD CONSTRAINT "user_incidents_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "mf"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "mf"."user_incidents" ADD CONSTRAINT "user_incidents_order_id_orders_id_fk" FOREIGN KEY ("order_id") REFERENCES "mf"."orders"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "mf"."user_incidents" ADD CONSTRAINT "user_incidents_added_by_staff_users_id_fk" FOREIGN KEY ("added_by") REFERENCES "mf"."staff_users"("id") ON DELETE no action ON UPDATE no action;
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -505,41 +505,6 @@
|
||||||
"when": 1770321591876,
|
"when": 1770321591876,
|
||||||
"tag": "0071_moaning_shadow_king",
|
"tag": "0071_moaning_shadow_king",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 72,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1770546741428,
|
|
||||||
"tag": "0072_flowery_deathbird",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 73,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1770561175889,
|
|
||||||
"tag": "0073_faithful_gravity",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 74,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1771674555093,
|
|
||||||
"tag": "0074_outgoing_black_cat",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 75,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1772196660983,
|
|
||||||
"tag": "0075_cuddly_rocket_racer",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 76,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1772637259874,
|
|
||||||
"tag": "0076_sturdy_wolverine",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -5,21 +5,21 @@ import cors from "cors";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { db } from '@/src/db/db_index';
|
import { db } from './src/db/db_index';
|
||||||
import { staffUsers, userDetails } from '@/src/db/schema';
|
import { staffUsers, userDetails } from './src/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import mainRouter from '@/src/main-router';
|
import mainRouter from './src/main-router';
|
||||||
import initFunc from '@/src/lib/init';
|
import initFunc from './src/lib/init';
|
||||||
import { createExpressMiddleware } from '@trpc/server/adapters/express';
|
import { createExpressMiddleware } from '@trpc/server/adapters/express';
|
||||||
import { appRouter } from '@/src/trpc/router';
|
import { appRouter } from './src/trpc/router';
|
||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import signedUrlCache from '@/src/lib/signed-url-cache';
|
import signedUrlCache from 'src/lib/signed-url-cache';
|
||||||
import { seed } from '@/src/db/seed';
|
import { seed } from 'src/db/seed';
|
||||||
import '@/src/jobs/jobs-index';
|
import './src/jobs/jobs-index';
|
||||||
import { startAutomatedJobs } from '@/src/lib/automatedJobs';
|
import { startAutomatedJobs } from './src/lib/automatedJobs';
|
||||||
|
|
||||||
seed()
|
// seed()
|
||||||
initFunc()
|
initFunc()
|
||||||
startAutomatedJobs()
|
startAutomatedJobs()
|
||||||
|
|
||||||
|
|
@ -163,15 +163,6 @@ if (fs.existsSync(fallbackUiIndex)) {
|
||||||
console.warn(`Fallback UI build not found at ${fallbackUiIndex}`)
|
console.warn(`Fallback UI build not found at ${fallbackUiIndex}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve /assets/public folder at /assets route
|
|
||||||
const assetsPublicDir = path.resolve(__dirname, './assets/public');
|
|
||||||
if (fs.existsSync(assetsPublicDir)) {
|
|
||||||
app.use('/assets', express.static(assetsPublicDir));
|
|
||||||
console.log('Serving /assets from', assetsPublicDir);
|
|
||||||
} else {
|
|
||||||
console.warn('Assets public folder not found at', assetsPublicDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global error handler
|
// Global error handler
|
||||||
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
@ -180,6 +171,6 @@ app.use((err: any, req: express.Request, res: express.Response, next: express.Ne
|
||||||
res.status(status).json({ message });
|
res.status(status).json({ message });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(4000, '::', () => {
|
app.listen(4000, () => {
|
||||||
console.log("Server is running on http://localhost:4000/api/mobile/");
|
console.log("Server is running on http://localhost:4000/api/mobile/");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"migrate": "drizzle-kit generate:pg",
|
"migrate": "drizzle-kit generate:pg",
|
||||||
"build": "rimraf ./dist && tsc --project tsconfig.json && tsc-alias -p tsconfig.json",
|
"build": "rimraf ./dist && tsc --project tsconfig.json",
|
||||||
"build2": "rimraf ./dist && tsc",
|
"build2": "rimraf ./dist && tsc",
|
||||||
"db:push": "drizzle-kit push:pg",
|
"db:push": "drizzle-kit push:pg",
|
||||||
"db:seed": "tsx src/db/seed.ts",
|
"db:seed": "tsx src/db/seed.ts",
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
"pg-sdk-node": "https://phonepe.mycloudrepo.io/public/repositories/phonepe-pg-sdk-node/releases/v2/phonepe-pg-sdk-node.tgz",
|
||||||
"razorpay": "^2.9.6",
|
"razorpay": "^2.9.6",
|
||||||
"redis": "^5.9.0",
|
"redis": "^5.9.0",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
|
|
@ -54,7 +55,6 @@
|
||||||
"rimraf": "^6.1.2",
|
"rimraf": "^6.1.2",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"tsx": "^4.20.5",
|
"tsx": "^4.20.5",
|
||||||
"tsc-alias": "^1.8.16",
|
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { authenticateStaff } from "@/src/middleware/staff-auth";
|
import { authenticateStaff } from "../middleware/staff-auth";
|
||||||
import productRouter from "@/src/apis/admin-apis/apis/product.router"
|
import productRouter from "./product.router";
|
||||||
import tagRouter from "@/src/apis/admin-apis/apis/tag.router"
|
import tagRouter from "./tag.router";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { db } from "@/src/db/db_index";
|
import { db } from "../db/db_index";
|
||||||
import { productTagInfo } from "@/src/db/schema";
|
import { productTagInfo } from "../db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { ApiError } from "@/src/lib/api-error";
|
import { ApiError } from "../lib/api-error";
|
||||||
import { imageUploadS3, generateSignedUrlFromS3Url } from "@/src/lib/s3-client";
|
import { imageUploadS3, generateSignedUrlFromS3Url } from "../lib/s3-client";
|
||||||
import { deleteS3Image } from "@/src/lib/delete-image";
|
import { deleteS3Image } from "../lib/delete-image";
|
||||||
import { initializeAllStores } from '@/src/stores/store-initializer';
|
import { initializeAllStores } from '../stores/store-initializer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new product tag
|
* Create a new product tag
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { db } from "@/src/db/db_index";
|
import { db } from "../db/db_index";
|
||||||
import { productInfo, units, specialDeals, productTags } from "@/src/db/schema";
|
import { productInfo, units, specialDeals, productTags } from "../db/schema";
|
||||||
import { eq, inArray } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
import { ApiError } from "@/src/lib/api-error";
|
import { ApiError } from "../lib/api-error";
|
||||||
import { imageUploadS3, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client";
|
import { imageUploadS3, getOriginalUrlFromSignedUrl } from "../lib/s3-client";
|
||||||
import { deleteS3Image } from "@/src/lib/delete-image";
|
import { deleteS3Image } from "../lib/delete-image";
|
||||||
import type { SpecialDeal } from "@/src/db/types";
|
import type { SpecialDeal } from "../db/types";
|
||||||
import { initializeAllStores } from '@/src/stores/store-initializer';
|
import { initializeAllStores } from '../stores/store-initializer';
|
||||||
|
|
||||||
type CreateDeal = {
|
type CreateDeal = {
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
|
@ -124,6 +124,7 @@ export const updateProduct = async (req: Request, res: Response) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals:dealsRaw, imagesToDelete:imagesToDeleteRaw, tagIds } = req.body;
|
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals:dealsRaw, imagesToDelete:imagesToDeleteRaw, tagIds } = req.body;
|
||||||
|
|
||||||
|
console.log({productQuantity})
|
||||||
|
|
||||||
const deals = dealsRaw ? JSON.parse(dealsRaw) : null;
|
const deals = dealsRaw ? JSON.parse(dealsRaw) : null;
|
||||||
const imagesToDelete = imagesToDeleteRaw ? JSON.parse(imagesToDeleteRaw) : [];
|
const imagesToDelete = imagesToDeleteRaw ? JSON.parse(imagesToDeleteRaw) : [];
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { createProduct, updateProduct } from "@/src/apis/admin-apis/apis/product.controller"
|
import { createProduct, updateProduct } from "./product.controller";
|
||||||
import uploadHandler from '@/src/lib/upload-handler';
|
import uploadHandler from '../lib/upload-handler';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { createTag, getAllTags, getTagById, updateTag, deleteTag } from "@/src/apis/admin-apis/apis/product-tags.controller"
|
import { createTag, getAllTags, getTagById, updateTag, deleteTag } from "./product-tags.controller";
|
||||||
import uploadHandler from '@/src/lib/upload-handler';
|
import uploadHandler from '../lib/upload-handler';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { eq, gt, and, sql, inArray } from "drizzle-orm";
|
import { eq, gt, and, sql, inArray } from "drizzle-orm";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { db } from "@/src/db/db_index"
|
import { db } from "../db/db_index";
|
||||||
import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "@/src/db/schema"
|
import { productInfo, units, productSlots, deliverySlotInfo, productTags } from "../db/schema";
|
||||||
import { scaffoldAssetUrl } from "@/src/lib/s3-client"
|
import { generateSignedUrlsFromS3Urls } from "../lib/s3-client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get next delivery date for a product
|
* Get next delivery date for a product
|
||||||
|
|
@ -89,7 +89,7 @@ export const getAllProductsSummary = async (req: Request, res: Response) => {
|
||||||
productQuantity: product.productQuantity,
|
productQuantity: product.productQuantity,
|
||||||
isOutOfStock: product.isOutOfStock,
|
isOutOfStock: product.isOutOfStock,
|
||||||
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
|
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
|
||||||
images: scaffoldAssetUrl((product.images as string[]) || []),
|
images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { getAllProductsSummary } from "@/src/apis/common-apis/apis/common-product.controller"
|
import { getAllProductsSummary } from "./common-product.controller";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import commonProductsRouter from "@/src/apis/common-apis/apis/common-product.router"
|
import commonProductsRouter from "./common-product.router";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { drizzle } from "drizzle-orm/node-postgres"
|
import { drizzle } from "drizzle-orm/node-postgres"
|
||||||
import { migrate } from "drizzle-orm/node-postgres/migrator"
|
import { migrate } from "drizzle-orm/node-postgres/migrator"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import * as schema from "@/src/db/schema"
|
import * as schema from "./schema"
|
||||||
|
|
||||||
const db = drizzle({ connection: process.env.DATABASE_URL!, casing: "snake_case", schema: schema })
|
const db = drizzle({ connection: process.env.DATABASE_URL!, casing: "snake_case", schema: schema })
|
||||||
// const db = drizzle('postgresql://postgres:postgres@localhost:2345/pooler');
|
// const db = drizzle('postgresql://postgres:postgres@localhost:2345/pooler');
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
* This was a one time script to change the composition of the signed urls
|
* This was a one time script to change the composition of the signed urls
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { db } from '@/src/db/db_index'
|
import { db } from './db_index';
|
||||||
import {
|
import {
|
||||||
userDetails,
|
userDetails,
|
||||||
productInfo,
|
productInfo,
|
||||||
productTagInfo,
|
productTagInfo,
|
||||||
complaints
|
complaints
|
||||||
} from '@/src/db/schema';
|
} from './schema';
|
||||||
import { eq, not, isNull } from 'drizzle-orm';
|
import { eq, not, isNull } from 'drizzle-orm';
|
||||||
|
|
||||||
const S3_DOMAIN = 'https://s3.sgp.io.cloud.ovh.net';
|
const S3_DOMAIN = 'https://s3.sgp.io.cloud.ovh.net';
|
||||||
|
|
@ -122,4 +122,4 @@ runMigration()
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Process failed:', error);
|
console.error('Process failed:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
@ -192,7 +192,6 @@ export const deliverySlotInfo = mf.table('delivery_slot_info', {
|
||||||
freezeTime: timestamp('freeze_time').notNull(),
|
freezeTime: timestamp('freeze_time').notNull(),
|
||||||
isActive: boolean('is_active').notNull().default(true),
|
isActive: boolean('is_active').notNull().default(true),
|
||||||
isFlash: boolean('is_flash').notNull().default(false),
|
isFlash: boolean('is_flash').notNull().default(false),
|
||||||
isCapacityFull: boolean('is_capacity_full').notNull().default(false),
|
|
||||||
deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}),
|
deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}),
|
||||||
groupIds: jsonb('group_ids').$defaultFn(() => []),
|
groupIds: jsonb('group_ids').$defaultFn(() => []),
|
||||||
});
|
});
|
||||||
|
|
@ -391,16 +390,6 @@ export const couponApplicableProducts = mf.table('coupon_applicable_products', {
|
||||||
unq_coupon_product: unique('unique_coupon_product').on(t.couponId, t.productId),
|
unq_coupon_product: unique('unique_coupon_product').on(t.couponId, t.productId),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const userIncidents = mf.table('user_incidents', {
|
|
||||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
|
||||||
userId: integer('user_id').notNull().references(() => users.id),
|
|
||||||
orderId: integer('order_id').references(() => orders.id),
|
|
||||||
dateAdded: timestamp('date_added').notNull().defaultNow(),
|
|
||||||
adminComment: text('admin_comment'),
|
|
||||||
addedBy: integer('added_by').references(() => staffUsers.id),
|
|
||||||
negativityScore: integer('negativity_score'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const reservedCoupons = mf.table('reserved_coupons', {
|
export const reservedCoupons = mf.table('reserved_coupons', {
|
||||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||||
secretCode: varchar('secret_code', { length: 50 }).notNull().unique(),
|
secretCode: varchar('secret_code', { length: 50 }).notNull().unique(),
|
||||||
|
|
@ -430,22 +419,6 @@ export const notifCreds = mf.table('notif_creds', {
|
||||||
lastVerified: timestamp('last_verified'),
|
lastVerified: timestamp('last_verified'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const unloggedUserTokens = mf.table('unlogged_user_tokens', {
|
|
||||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
|
||||||
token: varchar({ length: 500 }).notNull().unique(),
|
|
||||||
addedAt: timestamp('added_at').notNull().defaultNow(),
|
|
||||||
lastVerified: timestamp('last_verified'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const userNotifications = mf.table('user_notifications', {
|
|
||||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
|
||||||
title: varchar('title', { length: 255 }).notNull(),
|
|
||||||
imageUrl: varchar('image_url', { length: 500 }),
|
|
||||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
|
||||||
body: text('body').notNull(),
|
|
||||||
applicableUsers: jsonb('applicable_users'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const staffRoles = mf.table('staff_roles', {
|
export const staffRoles = mf.table('staff_roles', {
|
||||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||||
roleName: staffRoleEnum('role_name').notNull(),
|
roleName: staffRoleEnum('role_name').notNull(),
|
||||||
|
|
@ -483,7 +456,6 @@ export const usersRelations = relations(users, ({ many, one }) => ({
|
||||||
applicableCoupons: many(couponApplicableUsers),
|
applicableCoupons: many(couponApplicableUsers),
|
||||||
userDetails: one(userDetails),
|
userDetails: one(userDetails),
|
||||||
notifCreds: many(notifCreds),
|
notifCreds: many(notifCreds),
|
||||||
userIncidents: many(userIncidents),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const userCredsRelations = relations(userCreds, ({ one }) => ({
|
export const userCredsRelations = relations(userCreds, ({ one }) => ({
|
||||||
|
|
@ -553,7 +525,6 @@ export const ordersRelations = relations(orders, ({ one, many }) => ({
|
||||||
orderStatus: many(orderStatus),
|
orderStatus: many(orderStatus),
|
||||||
refunds: many(refunds),
|
refunds: many(refunds),
|
||||||
couponUsages: many(couponUsage),
|
couponUsages: many(couponUsage),
|
||||||
userIncidents: many(userIncidents),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const orderItemsRelations = relations(orderItems, ({ one }) => ({
|
export const orderItemsRelations = relations(orderItems, ({ one }) => ({
|
||||||
|
|
@ -617,10 +588,6 @@ export const notifCredsRelations = relations(notifCreds, ({ one }) => ({
|
||||||
user: one(users, { fields: [notifCreds.userId], references: [users.id] }),
|
user: one(users, { fields: [notifCreds.userId], references: [users.id] }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const userNotificationsRelations = relations(userNotifications, ({}) => ({
|
|
||||||
// No relations needed for now
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const storeInfoRelations = relations(storeInfo, ({ one, many }) => ({
|
export const storeInfoRelations = relations(storeInfo, ({ one, many }) => ({
|
||||||
owner: one(staffUsers, { fields: [storeInfo.owner], references: [staffUsers.id] }),
|
owner: one(staffUsers, { fields: [storeInfo.owner], references: [staffUsers.id] }),
|
||||||
products: many(productInfo),
|
products: many(productInfo),
|
||||||
|
|
@ -681,9 +648,3 @@ export const staffRolePermissionsRelations = relations(staffRolePermissions, ({
|
||||||
role: one(staffRoles, { fields: [staffRolePermissions.staffRoleId], references: [staffRoles.id] }),
|
role: one(staffRoles, { fields: [staffRolePermissions.staffRoleId], references: [staffRoles.id] }),
|
||||||
permission: one(staffPermissions, { fields: [staffRolePermissions.staffPermissionId], references: [staffPermissions.id] }),
|
permission: one(staffPermissions, { fields: [staffRolePermissions.staffPermissionId], references: [staffPermissions.id] }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const userIncidentsRelations = relations(userIncidents, ({ one }) => ({
|
|
||||||
user: one(users, { fields: [userIncidents.userId], references: [users.id] }),
|
|
||||||
order: one(orders, { fields: [userIncidents.orderId], references: [orders.id] }),
|
|
||||||
addedBy: one(staffUsers, { fields: [userIncidents.addedBy], references: [staffUsers.id] }),
|
|
||||||
}));
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { db } from "@/src/db/db_index"
|
import { db } from "./db_index";
|
||||||
import { units, productInfo, deliverySlotInfo, productSlots, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "@/src/db/schema"
|
import { units, productInfo, deliverySlotInfo, productSlots, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "./schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { minOrderValue, deliveryCharge } from '@/src/lib/env-exporter'
|
import { minOrderValue, deliveryCharge } from '../lib/env-exporter';
|
||||||
import { CONST_KEYS } from '@/src/lib/const-keys'
|
import { CONST_KEYS } from '../lib/const-keys';
|
||||||
|
|
||||||
export async function seed() {
|
export async function seed() {
|
||||||
console.log("Seeding database...");
|
console.log("Seeding database...");
|
||||||
|
|
@ -113,10 +113,9 @@ export async function seed() {
|
||||||
{ key: CONST_KEYS.flashFreeDeliveryThreshold, value: 500 },
|
{ key: CONST_KEYS.flashFreeDeliveryThreshold, value: 500 },
|
||||||
{ key: CONST_KEYS.flashDeliveryCharge, value: 69 },
|
{ key: CONST_KEYS.flashDeliveryCharge, value: 69 },
|
||||||
{ key: CONST_KEYS.popularItems, value: [] },
|
{ key: CONST_KEYS.popularItems, value: [] },
|
||||||
{ key: CONST_KEYS.allItemsOrder, value: [] },
|
|
||||||
{ key: CONST_KEYS.versionNum, value: '1.1.0' },
|
{ key: CONST_KEYS.versionNum, value: '1.1.0' },
|
||||||
{ key: CONST_KEYS.playStoreUrl, value: 'https://play.google.com/store/apps/details?id=in.freshyo.app' },
|
{ key: CONST_KEYS.playStoreUrl, value: 'https://play.google.com/store/apps/details?id=in.freshyo.app' },
|
||||||
{ key: CONST_KEYS.appStoreUrl, value: 'https://apps.apple.com/in/app/freshyo/id6756889077' },
|
{ key: CONST_KEYS.appStoreUrl, value: 'https://play.google.com/store/apps/details?id=in.freshyo.app' },
|
||||||
{ key: CONST_KEYS.isFlashDeliveryEnabled, value: false },
|
{ key: CONST_KEYS.isFlashDeliveryEnabled, value: false },
|
||||||
{ key: CONST_KEYS.supportMobile, value: '8688182552' },
|
{ key: CONST_KEYS.supportMobile, value: '8688182552' },
|
||||||
{ key: CONST_KEYS.supportEmail, value: 'qushammohd@gmail.com' },
|
{ key: CONST_KEYS.supportEmail, value: 'qushammohd@gmail.com' },
|
||||||
|
|
@ -135,4 +134,4 @@ export async function seed() {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Seeding completed.");
|
console.log("Seeding completed.");
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ import type {
|
||||||
productCategories,
|
productCategories,
|
||||||
cartItems,
|
cartItems,
|
||||||
coupons,
|
coupons,
|
||||||
} from "@/src/db/schema";
|
} from "./schema";
|
||||||
|
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Address = InferSelectModel<typeof addresses>;
|
export type Address = InferSelectModel<typeof addresses>;
|
||||||
|
|
@ -44,4 +44,4 @@ export type OrderWithItems = Order & {
|
||||||
|
|
||||||
export type CartItemWithProduct = CartItem & {
|
export type CartItemWithProduct = CartItem & {
|
||||||
product: ProductInfo;
|
product: ProductInfo;
|
||||||
};
|
};
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import * as cron from 'node-cron';
|
import * as cron from 'node-cron';
|
||||||
import { checkPendingPayments, checkRefundStatuses } from '@/src/jobs/payment-status-checker'
|
import { checkPendingPayments, checkRefundStatuses } from './payment-status-checker';
|
||||||
|
|
||||||
const runCombinedJob = async () => {
|
const runCombinedJob = async () => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import * as cron from 'node-cron';
|
import * as cron from 'node-cron';
|
||||||
import { db } from '@/src/db/db_index'
|
import { db } from '../db/db_index';
|
||||||
import { payments, orders, deliverySlotInfo, refunds } from '@/src/db/schema'
|
import { payments, orders, deliverySlotInfo, refunds } from '../db/schema';
|
||||||
import { eq, and, gt, isNotNull } from 'drizzle-orm';
|
import { eq, and, gt, isNotNull } from 'drizzle-orm';
|
||||||
import { RazorpayPaymentService } from '@/src/lib/payments-utils'
|
import { RazorpayPaymentService } from '../lib/payments-utils';
|
||||||
|
|
||||||
interface PendingPaymentRecord {
|
interface PendingPaymentRecord {
|
||||||
payment: typeof payments.$inferSelect;
|
payment: typeof payments.$inferSelect;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import * as cron from 'node-cron';
|
import * as cron from 'node-cron';
|
||||||
import { db } from '@/src/db/db_index'
|
import { db } from '../db/db_index';
|
||||||
import { productInfo, keyValStore } from '@/src/db/schema'
|
import { productInfo, keyValStore } from '../db/schema';
|
||||||
import { inArray, eq } from 'drizzle-orm';
|
import { inArray, eq } from 'drizzle-orm';
|
||||||
import { CONST_KEYS } from '@/src/lib/const-keys'
|
import { CONST_KEYS } from '../lib/const-keys';
|
||||||
import { computeConstants } from '@/src/lib/const-store'
|
import { computeConstants } from '../lib/const-store';
|
||||||
|
|
||||||
|
|
||||||
const MUTTON_ITEMS = [
|
const MUTTON_ITEMS = [
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import axiosParent from "axios";
|
import axiosParent from "axios";
|
||||||
import { phonePeBaseUrl } from "@/src/lib/env-exporter"
|
import { phonePeBaseUrl } from "./env-exporter";
|
||||||
|
|
||||||
export const phonepeAxios = axiosParent.create({
|
export const phonepeAxios = axiosParent.create({
|
||||||
baseURL: phonePeBaseUrl,
|
baseURL: phonePeBaseUrl,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ export const CONST_KEYS = {
|
||||||
flashDeliveryCharge: 'flashDeliveryCharge',
|
flashDeliveryCharge: 'flashDeliveryCharge',
|
||||||
platformFeePercent: 'platformFeePercent',
|
platformFeePercent: 'platformFeePercent',
|
||||||
taxRate: 'taxRate',
|
taxRate: 'taxRate',
|
||||||
tester: 'tester',
|
|
||||||
minOrderAmountForCoupon: 'minOrderAmountForCoupon',
|
minOrderAmountForCoupon: 'minOrderAmountForCoupon',
|
||||||
maxCouponDiscount: 'maxCouponDiscount',
|
maxCouponDiscount: 'maxCouponDiscount',
|
||||||
flashDeliverySlotId: 'flashDeliverySlotId',
|
flashDeliverySlotId: 'flashDeliverySlotId',
|
||||||
|
|
@ -15,7 +14,6 @@ export const CONST_KEYS = {
|
||||||
playStoreUrl: 'playStoreUrl',
|
playStoreUrl: 'playStoreUrl',
|
||||||
appStoreUrl: 'appStoreUrl',
|
appStoreUrl: 'appStoreUrl',
|
||||||
popularItems: 'popularItems',
|
popularItems: 'popularItems',
|
||||||
allItemsOrder: 'allItemsOrder',
|
|
||||||
isFlashDeliveryEnabled: 'isFlashDeliveryEnabled',
|
isFlashDeliveryEnabled: 'isFlashDeliveryEnabled',
|
||||||
supportMobile: 'supportMobile',
|
supportMobile: 'supportMobile',
|
||||||
supportEmail: 'supportEmail',
|
supportEmail: 'supportEmail',
|
||||||
|
|
@ -29,7 +27,6 @@ export const CONST_LABELS: Record<ConstKey, string> = {
|
||||||
flashDeliveryCharge: 'Flash Delivery Charge',
|
flashDeliveryCharge: 'Flash Delivery Charge',
|
||||||
platformFeePercent: 'Platform Fee Percent',
|
platformFeePercent: 'Platform Fee Percent',
|
||||||
taxRate: 'Tax Rate',
|
taxRate: 'Tax Rate',
|
||||||
tester: 'Tester',
|
|
||||||
minOrderAmountForCoupon: 'Minimum Order Amount for Coupon',
|
minOrderAmountForCoupon: 'Minimum Order Amount for Coupon',
|
||||||
maxCouponDiscount: 'Maximum Coupon Discount',
|
maxCouponDiscount: 'Maximum Coupon Discount',
|
||||||
flashDeliverySlotId: 'Flash Delivery Slot ID',
|
flashDeliverySlotId: 'Flash Delivery Slot ID',
|
||||||
|
|
@ -38,7 +35,6 @@ export const CONST_LABELS: Record<ConstKey, 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',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { db } from '@/src/db/db_index'
|
import { db } from '../db/db_index';
|
||||||
import { keyValStore } from '@/src/db/schema'
|
import { keyValStore } from '../db/schema';
|
||||||
import redisClient from '@/src/lib/redis-client'
|
import redisClient from './redis-client';
|
||||||
import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from '@/src/lib/const-keys'
|
import { CONST_KEYS, CONST_KEYS_ARRAY, type ConstKey } from './const-keys';
|
||||||
|
|
||||||
const CONST_REDIS_PREFIX = 'const:';
|
const CONST_REDIS_PREFIX = 'const:';
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ export const computeConstants = async (): Promise<void> => {
|
||||||
for (const constant of constants) {
|
for (const constant of constants) {
|
||||||
const redisKey = `${CONST_REDIS_PREFIX}${constant.key}`;
|
const redisKey = `${CONST_REDIS_PREFIX}${constant.key}`;
|
||||||
const value = JSON.stringify(constant.value);
|
const value = JSON.stringify(constant.value);
|
||||||
// console.log({redisKey, value})
|
console.log({redisKey, value})
|
||||||
|
|
||||||
await redisClient.set(redisKey, value);
|
await redisClient.set(redisKey, value);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "@/src/db/db_index"
|
import { db } from "../db/db_index";
|
||||||
import { deleteImageUtil, getOriginalUrlFromSignedUrl } from "@/src/lib/s3-client"
|
import { deleteImageUtil, getOriginalUrlFromSignedUrl } from "./s3-client";
|
||||||
import { s3Url } from "@/src/lib/env-exporter"
|
import { s3Url } from "./env-exporter";
|
||||||
|
|
||||||
function extractS3Key(url: string): string | null {
|
function extractS3Key(url: string): string | null {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { db } from '@/src/db/db_index'
|
import { db } from '../db/db_index';
|
||||||
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '@/src/db/schema'
|
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '../db/schema';
|
||||||
import { eq, inArray } from 'drizzle-orm';
|
import { eq, inArray } from 'drizzle-orm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@ export const s3BucketName = process.env.S3_BUCKET_NAME as string
|
||||||
|
|
||||||
export const s3Region = process.env.S3_REGION as string
|
export const s3Region = process.env.S3_REGION as string
|
||||||
|
|
||||||
export const assetsDomain = process.env.ASSETS_DOMAIN as string;
|
|
||||||
|
|
||||||
export const s3Url = process.env.S3_URL as string
|
export const s3Url = process.env.S3_URL as string
|
||||||
|
|
||||||
export const redisUrl = process.env.REDIS_URL as string
|
export const redisUrl = process.env.REDIS_URL as string
|
||||||
|
|
@ -48,4 +46,4 @@ export const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN as string;
|
||||||
|
|
||||||
export const telegramChatIds = (process.env.TELEGRAM_CHAT_IDS as string)?.split(',').map(id => id.trim()) || [];
|
export const telegramChatIds = (process.env.TELEGRAM_CHAT_IDS as string)?.split(',').map(id => id.trim()) || [];
|
||||||
|
|
||||||
export const isDevMode = (process.env.ENV_MODE as string) === 'dev';
|
export const isDevMode = (process.env.ENV_MODE as string) === 'dev';
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import redisClient from '@/src/lib/redis-client'
|
import redisClient from './redis-client';
|
||||||
|
|
||||||
export async function enqueue(queueName: string, eventData: any): Promise<boolean> {
|
export async function enqueue(queueName: string, eventData: any): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Expo } from "expo-server-sdk";
|
import { Expo } from "expo-server-sdk";
|
||||||
import { title } from "process";
|
import { title } from "process";
|
||||||
import { expoAccessToken } from "@/src/lib/env-exporter"
|
import { expoAccessToken } from "./env-exporter";
|
||||||
|
|
||||||
const expo = new Expo({
|
const expo = new Expo({
|
||||||
accessToken: expoAccessToken,
|
accessToken: expoAccessToken,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import '@/src/lib/notif-job'
|
import './notif-job';
|
||||||
import { initializeAllStores } from '@/src/stores/store-initializer'
|
import { initializeAllStores } from '../stores/store-initializer';
|
||||||
import { initializeUserNegativityStore } from '@/src/stores/user-negativity-store'
|
import { startOrderHandler, startCancellationHandler, publishOrder } from './post-order-handler';
|
||||||
import { startOrderHandler, startCancellationHandler, publishOrder } from '@/src/lib/post-order-handler'
|
import { deleteOrders } from './delete-orders';
|
||||||
import { deleteOrders } from '@/src/lib/delete-orders'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize all application services
|
* Initialize all application services
|
||||||
|
|
@ -11,7 +10,6 @@ import { deleteOrders } from '@/src/lib/delete-orders'
|
||||||
* - Const Store (syncs constants from DB to Redis)
|
* - Const Store (syncs constants from DB to Redis)
|
||||||
* - Post Order Handler (Redis Pub/Sub subscriber)
|
* - Post Order Handler (Redis Pub/Sub subscriber)
|
||||||
* - Cancellation Handler (Redis Pub/Sub subscriber for order cancellations)
|
* - Cancellation Handler (Redis Pub/Sub subscriber for order cancellations)
|
||||||
* - User Negativity Store (caches user negativity scores in Redis)
|
|
||||||
* - Other services can be added here in the future
|
* - Other services can be added here in the future
|
||||||
*/
|
*/
|
||||||
export const initFunc = async (): Promise<void> => {
|
export const initFunc = async (): Promise<void> => {
|
||||||
|
|
@ -20,7 +18,6 @@ export const initFunc = async (): Promise<void> => {
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
initializeAllStores(),
|
initializeAllStores(),
|
||||||
initializeUserNegativityStore(),
|
|
||||||
startOrderHandler(),
|
startOrderHandler(),
|
||||||
startCancellationHandler(),
|
startCancellationHandler(),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import { Queue, Worker } from 'bullmq';
|
import { Queue, Worker } from 'bullmq';
|
||||||
import { Expo } from 'expo-server-sdk';
|
import { redisUrl } from './env-exporter';
|
||||||
import { redisUrl } from '@/src/lib/env-exporter'
|
|
||||||
import { db } from '@/src/db/db_index'
|
|
||||||
import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
|
||||||
import {
|
import {
|
||||||
NOTIFS_QUEUE,
|
NOTIFS_QUEUE,
|
||||||
ORDER_PLACED_MESSAGE,
|
ORDER_PLACED_MESSAGE,
|
||||||
|
|
@ -12,78 +9,26 @@ import {
|
||||||
ORDER_DELIVERED_MESSAGE,
|
ORDER_DELIVERED_MESSAGE,
|
||||||
ORDER_CANCELLED_MESSAGE,
|
ORDER_CANCELLED_MESSAGE,
|
||||||
REFUND_INITIATED_MESSAGE
|
REFUND_INITIATED_MESSAGE
|
||||||
} from '@/src/lib/const-strings';
|
} from './const-strings';
|
||||||
|
|
||||||
export const notificationQueue = new Queue(NOTIFS_QUEUE, {
|
export const notificationQueue = new Queue(NOTIFS_QUEUE, {
|
||||||
connection: { url: redisUrl },
|
connection: { url: redisUrl },
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
removeOnComplete: true,
|
removeOnComplete: 50,
|
||||||
removeOnFail: 10,
|
removeOnFail: 100,
|
||||||
attempts: 3,
|
attempts: 3,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const notificationWorker = new Worker(NOTIFS_QUEUE, async (job) => {
|
export const notificationWorker = new Worker(NOTIFS_QUEUE, async (job) => {
|
||||||
if (!job) return;
|
if (!job) return;
|
||||||
|
console.log(`Processing notification job ${job.id}`);
|
||||||
const { name, data } = job;
|
// TODO: Implement sendPushNotification
|
||||||
console.log(`Processing notification job ${job.id} - ${name}`);
|
|
||||||
|
|
||||||
if (name === 'send-admin-notification') {
|
|
||||||
await sendAdminNotification(data);
|
|
||||||
} else if (name === 'send-notification') {
|
|
||||||
// Handle legacy notification type
|
|
||||||
console.log('Legacy notification job - not implemented yet');
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
connection: { url: redisUrl },
|
connection: { url: redisUrl },
|
||||||
concurrency: 5,
|
concurrency: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function sendAdminNotification(data: {
|
|
||||||
token: string;
|
|
||||||
title: string;
|
|
||||||
body: string;
|
|
||||||
imageUrl: string | null;
|
|
||||||
}) {
|
|
||||||
const { token, title, body, imageUrl } = data;
|
|
||||||
|
|
||||||
// Validate Expo push token
|
|
||||||
if (!Expo.isExpoPushToken(token)) {
|
|
||||||
console.error(`Invalid Expo push token: ${token}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate signed URL for image if provided
|
|
||||||
const signedImageUrl = imageUrl ? await generateSignedUrlFromS3Url(imageUrl) : null;
|
|
||||||
|
|
||||||
// Send notification
|
|
||||||
const expo = new Expo();
|
|
||||||
const message = {
|
|
||||||
to: token,
|
|
||||||
sound: 'default',
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
data: { imageUrl },
|
|
||||||
...(signedImageUrl ? {
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
url: signedImageUrl,
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
} : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [ticket] = await expo.sendPushNotificationsAsync([message]);
|
|
||||||
console.log(`Notification sent:`, ticket);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to send notification:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationWorker.on('completed', (job) => {
|
notificationWorker.on('completed', (job) => {
|
||||||
if (job) console.log(`Notification job ${job.id} completed`);
|
if (job) console.log(`Notification job ${job.id} completed`);
|
||||||
});
|
});
|
||||||
|
|
@ -163,4 +108,4 @@ export async function sendRefundInitiatedNotification(userId: number, orderId?:
|
||||||
process.on('SIGTERM', async () => {
|
process.on('SIGTERM', async () => {
|
||||||
await notificationQueue.close();
|
await notificationQueue.close();
|
||||||
await notificationWorker.close();
|
await notificationWorker.close();
|
||||||
});
|
});
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { db } from "@/src/db/db_index"
|
import { db } from "../db/db_index";
|
||||||
import { sendPushNotificationsMany } from "@/src/lib/expo-service"
|
import { sendPushNotificationsMany } from "./expo-service";
|
||||||
// import { usersTable, notifCredsTable, notificationTable } from "@/src/db/schema";
|
// import { usersTable, notifCredsTable, notificationTable } from "../db/schema";
|
||||||
import { eq, inArray } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
|
|
||||||
// Core notification dispatch methods (renamed for clarity)
|
// Core notification dispatch methods (renamed for clarity)
|
||||||
|
|
@ -244,4 +244,4 @@ export const sendNotifToSingleUser = dispatchUserNotification;
|
||||||
/**
|
/**
|
||||||
* @deprecated Use notifyNewOffer() or other purpose-specific methods instead
|
* @deprecated Use notifyNewOffer() or other purpose-specific methods instead
|
||||||
*/
|
*/
|
||||||
export const sendNotifToManyUsers = dispatchBulkNotification;
|
export const sendNotifToManyUsers = dispatchBulkNotification;
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ApiError } from '@/src/lib/api-error'
|
import { ApiError } from './api-error';
|
||||||
import { otpSenderAuthToken } from '@/src/lib/env-exporter'
|
import { otpSenderAuthToken } from './env-exporter';
|
||||||
|
|
||||||
const otpStore = new Map<string, string>();
|
const otpStore = new Map<string, string>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import Razorpay from "razorpay";
|
import Razorpay from "razorpay";
|
||||||
import { razorpayId, razorpaySecret } from "@/src/lib/env-exporter"
|
import { razorpayId, razorpaySecret } from "./env-exporter";
|
||||||
import { db } from "@/src/db/db_index"
|
import { db } from "../db/db_index";
|
||||||
import { payments } from "@/src/db/schema"
|
import { payments } from "../db/schema";
|
||||||
|
|
||||||
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];
|
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { db } from '@/src/db/db_index'
|
import { db } from '../db/db_index';
|
||||||
import { orders, orderStatus } from '@/src/db/schema'
|
import { orders, orderStatus } from '../db/schema';
|
||||||
import redisClient from '@/src/lib/redis-client'
|
import redisClient from './redis-client';
|
||||||
import { sendTelegramMessage } from '@/src/lib/telegram-service'
|
import { sendTelegramMessage } from './telegram-service';
|
||||||
import { inArray, eq } from 'drizzle-orm';
|
import { inArray, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
const ORDER_CHANNEL = 'orders:placed';
|
const ORDER_CHANNEL = 'orders:placed';
|
||||||
|
|
@ -35,10 +35,7 @@ const formatOrderMessageWithFullData = (ordersData: any[]): string => {
|
||||||
|
|
||||||
message += '📦 <b>Items:</b>\n';
|
message += '📦 <b>Items:</b>\n';
|
||||||
order.orderItems?.forEach((item: any) => {
|
order.orderItems?.forEach((item: any) => {
|
||||||
const productQuantity = item.product?.productQuantity ?? 1
|
message += ` • ${item.product?.name || 'Unknown'} x${item.quantity}\n`;
|
||||||
const unitNotation = item.product?.unit?.shortNotation || ''
|
|
||||||
const quantityWithUnit = unitNotation ? `${productQuantity}${unitNotation}` : `${productQuantity}`
|
|
||||||
message += ` • ${item.product?.name || 'Unknown'} ${quantityWithUnit} x${item.quantity}\n`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
message += `\n💰 <b>Total:</b> ₹${order.totalAmount}\n`;
|
message += `\n💰 <b>Total:</b> ₹${order.totalAmount}\n`;
|
||||||
|
|
@ -75,12 +72,7 @@ const formatCancellationMessage = (orderData: any, cancellationData: Cancellatio
|
||||||
📞 <b>Phone:</b> ${orderData.address?.phone || 'N/A'}
|
📞 <b>Phone:</b> ${orderData.address?.phone || 'N/A'}
|
||||||
|
|
||||||
📦 <b>Items:</b>
|
📦 <b>Items:</b>
|
||||||
${orderData.orderItems?.map((item: any) => {
|
${orderData.orderItems?.map((item: any) => ` • ${item.product?.name || 'Unknown'} x${item.quantity}`).join('\n') || ' N/A'}
|
||||||
const productQuantity = item.product?.productQuantity ?? 1
|
|
||||||
const unitNotation = item.product?.unit?.shortNotation || ''
|
|
||||||
const quantityWithUnit = unitNotation ? `${productQuantity}${unitNotation}` : `${productQuantity}`
|
|
||||||
return ` • ${item.product?.name || 'Unknown'} ${quantityWithUnit} x${item.quantity}`
|
|
||||||
}).join('\n') || ' N/A'}
|
|
||||||
|
|
||||||
💰 <b>Total:</b> ₹${orderData.totalAmount}
|
💰 <b>Total:</b> ₹${orderData.totalAmount}
|
||||||
💳 <b>Refund:</b> ${orderData.refundStatus === 'na' ? 'N/A (COD)' : orderData.refundStatus || 'Pending'}
|
💳 <b>Refund:</b> ${orderData.refundStatus === 'na' ? 'N/A (COD)' : orderData.refundStatus || 'Pending'}
|
||||||
|
|
@ -110,7 +102,7 @@ export const startOrderHandler = async (): Promise<void> => {
|
||||||
where: inArray(orders.id, orderIds),
|
where: inArray(orders.id, orderIds),
|
||||||
with: {
|
with: {
|
||||||
address: true,
|
address: true,
|
||||||
orderItems: { with: { product: { with: { unit: true } } } },
|
orderItems: { with: { product: true } },
|
||||||
slot: true,
|
slot: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -155,7 +147,7 @@ export const startCancellationHandler = async (): Promise<void> => {
|
||||||
where: eq(orders.id, cancellationData.orderId),
|
where: eq(orders.id, cancellationData.orderId),
|
||||||
with: {
|
with: {
|
||||||
address: true,
|
address: true,
|
||||||
orderItems: { with: { product: { with: { unit: true } } } },
|
orderItems: { with: { product: true } },
|
||||||
refunds: true,
|
refunds: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createClient, RedisClientType } from 'redis';
|
import { createClient, RedisClientType } from 'redis';
|
||||||
import { redisUrl } from '@/src/lib/env-exporter'
|
import { redisUrl } from './env-exporter';
|
||||||
|
|
||||||
class RedisClient {
|
class RedisClient {
|
||||||
private client: RedisClientType;
|
private client: RedisClientType;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { db } from "@/src/db/db_index"
|
import { db } from "../db/db_index";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constants for role names to avoid hardcoding and typos
|
* Constants for role names to avoid hardcoding and typos
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "@/src/lib/env-exporter"
|
// import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "../lib/env-exporter"
|
||||||
import { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
|
import { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
|
||||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
||||||
import signedUrlCache from "@/src/lib/signed-url-cache"
|
import signedUrlCache from "./signed-url-cache"
|
||||||
import { s3AccessKeyId, s3Region, s3Url, s3SecretAccessKey, s3BucketName, assetsDomain } from "@/src/lib/env-exporter"
|
import { s3AccessKeyId, s3Region, s3Url, s3SecretAccessKey, s3BucketName } from "./env-exporter";
|
||||||
import { db } from "@/src/db/db_index"; // Adjust path if needed
|
import { db } from "../db/db_index"; // Adjust path if needed
|
||||||
import { uploadUrlStatus } from "@/src/db/schema"
|
import { uploadUrlStatus } from "../db/schema";
|
||||||
import { and, eq } from 'drizzle-orm';
|
import { and, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
const s3Client = new S3Client({
|
const s3Client = new S3Client({
|
||||||
|
|
@ -60,22 +60,6 @@ export async function deleteImageUtil({bucket = s3BucketName, keys}:{bucket?:str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scaffoldAssetUrl(input: string | null): string
|
|
||||||
export function scaffoldAssetUrl(input: (string | null)[]): string[]
|
|
||||||
export function scaffoldAssetUrl(input: string | null | (string | null)[]): string | string[] {
|
|
||||||
if (Array.isArray(input)) {
|
|
||||||
return input.map(key => scaffoldAssetUrl(key) as string);
|
|
||||||
}
|
|
||||||
if (!input) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const normalizedKey = input.replace(/^\/+/, '');
|
|
||||||
const domain = assetsDomain.endsWith('/')
|
|
||||||
? assetsDomain.slice(0, -1)
|
|
||||||
: assetsDomain;
|
|
||||||
return `${domain}/${normalizedKey}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a signed URL from an S3 URL
|
* Generate a signed URL from an S3 URL
|
||||||
|
|
@ -218,4 +202,4 @@ export async function claimUploadUrl(url: string): Promise<void> {
|
||||||
console.error('Error claiming upload URL:', error);
|
console.error('Error claiming upload URL:', error);
|
||||||
throw new Error('Failed to claim upload URL');
|
throw new Error('Failed to claim upload URL');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { isDevMode, telegramBotToken, telegramChatIds } from '@/src/lib/env-exporter'
|
import { isDevMode, telegramBotToken, telegramChatIds } from './env-exporter';
|
||||||
|
|
||||||
const BOT_TOKEN = telegramBotToken;
|
const BOT_TOKEN = telegramBotToken;
|
||||||
const CHAT_IDS = telegramChatIds;
|
const CHAT_IDS = telegramChatIds;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Router, Request, Response, NextFunction } from "express";
|
import { Router, Request, Response, NextFunction } from "express";
|
||||||
import avRouter from "@/src/apis/admin-apis/apis/av-router"
|
import avRouter from "./admin-apis/av-router";
|
||||||
import { ApiError } from "@/src/lib/api-error"
|
import { ApiError } from "./lib/api-error";
|
||||||
import v1Router from "@/src/v1-router"
|
import v1Router from "./v1-router";
|
||||||
import testController from "@/src/test-controller"
|
import testController from "./test-controller";
|
||||||
import { authenticateUser } from "@/src/middleware/auth.middleware"
|
import { authenticateUser } from "./middleware/auth.middleware";
|
||||||
import { raiseComplaint } from "@/src/uv-apis/user-rest.controller"
|
import { raiseComplaint } from "./uv-apis/user-rest.controller";
|
||||||
import uploadHandler from "@/src/lib/upload-handler"
|
import uploadHandler from "./lib/upload-handler";
|
||||||
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { db } from '@/src/db/db_index'
|
import { db } from '../db/db_index';
|
||||||
import { staffUsers, userDetails } from '@/src/db/schema'
|
import { staffUsers, userDetails } from '../db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { ApiError } from '@/src/lib/api-error'
|
import { ApiError } from '../lib/api-error';
|
||||||
|
|
||||||
interface AuthenticatedRequest extends Request {
|
interface AuthenticatedRequest extends Request {
|
||||||
user?: {
|
user?: {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue