freshyo/apps/admin-ui/components/UserIncidentsView.tsx
2026-03-04 23:36:11 +05:30

206 lines
7.4 KiB
TypeScript

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