240 lines
8.1 KiB
TypeScript
240 lines
8.1 KiB
TypeScript
import React, { useState, useCallback, useMemo } from "react";
|
|
import { View, TouchableOpacity, Alert, ActivityIndicator } from "react-native";
|
|
import { MaterialIcons } from "@expo/vector-icons";
|
|
import { useRouter } from "expo-router";
|
|
import { tw, ConfirmationDialog, MyText, MyFlatList, useMarkDataFetchers, ImageViewerURI } from "common-ui";
|
|
import { trpc } from "@/src/trpc-client";
|
|
|
|
export default function Complaints() {
|
|
const router = useRouter();
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [selectedComplaintId, setSelectedComplaintId] = useState<number | null>(null);
|
|
|
|
const {
|
|
data,
|
|
isLoading,
|
|
isError,
|
|
error,
|
|
fetchNextPage,
|
|
hasNextPage,
|
|
isFetchingNextPage,
|
|
refetch,
|
|
} = trpc.admin.complaint.getAll.useInfiniteQuery(
|
|
{ limit: 20 },
|
|
{
|
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
}
|
|
);
|
|
|
|
useMarkDataFetchers(() => {
|
|
refetch();
|
|
});
|
|
|
|
const resolveComplaint = trpc.admin.complaint.resolve.useMutation();
|
|
|
|
const complaints = useMemo(() => {
|
|
const allComplaints = data?.pages.flatMap((page) => page.complaints) || [];
|
|
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) => {
|
|
setSelectedComplaintId(id);
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const handleConfirmResolve = (response?: string) => {
|
|
if (!selectedComplaintId) return;
|
|
|
|
resolveComplaint.mutate(
|
|
{ id: String(selectedComplaintId), response },
|
|
{
|
|
onSuccess: () => {
|
|
Alert.alert("Success", "Complaint marked as resolved");
|
|
refetch();
|
|
setDialogOpen(false);
|
|
setSelectedComplaintId(null);
|
|
},
|
|
onError: (error: any) => {
|
|
Alert.alert(
|
|
"Error",
|
|
error.message || "Failed to resolve complaint"
|
|
);
|
|
setDialogOpen(false);
|
|
setSelectedComplaintId(null);
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
|
|
<ActivityIndicator size="large" color="#3B82F6" />
|
|
<MyText style={tw`text-gray-500 mt-4`}>Loading complaints...</MyText>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (isError) {
|
|
return (
|
|
<View style={tw`flex-1 justify-center items-center bg-gray-50 p-6`}>
|
|
<MaterialIcons name="error-outline" size={48} color="#EF4444" />
|
|
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Error</MyText>
|
|
<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 (
|
|
<View style={tw`flex-1 bg-gray-50`}>
|
|
<MyFlatList
|
|
style={tw`flex-1`}
|
|
contentContainerStyle={tw`px-4 py-4`}
|
|
data={complaints}
|
|
keyExtractor={(item) => item.id.toString()}
|
|
onEndReached={handleLoadMore}
|
|
onEndReachedThreshold={0.5}
|
|
renderItem={({ item }) => (
|
|
<View style={tw`bg-white p-4 mb-4 rounded-2xl shadow-sm border border-gray-100`}>
|
|
<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.text}
|
|
</MyText>
|
|
|
|
{item.images && item.images.length > 0 && (
|
|
<View style={tw`mb-3`}>
|
|
<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.map((imageUri: string, index: number) => (
|
|
<ImageViewerURI
|
|
key={index}
|
|
uri={imageUri}
|
|
style={tw`w-16 h-16 rounded-lg border border-gray-200`}
|
|
/>
|
|
))}
|
|
</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" && (
|
|
<TouchableOpacity
|
|
onPress={() => handleMarkResolved(item.id)}
|
|
style={tw`bg-blue-500 py-3 rounded-xl items-center shadow-sm mt-2`}
|
|
>
|
|
<MyText style={tw`text-white font-semibold`}>
|
|
Resolve Complaint
|
|
</MyText>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
)}
|
|
ListEmptyComponent={
|
|
<View style={tw`flex-1 justify-center items-center py-20`}>
|
|
<View style={tw`bg-white p-6 rounded-full shadow-sm mb-4`}>
|
|
<MaterialIcons name="inbox" size={48} color="#D1D5DB" />
|
|
</View>
|
|
<MyText style={tw`text-gray-900 text-lg font-bold`}>
|
|
No complaints
|
|
</MyText>
|
|
<MyText style={tw`text-gray-500 text-center mt-2`}>
|
|
All complaints will appear here
|
|
</MyText>
|
|
</View>
|
|
}
|
|
ListFooterComponent={
|
|
isFetchingNextPage ? (
|
|
<View style={tw`py-4 items-center flex-row justify-center`}>
|
|
<ActivityIndicator size="small" color="#3B82F6" />
|
|
<MyText style={tw`text-gray-500 ml-2`}>Loading more...</MyText>
|
|
</View>
|
|
) : null
|
|
}
|
|
/>
|
|
<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>
|
|
);
|
|
}
|