freshyo/apps/admin-ui/app/(drawer)/complaints/index.tsx
2026-02-21 16:13:23 +05:30

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