feat: implement real-time customer support chat system with socket.io integration
This commit is contained in:
parent
eecbf9484c
commit
495c1f5f6b
@ -117,3 +117,5 @@ dependencies {
|
|||||||
implementation jscFlavor
|
implementation jscFlavor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")
|
||||||
@ -9,3 +9,4 @@ export * from './orderApi';
|
|||||||
export * from './offerApi';
|
export * from './offerApi';
|
||||||
export * from './reviewApi';
|
export * from './reviewApi';
|
||||||
export * from './walletApi';
|
export * from './walletApi';
|
||||||
|
export * from './supportApi';
|
||||||
48
app/api/supportApi.ts
Normal file
48
app/api/supportApi.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { apiClient } from '@services';
|
||||||
|
import {
|
||||||
|
SupportTicket,
|
||||||
|
CreateTicketPayload,
|
||||||
|
PostMessagePayload,
|
||||||
|
SupportMessage,
|
||||||
|
} from '@interfaces';
|
||||||
|
|
||||||
|
export const createTicketApi = async (payload: CreateTicketPayload) => {
|
||||||
|
return await apiClient.post<SupportTicket>('/support/tickets', payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMyTicketsApi = async () => {
|
||||||
|
return await apiClient.get<SupportTicket[]>('/support/tickets');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTicketDetailsApi = async (ticketId: string) => {
|
||||||
|
return await apiClient.get<SupportTicket>(`/support/tickets/${ticketId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const postMessageApi = async (
|
||||||
|
ticketId: string,
|
||||||
|
payload: PostMessagePayload,
|
||||||
|
) => {
|
||||||
|
return await apiClient.post<SupportMessage>(
|
||||||
|
`/support/tickets/${ticketId}/messages`,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reopenTicketApi = async (ticketId: string) => {
|
||||||
|
return await apiClient.patch<SupportTicket>(
|
||||||
|
`/support/tickets/${ticketId}/reopen`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadSupportAttachmentApi = async (formData: FormData) => {
|
||||||
|
return await apiClient.post<{ id: string; filename: string; url: string }>(
|
||||||
|
'/support/upload',
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -37,12 +37,12 @@ const MENU_SECTIONS: MenuSectionData[] = [
|
|||||||
{
|
{
|
||||||
title: 'Account',
|
title: 'Account',
|
||||||
items: [
|
items: [
|
||||||
{
|
// {
|
||||||
icon: '📍',
|
// icon: '📍',
|
||||||
label: 'My Addresses',
|
// label: 'My Addresses',
|
||||||
subLabel: 'Manage delivery addresses',
|
// subLabel: 'Manage delivery addresses',
|
||||||
tint: '#FDECEA',
|
// tint: '#FDECEA',
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
icon: '💳',
|
icon: '💳',
|
||||||
label: 'Wallet',
|
label: 'Wallet',
|
||||||
@ -136,12 +136,12 @@ export const AccountScreen: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity
|
{/* <TouchableOpacity
|
||||||
style={styles.editProfileButton}
|
style={styles.editProfileButton}
|
||||||
activeOpacity={0.75}
|
activeOpacity={0.75}
|
||||||
>
|
>
|
||||||
<Text style={styles.editProfileButtonText}>Edit Profile</Text>
|
<Text style={styles.editProfileButtonText}>Edit Profile</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity> */}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Quick stats */}
|
{/* Quick stats */}
|
||||||
|
|||||||
@ -59,4 +59,190 @@ export const getStyles = (colors: any) => StyleSheet.create({
|
|||||||
fontWeight: typography.fontWeight.semibold,
|
fontWeight: typography.fontWeight.semibold,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
},
|
},
|
||||||
|
tabContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 4,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
tabButton: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
activeTabButton: {
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
tabText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
activeTabText: {
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
ticketCard: {
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
|
marginBottom: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
},
|
||||||
|
ticketHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
ticketNumber: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
ticketDate: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
ticketSubjectText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
ticketFooter: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
paddingTop: 8,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
categoryBadge: {
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
categoryText: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
statusBadge: {
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
createTicketBtn: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingVertical: 14,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 12,
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
createTicketBtnText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderRadius: 16,
|
||||||
|
width: '100%',
|
||||||
|
maxHeight: '90%',
|
||||||
|
padding: 20,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.25,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 5,
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
fontSize: typography.fontSize.lg,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 6,
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
textArea: {
|
||||||
|
minHeight: 80,
|
||||||
|
textAlignVertical: 'top',
|
||||||
|
},
|
||||||
|
categorySelect: {
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
categorySelectText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
modalActions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginTop: 20,
|
||||||
|
},
|
||||||
|
modalBtn: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
cancelBtn: {
|
||||||
|
backgroundColor: colors.border,
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
submitBtn: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
cancelBtnText: {
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
submitBtnText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,11 +1,24 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, ScrollView, TouchableOpacity } from 'react-native';
|
import {
|
||||||
import { useNavigation } from '@react-navigation/native';
|
View,
|
||||||
|
Text,
|
||||||
|
ScrollView,
|
||||||
|
TouchableOpacity,
|
||||||
|
FlatList,
|
||||||
|
Modal,
|
||||||
|
TextInput,
|
||||||
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useNavigation, useIsFocused } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { getStyles } from './helpSupportScreen.styles';
|
import { getStyles } from './helpSupportScreen.styles';
|
||||||
import { Header } from '@components';
|
import { Header } from '@components';
|
||||||
import { useAppTheme } from '@theme';
|
import { useAppTheme } from '@theme';
|
||||||
import { AppStackParamList } from '../../../navigation/appStack';
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
import { useCustomerSupport } from '../../../hooks/useCustomerSupport';
|
||||||
|
import { TicketCategory, TicketPriority } from '@interfaces';
|
||||||
|
import { formatDate } from '../../../utils/helper';
|
||||||
|
|
||||||
type HelpSupportNavProp = StackNavigationProp<AppStackParamList, 'HelpSupportScreen'>;
|
type HelpSupportNavProp = StackNavigationProp<AppStackParamList, 'HelpSupportScreen'>;
|
||||||
|
|
||||||
@ -16,39 +29,297 @@ const FAQS = [
|
|||||||
{ q: 'What payment methods are accepted?', a: 'UPI, Credit/Debit Card, Wallet, and Cash on Delivery.' },
|
{ q: 'What payment methods are accepted?', a: 'UPI, Credit/Debit Card, Wallet, and Cash on Delivery.' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const TICKET_CATEGORIES: { value: TicketCategory; label: string }[] = [
|
||||||
|
{ value: 'ORDER', label: 'Order' },
|
||||||
|
{ value: 'PAYMENT', label: 'Payment' },
|
||||||
|
{ value: 'PAYOUT', label: 'Payout' },
|
||||||
|
{ value: 'TECHNICAL_ISSUE', label: 'Technical Issue' },
|
||||||
|
{ value: 'ACCOUNT', label: 'Account' },
|
||||||
|
{ value: 'OTHER', label: 'Other' },
|
||||||
|
];
|
||||||
|
|
||||||
export const HelpSupportScreen: React.FC = () => {
|
export const HelpSupportScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
const styles = getStyles(colors);
|
const styles = getStyles(colors);
|
||||||
const navigation = useNavigation<HelpSupportNavProp>();
|
const navigation = useNavigation<HelpSupportNavProp>();
|
||||||
|
const isFocused = useIsFocused();
|
||||||
|
|
||||||
|
// Tab State
|
||||||
|
const [activeTab, setActiveTab] = useState<'faq' | 'tickets'>('faq');
|
||||||
|
|
||||||
|
// Custom Support Hook
|
||||||
|
const { tickets, loading, fetchTickets, createTicket } = useCustomerSupport();
|
||||||
|
|
||||||
|
// Create Ticket Modal State
|
||||||
|
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||||
|
const [categorySelectVisible, setCategorySelectVisible] = useState(false);
|
||||||
|
|
||||||
|
const [category, setCategory] = useState<TicketCategory>('ORDER');
|
||||||
|
const [priority] = useState<TicketPriority>('MEDIUM');
|
||||||
|
const [subject, setSubject] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFocused && activeTab === 'tickets') {
|
||||||
|
fetchTickets();
|
||||||
|
}
|
||||||
|
}, [isFocused, activeTab, fetchTickets]);
|
||||||
|
|
||||||
|
const handleCreateTicketSubmit = async () => {
|
||||||
|
if (!subject.trim()) {
|
||||||
|
Alert.alert('Error', 'Please enter a subject.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!description.trim()) {
|
||||||
|
Alert.alert('Error', 'Please enter ticket details.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newTicket = await createTicket({
|
||||||
|
category,
|
||||||
|
priority,
|
||||||
|
subject: subject.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
});
|
||||||
|
setCreateModalVisible(false);
|
||||||
|
setSubject('');
|
||||||
|
setDescription('');
|
||||||
|
Alert.alert('Success', 'Support Ticket created successfully!');
|
||||||
|
// Navigate to chat
|
||||||
|
navigation.navigate('SupportChatScreen', { ticketId: newTicket.id });
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Error', 'Failed to create support ticket. Please try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'OPEN':
|
||||||
|
return { bg: '#E3F2FD', text: '#1E88E5' }; // Blue
|
||||||
|
case 'IN_PROGRESS':
|
||||||
|
return { bg: '#FFF3E0', text: '#FB8C00' }; // Amber
|
||||||
|
case 'RESOLVED':
|
||||||
|
return { bg: '#E8F5E9', text: '#43A047' }; // Green
|
||||||
|
case 'CLOSED':
|
||||||
|
return { bg: '#ECEFF1', text: '#546E7A' }; // Grey
|
||||||
|
default:
|
||||||
|
return { bg: '#F5F5F5', text: '#9E9E9E' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFaqTab = () => (
|
||||||
|
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
|
||||||
|
<Text style={styles.sectionTitle}>Frequently Asked Questions</Text>
|
||||||
|
{FAQS.map((faq, index) => (
|
||||||
|
<View key={index} style={styles.faqItem}>
|
||||||
|
<Text style={styles.faqQuestion}>{faq.q}</Text>
|
||||||
|
<Text style={styles.faqAnswer}>{faq.a}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Text style={styles.sectionTitle}>Contact Support</Text>
|
||||||
|
<TouchableOpacity style={styles.supportCard} activeOpacity={0.7}>
|
||||||
|
<Text style={styles.supportIcon}>📞</Text>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.supportLabel}>Call us</Text>
|
||||||
|
<Text style={styles.supportValue}>+91 1800 123 4567</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={styles.supportCard} activeOpacity={0.7}>
|
||||||
|
<Text style={styles.supportIcon}>✉️</Text>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.supportLabel}>Email us</Text>
|
||||||
|
<Text style={styles.supportValue}>support@sgdelivery.com</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTicketsTab = () => (
|
||||||
|
<View style={[styles.container, { padding: 16 }]}>
|
||||||
|
{loading && tickets.length === 0 ? (
|
||||||
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<ActivityIndicator size="large" color={colors.primary} />
|
||||||
|
<Text style={{ marginTop: 8, color: colors.textSecondary }}>Fetching tickets...</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<FlatList
|
||||||
|
data={tickets}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<View style={{ paddingVertical: 40, alignItems: 'center' }}>
|
||||||
|
<Text style={{ fontSize: 16, color: colors.textSecondary, marginBottom: 12 }}>
|
||||||
|
No support tickets found
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 12, color: colors.textSecondary, textAlign: 'center' }}>
|
||||||
|
Need help with your account or order? Create a ticket below!
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
renderItem={({ item }) => {
|
||||||
|
const statusStyle = getStatusColor(item.status);
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.ticketCard}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
onPress={() => navigation.navigate('SupportChatScreen', { ticketId: item.id })}
|
||||||
|
>
|
||||||
|
<View style={styles.ticketHeader}>
|
||||||
|
<Text style={styles.ticketNumber}>Ticket #{item.ticketNumber}</Text>
|
||||||
|
<Text style={styles.ticketDate}>{formatDate(item.createdAt)}</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.ticketSubjectText} numberOfLines={1}>
|
||||||
|
{item.subject}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.ticketFooter}>
|
||||||
|
<View style={styles.categoryBadge}>
|
||||||
|
<Text style={styles.categoryText}>{item.category.replace('_', ' ')}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.statusBadge, { backgroundColor: statusStyle.bg }]}>
|
||||||
|
<Text style={[styles.statusText, { color: statusStyle.text }]}>
|
||||||
|
{item.status.replace('_', ' ')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.createTicketBtn}
|
||||||
|
onPress={() => setCreateModalVisible(true)}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={styles.createTicketBtnText}>+ Create Support Ticket</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title="Help & Support" onBack={() => navigation.goBack()} />
|
<Header title="Help & Support" onBack={() => navigation.goBack()} />
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
|
||||||
<Text style={styles.sectionTitle}>Frequently Asked Questions</Text>
|
|
||||||
{FAQS.map((faq, index) => (
|
|
||||||
<View key={index} style={styles.faqItem}>
|
|
||||||
<Text style={styles.faqQuestion}>{faq.q}</Text>
|
|
||||||
<Text style={styles.faqAnswer}>{faq.a}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Text style={styles.sectionTitle}>Contact Support</Text>
|
<View style={{ paddingHorizontal: 16, paddingTop: 16 }}>
|
||||||
<TouchableOpacity style={styles.supportCard} activeOpacity={0.7}>
|
<View style={styles.tabContainer}>
|
||||||
<Text style={styles.supportIcon}>📞</Text>
|
<TouchableOpacity
|
||||||
<View>
|
style={[styles.tabButton, activeTab === 'faq' && styles.activeTabButton]}
|
||||||
<Text style={styles.supportLabel}>Call us</Text>
|
onPress={() => setActiveTab('faq')}
|
||||||
<Text style={styles.supportValue}>+91 1800 123 4567</Text>
|
>
|
||||||
|
<Text style={[styles.tabText, activeTab === 'faq' && styles.activeTabText]}>FAQs</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.tabButton, activeTab === 'tickets' && styles.activeTabButton]}
|
||||||
|
onPress={() => setActiveTab('tickets')}
|
||||||
|
>
|
||||||
|
<Text style={[styles.tabText, activeTab === 'tickets' && styles.activeTabText]}>
|
||||||
|
My Support Tickets
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{activeTab === 'faq' ? renderFaqTab() : renderTicketsTab()}
|
||||||
|
|
||||||
|
{/* Create Ticket Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={createModalVisible}
|
||||||
|
transparent
|
||||||
|
animationType="slide"
|
||||||
|
onRequestClose={() => setCreateModalVisible(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
<Text style={styles.modalTitle}>Create Support Ticket</Text>
|
||||||
|
|
||||||
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
|
<Text style={styles.label}>Category</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.categorySelect}
|
||||||
|
onPress={() => setCategorySelectVisible(true)}
|
||||||
|
>
|
||||||
|
<Text style={styles.categorySelectText}>
|
||||||
|
{TICKET_CATEGORIES.find((c) => c.value === category)?.label || 'Select category'}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: colors.textSecondary }}>▼</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<Text style={styles.label}>Subject</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={subject}
|
||||||
|
onChangeText={setSubject}
|
||||||
|
placeholder="What is the issue about?"
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={styles.label}>Description</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, styles.textArea]}
|
||||||
|
value={description}
|
||||||
|
onChangeText={setDescription}
|
||||||
|
placeholder="Please describe your issue in detail..."
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
multiline
|
||||||
|
numberOfLines={4}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.modalBtn, styles.cancelBtn]}
|
||||||
|
onPress={() => setCreateModalVisible(false)}
|
||||||
|
>
|
||||||
|
<Text style={styles.cancelBtnText}>Cancel</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.modalBtn, styles.submitBtn]}
|
||||||
|
onPress={handleCreateTicketSubmit}
|
||||||
|
>
|
||||||
|
<Text style={styles.submitBtnText}>Submit</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
<TouchableOpacity style={styles.supportCard} activeOpacity={0.7}>
|
</Modal>
|
||||||
<Text style={styles.supportIcon}>✉️</Text>
|
|
||||||
<View>
|
{/* Category Dropdown Modal */}
|
||||||
<Text style={styles.supportLabel}>Email us</Text>
|
<Modal
|
||||||
<Text style={styles.supportValue}>support@sgdelivery.com</Text>
|
visible={categorySelectVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setCategorySelectVisible(false)}
|
||||||
|
>
|
||||||
|
<View style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', padding: 24 }}>
|
||||||
|
<View style={{ backgroundColor: colors.cardBg, borderRadius: 12, padding: 16 }}>
|
||||||
|
<Text style={{ fontSize: 18, fontWeight: 'bold', color: colors.text, marginBottom: 12, textAlign: 'center' }}>
|
||||||
|
Select Category
|
||||||
|
</Text>
|
||||||
|
{TICKET_CATEGORIES.map((item) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={item.value}
|
||||||
|
style={{ paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.border }}
|
||||||
|
onPress={() => {
|
||||||
|
setCategory(item.value);
|
||||||
|
setCategorySelectVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 16, color: colors.text }}>{item.label}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ marginTop: 16, paddingVertical: 12, alignItems: 'center' }}
|
||||||
|
onPress={() => setCategorySelectVisible(false)}
|
||||||
|
>
|
||||||
|
<Text style={{ color: colors.primary, fontWeight: 'bold' }}>Close</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
</ScrollView>
|
</Modal>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
export default HelpSupportScreen;
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from './helpSupportScreen';
|
export * from './helpSupportScreen';
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import {
|
|||||||
} from '@store';
|
} from '@store';
|
||||||
import { getAllCategoriesThunk, getAllProductsThunk } from './thunk';
|
import { getAllCategoriesThunk, getAllProductsThunk } from './thunk';
|
||||||
import { getCategoryEmoji } from '@utils';
|
import { getCategoryEmoji } from '@utils';
|
||||||
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
|
|
||||||
type NavProp = CompositeNavigationProp<
|
type NavProp = CompositeNavigationProp<
|
||||||
BottomTabNavigationProp<MainTabParamList, 'HomeScreen'>,
|
BottomTabNavigationProp<MainTabParamList, 'HomeScreen'>,
|
||||||
@ -121,7 +122,11 @@ export const HomeScreen: React.FC = () => {
|
|||||||
<View style={styles.topBar}>
|
<View style={styles.topBar}>
|
||||||
<TouchableOpacity style={styles.addressBar} activeOpacity={0.7}>
|
<TouchableOpacity style={styles.addressBar} activeOpacity={0.7}>
|
||||||
<View style={styles.pinBadge}>
|
<View style={styles.pinBadge}>
|
||||||
<Text style={styles.pinEmoji}>📍</Text>
|
<MaterialCommunityIcons
|
||||||
|
name="map-marker"
|
||||||
|
size={22}
|
||||||
|
color="#05824C"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.addressTextWrap}>
|
<View style={styles.addressTextWrap}>
|
||||||
<Text style={styles.addressLabel}>Deliver to</Text>
|
<Text style={styles.addressLabel}>Deliver to</Text>
|
||||||
@ -139,7 +144,11 @@ export const HomeScreen: React.FC = () => {
|
|||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
onPress={() => navigation.navigate('CartScreen')}
|
onPress={() => navigation.navigate('CartScreen')}
|
||||||
>
|
>
|
||||||
<Text style={styles.avatarEmoji}>🧺</Text>
|
<MaterialCommunityIcons
|
||||||
|
name="cart"
|
||||||
|
size={26}
|
||||||
|
color="#666"
|
||||||
|
/>
|
||||||
<View style={styles.notifDot} />
|
<View style={styles.notifDot} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -22,3 +22,4 @@ export * from './accountScreen';
|
|||||||
export * from './writeReviewScreen';
|
export * from './writeReviewScreen';
|
||||||
export * from './orderDetailsScreen';
|
export * from './orderDetailsScreen';
|
||||||
export * from './walletScreen';
|
export * from './walletScreen';
|
||||||
|
export * from './supportChatScreen'
|
||||||
|
|||||||
@ -252,4 +252,104 @@ export const getStyles = (colors: any) =>
|
|||||||
color: '#92400E',
|
color: '#92400E',
|
||||||
fontWeight: typography.fontWeight.medium,
|
fontWeight: typography.fontWeight.medium,
|
||||||
},
|
},
|
||||||
|
orderNeedHelpBtn: {
|
||||||
|
marginTop: 14,
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
backgroundColor: '#E8F5E9',
|
||||||
|
borderRadius: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#C8E6C9',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
orderNeedHelpText: {
|
||||||
|
color: colors.primary,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
},
|
||||||
|
// Help modal specific styles
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderRadius: 16,
|
||||||
|
width: '100%',
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 6,
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
textArea: {
|
||||||
|
minHeight: 80,
|
||||||
|
textAlignVertical: 'top',
|
||||||
|
},
|
||||||
|
categorySelect: {
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
categorySelectText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
modalActions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginTop: 20,
|
||||||
|
},
|
||||||
|
modalBtn: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
cancelBtn: {
|
||||||
|
backgroundColor: colors.border,
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
submitBtn: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
cancelBtnText: {
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
submitBtnText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,9 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
Modal,
|
||||||
|
TextInput,
|
||||||
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
import { useRoute, useNavigation } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
@ -16,8 +19,9 @@ import { RootState, useAppDispatch, useAppSelector } from '@store';
|
|||||||
import { getOrderByIdThunk } from '../checkoutPaymentScreen';
|
import { getOrderByIdThunk } from '../checkoutPaymentScreen';
|
||||||
import { formatDate, formatTime } from '@utils/helper';
|
import { formatDate, formatTime } from '@utils/helper';
|
||||||
import { submitReview } from './thunk';
|
import { submitReview } from './thunk';
|
||||||
import { giveRatingPayload, OrderItem } from '@interfaces';
|
import { giveRatingPayload, OrderItem, TicketCategory } from '@interfaces';
|
||||||
import { AppStackParamList } from '@navigation';
|
import { AppStackParamList } from '@navigation';
|
||||||
|
import { useCustomerSupport } from '../../../hooks/useCustomerSupport';
|
||||||
|
|
||||||
type OrderDetailsRouteProp = RouteProp<AppStackParamList, 'OrderDetailsScreen'>;
|
type OrderDetailsRouteProp = RouteProp<AppStackParamList, 'OrderDetailsScreen'>;
|
||||||
type OrderDetailsNavProp = StackNavigationProp<AppStackParamList>;
|
type OrderDetailsNavProp = StackNavigationProp<AppStackParamList>;
|
||||||
@ -45,6 +49,43 @@ export const OrderDetailsScreen: React.FC = () => {
|
|||||||
|
|
||||||
const isDelivered = orderDetails?.status === 'DELIVERED';
|
const isDelivered = orderDetails?.status === 'DELIVERED';
|
||||||
|
|
||||||
|
// ── Support / Need Help modal state ───────────────────────────────────────
|
||||||
|
const [helpModalVisible, setHelpModalVisible] = useState(false);
|
||||||
|
const [categorySelectVisible, setCategorySelectVisible] = useState(false);
|
||||||
|
const [helpCategory, setHelpCategory] = useState<TicketCategory>('ORDER');
|
||||||
|
const [helpDescription, setHelpDescription] = useState('');
|
||||||
|
const [submittingTicket, setSubmittingTicket] = useState(false);
|
||||||
|
|
||||||
|
const { createTicket } = useCustomerSupport();
|
||||||
|
|
||||||
|
const handleHelpSubmit = async () => {
|
||||||
|
if (!orderDetails) return;
|
||||||
|
if (!helpDescription.trim()) {
|
||||||
|
Alert.alert('Error', 'Please describe the issue in detail.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmittingTicket(true);
|
||||||
|
try {
|
||||||
|
const orderShortId = orderDetails.orderNumber?.split('-').pop() || orderDetails.id;
|
||||||
|
const newTicket = await createTicket({
|
||||||
|
category: helpCategory,
|
||||||
|
priority: 'MEDIUM',
|
||||||
|
subject: `Issue with Order #${orderShortId}`,
|
||||||
|
description: helpDescription.trim(),
|
||||||
|
orderId: orderDetails.id,
|
||||||
|
});
|
||||||
|
setHelpModalVisible(false);
|
||||||
|
setHelpDescription('');
|
||||||
|
Alert.alert('Success', 'Support ticket created successfully!');
|
||||||
|
navigation.navigate('SupportChatScreen', { ticketId: newTicket.id });
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Error', 'Failed to create support ticket. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setSubmittingTicket(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (orderId) {
|
if (orderId) {
|
||||||
dispatch(getOrderByIdThunk(orderId));
|
dispatch(getOrderByIdThunk(orderId));
|
||||||
@ -140,6 +181,15 @@ export const OrderDetailsScreen: React.FC = () => {
|
|||||||
{orderDetails.merchant?.name || 'Store'}
|
{orderDetails.merchant?.name || 'Store'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Need Help Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.orderNeedHelpBtn}
|
||||||
|
onPress={() => setHelpModalVisible(true)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={styles.orderNeedHelpText}>❓ Need help with this order?</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Order Status Timeline Section */}
|
{/* Order Status Timeline Section */}
|
||||||
@ -289,6 +339,113 @@ export const OrderDetailsScreen: React.FC = () => {
|
|||||||
onSubmit={handleRatingSubmit}
|
onSubmit={handleRatingSubmit}
|
||||||
onClose={handleModalClose}
|
onClose={handleModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Need Help / Ticket Creation Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={helpModalVisible}
|
||||||
|
transparent
|
||||||
|
animationType="slide"
|
||||||
|
onRequestClose={() => setHelpModalVisible(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
<Text style={styles.modalTitle}>Report Issue with Order</Text>
|
||||||
|
|
||||||
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
|
<Text style={styles.label}>Category</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.categorySelect}
|
||||||
|
onPress={() => setCategorySelectVisible(true)}
|
||||||
|
>
|
||||||
|
<Text style={styles.categorySelectText}>
|
||||||
|
{helpCategory.replace('_', ' ')}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: colors.textSecondary }}>▼</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<Text style={styles.label}>Order ID</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, { opacity: 0.6 }]}
|
||||||
|
value={orderDetails.orderNumber || orderDetails.id}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={styles.label}>Details</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, styles.textArea]}
|
||||||
|
value={helpDescription}
|
||||||
|
onChangeText={setHelpDescription}
|
||||||
|
placeholder="What seems to be the problem with this order?"
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
multiline
|
||||||
|
numberOfLines={4}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.modalBtn, styles.cancelBtn]}
|
||||||
|
onPress={() => setHelpModalVisible(false)}
|
||||||
|
disabled={submittingTicket}
|
||||||
|
>
|
||||||
|
<Text style={styles.cancelBtnText}>Cancel</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.modalBtn, styles.submitBtn]}
|
||||||
|
onPress={handleHelpSubmit}
|
||||||
|
disabled={submittingTicket}
|
||||||
|
>
|
||||||
|
{submittingTicket ? (
|
||||||
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.submitBtnText}>Submit</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Help Category Select Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={categorySelectVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setCategorySelectVisible(false)}
|
||||||
|
>
|
||||||
|
<View style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', padding: 24 }}>
|
||||||
|
<View style={{ backgroundColor: colors.cardBg, borderRadius: 12, padding: 16 }}>
|
||||||
|
<Text style={{ fontSize: 18, fontWeight: 'bold', color: colors.text, marginBottom: 12, textAlign: 'center' }}>
|
||||||
|
Select Issue Category
|
||||||
|
</Text>
|
||||||
|
{[
|
||||||
|
{ value: 'ORDER', label: 'Order' },
|
||||||
|
{ value: 'PAYMENT', label: 'Payment' },
|
||||||
|
{ value: 'PAYOUT', label: 'Payout' },
|
||||||
|
{ value: 'TECHNICAL_ISSUE', label: 'Technical Issue' },
|
||||||
|
{ value: 'ACCOUNT', label: 'Account' },
|
||||||
|
{ value: 'OTHER', label: 'Other' },
|
||||||
|
].map((item) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={item.value}
|
||||||
|
style={{ paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.border }}
|
||||||
|
onPress={() => {
|
||||||
|
setHelpCategory(item.value as TicketCategory);
|
||||||
|
setCategorySelectVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 16, color: colors.text }}>{item.label}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ marginTop: 16, paddingVertical: 12, alignItems: 'center' }}
|
||||||
|
onPress={() => setCategorySelectVisible(false)}
|
||||||
|
>
|
||||||
|
<Text style={{ color: colors.primary, fontWeight: 'bold' }}>Close</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -112,9 +112,9 @@ export const ProviderDetailsScreen: React.FC = () => {
|
|||||||
dispatch(updateCartItemThunk({ productId: product.id, quantity: newQty }));
|
dispatch(updateCartItemThunk({ productId: product.id, quantity: newQty }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRateProduct = () => {
|
// const handleRateProduct = () => {
|
||||||
navigation.navigate('WriteReviewScreen', { productId: product?.id ?? '' });
|
// navigation.navigate('WriteReviewScreen', { productId: product?.id ?? '' });
|
||||||
};
|
// };
|
||||||
|
|
||||||
const renderStars = (value: number, size = 14) => {
|
const renderStars = (value: number, size = 14) => {
|
||||||
const rounded = Math.round(value);
|
const rounded = Math.round(value);
|
||||||
@ -150,15 +150,15 @@ export const ProviderDetailsScreen: React.FC = () => {
|
|||||||
const images = product?.media?.length
|
const images = product?.media?.length
|
||||||
? product.media
|
? product.media
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
id: 'fallback',
|
id: 'fallback',
|
||||||
url: product?.imageUrl || '',
|
url: product?.imageUrl || '',
|
||||||
mediaType: 'IMAGE',
|
mediaType: 'IMAGE',
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
productId: '',
|
productId: '',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.carouselWrap}>
|
<View style={styles.carouselWrap}>
|
||||||
@ -310,7 +310,7 @@ export const ProviderDetailsScreen: React.FC = () => {
|
|||||||
<View style={styles.emptyRatingsCard}>
|
<View style={styles.emptyRatingsCard}>
|
||||||
<Text style={styles.emptyRatingsIcon}>⭐</Text>
|
<Text style={styles.emptyRatingsIcon}>⭐</Text>
|
||||||
<Text style={styles.emptyRatingsTitle}>No ratings yet</Text>
|
<Text style={styles.emptyRatingsTitle}>No ratings yet</Text>
|
||||||
<Text style={styles.emptyRatingsSubtitle}>
|
{/* <Text style={styles.emptyRatingsSubtitle}>
|
||||||
Be the first to share what you think of this product.
|
Be the first to share what you think of this product.
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@ -319,7 +319,7 @@ export const ProviderDetailsScreen: React.FC = () => {
|
|||||||
onPress={handleRateProduct}
|
onPress={handleRateProduct}
|
||||||
>
|
>
|
||||||
<Text style={styles.rateProductBtnText}>Rate this product</Text>
|
<Text style={styles.rateProductBtnText}>Rate this product</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity> */}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -342,9 +342,9 @@ export const ProviderDetailsScreen: React.FC = () => {
|
|||||||
<Text style={styles.reviewDate}>
|
<Text style={styles.reviewDate}>
|
||||||
{review.createdAt
|
{review.createdAt
|
||||||
? new Date(review.createdAt).toLocaleDateString('en-IN', {
|
? new Date(review.createdAt).toLocaleDateString('en-IN', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
})
|
})
|
||||||
: ''}
|
: ''}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@ -500,8 +500,8 @@ export const ProviderDetailsScreen: React.FC = () => {
|
|||||||
{!product
|
{!product
|
||||||
? 'Loading...'
|
? 'Loading...'
|
||||||
: product.isTrackStock && product.stockQuantity === 0
|
: product.isTrackStock && product.stockQuantity === 0
|
||||||
? 'Out of Stock'
|
? 'Out of Stock'
|
||||||
: 'Add to Cart'}
|
: 'Add to Cart'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
|
|||||||
1
app/features/screens/supportChatScreen/index.ts
Normal file
1
app/features/screens/supportChatScreen/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './supportChatScreen';
|
||||||
@ -0,0 +1,236 @@
|
|||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import { typography } from '@theme';
|
||||||
|
|
||||||
|
export const getStyles = (colors: any) =>
|
||||||
|
StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
},
|
||||||
|
headerRight: {
|
||||||
|
marginRight: 16,
|
||||||
|
},
|
||||||
|
statusBadge: {
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
ticketInfoBar: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.border,
|
||||||
|
},
|
||||||
|
ticketSubject: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
ticketMeta: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
chatList: {
|
||||||
|
padding: 16,
|
||||||
|
paddingBottom: 24,
|
||||||
|
},
|
||||||
|
messageRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginBottom: 16,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
customerRow: {
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
agentRow: {
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
},
|
||||||
|
bubble: {
|
||||||
|
maxWidth: '75%',
|
||||||
|
borderRadius: 16,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 10,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 1,
|
||||||
|
},
|
||||||
|
customerBubble: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
borderBottomRightRadius: 2,
|
||||||
|
},
|
||||||
|
agentBubble: {
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderBottomLeftRadius: 2,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
},
|
||||||
|
senderName: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
messageText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
customerText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
agentText: {
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
timeText: {
|
||||||
|
fontSize: 9,
|
||||||
|
marginTop: 4,
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
customerTime: {
|
||||||
|
color: 'rgba(255,255,255,0.7)',
|
||||||
|
},
|
||||||
|
agentTime: {
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
attachmentImage: {
|
||||||
|
width: 200,
|
||||||
|
height: 150,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
typingContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
},
|
||||||
|
typingText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
marginLeft: 6,
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
},
|
||||||
|
attachmentBtn: {
|
||||||
|
padding: 8,
|
||||||
|
marginRight: 4,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 40,
|
||||||
|
maxHeight: 100,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderRadius: 20,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.text,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
sendBtn: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
sendBtnDisabled: {
|
||||||
|
backgroundColor: colors.border,
|
||||||
|
},
|
||||||
|
sendIcon: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
previewContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
},
|
||||||
|
previewImageWrapper: {
|
||||||
|
position: 'relative',
|
||||||
|
marginRight: 12,
|
||||||
|
},
|
||||||
|
previewImage: {
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
removePreviewBtn: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: -6,
|
||||||
|
right: -6,
|
||||||
|
backgroundColor: '#FF3B30',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
borderRadius: 10,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#FFFFFF',
|
||||||
|
},
|
||||||
|
removePreviewText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
reopenContainer: {
|
||||||
|
padding: 16,
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
reopenTitle: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.semibold,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginBottom: 10,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
reopenBtn: {
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingVertical: 12,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
borderRadius: 24,
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
reopenBtnText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
fontWeight: typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: 12,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
});
|
||||||
373
app/features/screens/supportChatScreen/supportChatScreen.tsx
Normal file
373
app/features/screens/supportChatScreen/supportChatScreen.tsx
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
FlatList,
|
||||||
|
Image,
|
||||||
|
ActivityIndicator,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
Alert,
|
||||||
|
Modal,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
|
import { Header } from '@components';
|
||||||
|
import { useAppTheme } from '@theme';
|
||||||
|
import { useCustomerSupport } from '@hooks';
|
||||||
|
import { getStyles } from './supportChatScreen.styles';
|
||||||
|
import { AppStackParamList } from '../../../navigation/appStack';
|
||||||
|
import { formatTime, getFullUrl } from '../../../utils/helper';
|
||||||
|
|
||||||
|
type SupportChatRouteProp = RouteProp<AppStackParamList, 'SupportChatScreen'>;
|
||||||
|
type SupportChatNavProp = StackNavigationProp<AppStackParamList>;
|
||||||
|
|
||||||
|
// Mock attachments that users can select to simulate image attachments
|
||||||
|
const MOCK_ATTACHMENTS = [
|
||||||
|
{
|
||||||
|
id: 'att1',
|
||||||
|
label: 'Receipt Invoice',
|
||||||
|
url: 'https://images.unsplash.com/photo-1554415707-6e8cfc93fe23?w=500&auto=format&fit=crop&q=60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'att2',
|
||||||
|
label: 'Damaged Packing',
|
||||||
|
url: 'https://images.unsplash.com/photo-1607344645866-009c320c5ab8?w=500&auto=format&fit=crop&q=60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'att3',
|
||||||
|
label: 'Wrong Item Delivered',
|
||||||
|
url: 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=500&auto=format&fit=crop&q=60',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SupportChatScreen: React.FC = () => {
|
||||||
|
const { colors } = useAppTheme();
|
||||||
|
const styles = getStyles(colors);
|
||||||
|
const route = useRoute<SupportChatRouteProp>();
|
||||||
|
const navigation = useNavigation<SupportChatNavProp>();
|
||||||
|
const { ticketId } = route.params;
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentTicket,
|
||||||
|
messages,
|
||||||
|
loading,
|
||||||
|
isAgentTyping,
|
||||||
|
fetchTicketDetails,
|
||||||
|
sendMessage,
|
||||||
|
reopenTicket,
|
||||||
|
emitTypingStart,
|
||||||
|
emitTypingStop,
|
||||||
|
} = useCustomerSupport(ticketId);
|
||||||
|
|
||||||
|
const [inputText, setInputText] = useState('');
|
||||||
|
const [selectedAttachmentUrl, setSelectedAttachmentUrl] = useState<string | null>(null);
|
||||||
|
const [attachmentModalVisible, setAttachmentModalVisible] = useState(false);
|
||||||
|
const flatListRef = useRef<FlatList>(null);
|
||||||
|
const typingTimeoutRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ticketId) {
|
||||||
|
fetchTicketDetails(ticketId);
|
||||||
|
}
|
||||||
|
}, [ticketId, fetchTicketDetails]);
|
||||||
|
|
||||||
|
// Auto scroll to bottom when messages change
|
||||||
|
useEffect(() => {
|
||||||
|
if (messages.length > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
flatListRef.current?.scrollToEnd({ animated: true });
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (!inputText.trim() && !selectedAttachmentUrl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const textToSend = inputText.trim();
|
||||||
|
const attachmentsToSend = selectedAttachmentUrl ? [selectedAttachmentUrl] : undefined;
|
||||||
|
|
||||||
|
setInputText('');
|
||||||
|
setSelectedAttachmentUrl(null);
|
||||||
|
emitTypingStop();
|
||||||
|
|
||||||
|
await sendMessage(ticketId, textToSend, attachmentsToSend);
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Error', 'Failed to send message. Please try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (text: string) => {
|
||||||
|
setInputText(text);
|
||||||
|
|
||||||
|
// Emit typing start and manage typing stop timeout
|
||||||
|
emitTypingStart();
|
||||||
|
if (typingTimeoutRef.current) {
|
||||||
|
clearTimeout(typingTimeoutRef.current);
|
||||||
|
}
|
||||||
|
typingTimeoutRef.current = setTimeout(() => {
|
||||||
|
emitTypingStop();
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReopen = async () => {
|
||||||
|
try {
|
||||||
|
await reopenTicket(ticketId);
|
||||||
|
Alert.alert('Success', 'Ticket has been reopened.');
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Error', 'Failed to reopen ticket.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status?: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'OPEN':
|
||||||
|
return { bg: '#E3F2FD', text: '#1E88E5' }; // Blue
|
||||||
|
case 'IN_PROGRESS':
|
||||||
|
return { bg: '#FFF3E0', text: '#FB8C00' }; // Amber
|
||||||
|
case 'RESOLVED':
|
||||||
|
return { bg: '#E8F5E9', text: '#43A047' }; // Green
|
||||||
|
case 'CLOSED':
|
||||||
|
return { bg: '#ECEFF1', text: '#546E7A' }; // Grey
|
||||||
|
default:
|
||||||
|
return { bg: '#F5F5F5', text: '#9E9E9E' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusStyle = getStatusColor(currentTicket?.status);
|
||||||
|
|
||||||
|
if (loading && !currentTicket) {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Header title="Support Ticket" onBack={() => navigation.goBack()} />
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color={colors.primary} />
|
||||||
|
<Text style={styles.loadingText}>Loading support ticket...</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isClosed = currentTicket?.status === 'RESOLVED' || currentTicket?.status === 'CLOSED';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={styles.container}
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
|
>
|
||||||
|
<Header
|
||||||
|
title={`Support Chat`}
|
||||||
|
onBack={() => navigation.goBack()}
|
||||||
|
rightComponent={
|
||||||
|
currentTicket && (
|
||||||
|
<View style={[styles.statusBadge, { backgroundColor: statusStyle.bg }]}>
|
||||||
|
<Text style={[styles.statusText, { color: statusStyle.text }]}>
|
||||||
|
{currentTicket.status.replace('_', ' ')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{currentTicket && (
|
||||||
|
<View style={styles.ticketInfoBar}>
|
||||||
|
<Text style={styles.ticketSubject}>{currentTicket.subject}</Text>
|
||||||
|
<Text style={styles.ticketMeta}>
|
||||||
|
ID: #{currentTicket.ticketNumber} | Category: {currentTicket.category.replace('_', ' ')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FlatList
|
||||||
|
ref={flatListRef}
|
||||||
|
data={messages}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
contentContainerStyle={styles.chatList}
|
||||||
|
renderItem={({ item }) => {
|
||||||
|
const isCustomer = item.sender.roleEnum === 'CUSTOMER';
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.messageRow,
|
||||||
|
isCustomer ? styles.customerRow : styles.agentRow,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.bubble,
|
||||||
|
isCustomer ? styles.customerBubble : styles.agentBubble,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{!isCustomer && (
|
||||||
|
<Text style={styles.senderName}>{item.sender.name} (Support)</Text>
|
||||||
|
)}
|
||||||
|
{item.attachments && item.attachments.map((att: string, idx: number) => (
|
||||||
|
<Image
|
||||||
|
key={idx}
|
||||||
|
source={{ uri: getFullUrl(att) }}
|
||||||
|
style={styles.attachmentImage}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{item.message.trim() ? (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.messageText,
|
||||||
|
isCustomer ? styles.customerText : styles.agentText,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.message}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.timeText,
|
||||||
|
isCustomer ? styles.customerTime : styles.agentTime,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{formatTime(item.createdAt)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: true })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isAgentTyping && (
|
||||||
|
<View style={styles.typingContainer}>
|
||||||
|
<ActivityIndicator size="small" color={colors.textSecondary} />
|
||||||
|
<Text style={styles.typingText}>Agent is typing...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Selected attachment preview */}
|
||||||
|
{selectedAttachmentUrl && (
|
||||||
|
<View style={styles.previewContainer}>
|
||||||
|
<View style={styles.previewImageWrapper}>
|
||||||
|
<Image source={{ uri: selectedAttachmentUrl }} style={styles.previewImage} />
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.removePreviewBtn}
|
||||||
|
onPress={() => setSelectedAttachmentUrl(null)}
|
||||||
|
>
|
||||||
|
<Text style={styles.removePreviewText}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Input or Reopen prompt */}
|
||||||
|
{isClosed ? (
|
||||||
|
<View style={styles.reopenContainer}>
|
||||||
|
<Text style={styles.reopenTitle}>
|
||||||
|
This ticket has been resolved or closed.
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity style={styles.reopenBtn} onPress={handleReopen} activeOpacity={0.8}>
|
||||||
|
<Text style={styles.reopenBtnText}>Reopen Ticket</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.attachmentBtn}
|
||||||
|
onPress={() => setAttachmentModalVisible(true)}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 22 }}>📎</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={inputText}
|
||||||
|
onChangeText={handleInputChange}
|
||||||
|
placeholder="Type a message..."
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
multiline
|
||||||
|
onBlur={emitTypingStop}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.sendBtn,
|
||||||
|
(!inputText.trim() && !selectedAttachmentUrl) && styles.sendBtnDisabled,
|
||||||
|
]}
|
||||||
|
onPress={handleSend}
|
||||||
|
disabled={!inputText.trim() && !selectedAttachmentUrl}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={styles.sendIcon}>➤</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Attachment Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={attachmentModalVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setAttachmentModalVisible(false)}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.cardBg,
|
||||||
|
borderTopLeftRadius: 20,
|
||||||
|
borderTopRightRadius: 20,
|
||||||
|
padding: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Select Mock Attachment
|
||||||
|
</Text>
|
||||||
|
{MOCK_ATTACHMENTS.map((item) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={item.id}
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.border,
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
setSelectedAttachmentUrl(item.url);
|
||||||
|
setAttachmentModalVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 24, marginRight: 12 }}>🖼️</Text>
|
||||||
|
<Text style={{ fontSize: 16, color: colors.text }}>{item.label}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
backgroundColor: colors.border,
|
||||||
|
borderRadius: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
onPress={() => setAttachmentModalVisible(false)}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 16, fontWeight: 'bold', color: colors.text }}>Cancel</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SupportChatScreen;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './useCustomerSupport';
|
||||||
246
app/hooks/useCustomerSupport.ts
Normal file
246
app/hooks/useCustomerSupport.ts
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
import { tokenManager } from '@services';
|
||||||
|
import {
|
||||||
|
SupportTicket,
|
||||||
|
SupportMessage,
|
||||||
|
CreateTicketPayload,
|
||||||
|
} from '@interfaces';
|
||||||
|
import {
|
||||||
|
createTicketApi,
|
||||||
|
getMyTicketsApi,
|
||||||
|
getTicketDetailsApi,
|
||||||
|
postMessageApi,
|
||||||
|
reopenTicketApi,
|
||||||
|
uploadSupportAttachmentApi,
|
||||||
|
} from '../api/supportApi';
|
||||||
|
|
||||||
|
const BASE_URL = 'https://accb-115-187-33-94.ngrok-free.app';
|
||||||
|
const SUPPORT_SOCKET_URL = `${BASE_URL}/support`;
|
||||||
|
|
||||||
|
export const useCustomerSupport = (ticketId?: string) => {
|
||||||
|
const [tickets, setTickets] = useState<SupportTicket[]>([]);
|
||||||
|
const [currentTicket, setCurrentTicket] = useState<SupportTicket | null>(null);
|
||||||
|
const [messages, setMessages] = useState<SupportMessage[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [isAgentTyping, setIsAgentTyping] = useState<boolean>(false);
|
||||||
|
const [socketConnected, setSocketConnected] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const socketRef = useRef<Socket | null>(null);
|
||||||
|
|
||||||
|
// 1. Fetch all my tickets
|
||||||
|
const fetchTickets = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await getMyTicketsApi();
|
||||||
|
setTickets(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useCustomerSupport] fetchTickets error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 2. Fetch single ticket details
|
||||||
|
const fetchTicketDetails = useCallback(async (id: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await getTicketDetailsApi(id);
|
||||||
|
setCurrentTicket(data);
|
||||||
|
setMessages(data.messages || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useCustomerSupport] fetchTicketDetails error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 3. Create ticket
|
||||||
|
const createTicket = useCallback(async (payload: CreateTicketPayload) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const newTicket = await createTicketApi(payload);
|
||||||
|
setTickets(prev => [newTicket, ...prev]);
|
||||||
|
return newTicket;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useCustomerSupport] createTicket error:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 4. Send reply message
|
||||||
|
const sendMessage = useCallback(async (id: string, text: string, attachments?: string[]) => {
|
||||||
|
try {
|
||||||
|
const newMsg = await postMessageApi(id, { message: text, attachments });
|
||||||
|
setMessages(prev => {
|
||||||
|
if (prev.some(m => m.id === newMsg.id)) return prev;
|
||||||
|
return [...prev, newMsg];
|
||||||
|
});
|
||||||
|
return newMsg;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useCustomerSupport] sendMessage error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 5. Reopen ticket
|
||||||
|
const reopenTicket = useCallback(async (id: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const updatedTicket = await reopenTicketApi(id);
|
||||||
|
setCurrentTicket(updatedTicket);
|
||||||
|
return updatedTicket;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useCustomerSupport] reopenTicket error:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 6. Upload attachment
|
||||||
|
const uploadAttachment = useCallback(async (file: { uri: string; name: string; type: string }) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', {
|
||||||
|
uri: file.uri,
|
||||||
|
name: file.name,
|
||||||
|
type: file.type,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await uploadSupportAttachmentApi(formData);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useCustomerSupport] uploadAttachment error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 7. Client typing emits
|
||||||
|
const emitTypingStart = useCallback(() => {
|
||||||
|
if (socketRef.current && ticketId) {
|
||||||
|
socketRef.current.emit('typing_start', { ticketId });
|
||||||
|
}
|
||||||
|
}, [ticketId]);
|
||||||
|
|
||||||
|
const emitTypingStop = useCallback(() => {
|
||||||
|
if (socketRef.current && ticketId) {
|
||||||
|
socketRef.current.emit('typing_stop', { ticketId });
|
||||||
|
}
|
||||||
|
}, [ticketId]);
|
||||||
|
|
||||||
|
// 8. Socket Connection and Event Listeners Lifecycle
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ticketId) {
|
||||||
|
if (socketRef.current) {
|
||||||
|
socketRef.current.disconnect();
|
||||||
|
socketRef.current = null;
|
||||||
|
setSocketConnected(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
const setupSocket = async () => {
|
||||||
|
const token = await tokenManager.getAccessToken();
|
||||||
|
if (!token || !active) return;
|
||||||
|
|
||||||
|
const socket = io(SUPPORT_SOCKET_URL, {
|
||||||
|
auth: { token },
|
||||||
|
transports: ['websocket'],
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionAttempts: Infinity,
|
||||||
|
reconnectionDelay: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current = socket;
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
if (!active) return;
|
||||||
|
setSocketConnected(true);
|
||||||
|
console.log('[useCustomerSupport Socket] Connected to /support');
|
||||||
|
socket.emit('join_ticket_room', { ticketId });
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', (reason) => {
|
||||||
|
if (!active) return;
|
||||||
|
setSocketConnected(false);
|
||||||
|
console.log('[useCustomerSupport Socket] Disconnected:', reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('support:joined', (data) => {
|
||||||
|
console.log('[useCustomerSupport Socket] Joined room status:', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('support:new_message', (msg: SupportMessage) => {
|
||||||
|
if (!active) return;
|
||||||
|
if (msg.ticketId === ticketId) {
|
||||||
|
setMessages(prev => {
|
||||||
|
if (prev.some(m => m.id === msg.id)) return prev;
|
||||||
|
return [...prev, msg];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('support:status_changed', (data: { ticketId: string; status: any }) => {
|
||||||
|
if (!active) return;
|
||||||
|
if (data.ticketId === ticketId) {
|
||||||
|
setCurrentTicket(prev => prev ? { ...prev, status: data.status } : null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('support:typing_start', (data: { ticketId: string; role: string }) => {
|
||||||
|
if (!active) return;
|
||||||
|
if (data.ticketId === ticketId && data.role === 'ADMIN') {
|
||||||
|
setIsAgentTyping(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('support:typing_stop', (data: { ticketId: string }) => {
|
||||||
|
if (!active) return;
|
||||||
|
if (data.ticketId === ticketId) {
|
||||||
|
setIsAgentTyping(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setupSocket();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
if (socketRef.current) {
|
||||||
|
socketRef.current.emit('leave_ticket_room', { ticketId });
|
||||||
|
socketRef.current.off('connect');
|
||||||
|
socketRef.current.off('disconnect');
|
||||||
|
socketRef.current.off('support:joined');
|
||||||
|
socketRef.current.off('support:new_message');
|
||||||
|
socketRef.current.off('support:status_changed');
|
||||||
|
socketRef.current.off('support:typing_start');
|
||||||
|
socketRef.current.off('support:typing_stop');
|
||||||
|
socketRef.current.disconnect();
|
||||||
|
socketRef.current = null;
|
||||||
|
setSocketConnected(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [ticketId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tickets,
|
||||||
|
currentTicket,
|
||||||
|
messages,
|
||||||
|
loading,
|
||||||
|
isAgentTyping,
|
||||||
|
socketConnected,
|
||||||
|
fetchTickets,
|
||||||
|
fetchTicketDetails,
|
||||||
|
createTicket,
|
||||||
|
sendMessage,
|
||||||
|
reopenTicket,
|
||||||
|
uploadAttachment,
|
||||||
|
emitTypingStart,
|
||||||
|
emitTypingStop,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -99,3 +99,4 @@ export * from './order';
|
|||||||
export * from './offers';
|
export * from './offers';
|
||||||
export * from './productRating';
|
export * from './productRating';
|
||||||
export * from './wallet';
|
export * from './wallet';
|
||||||
|
export * from './support';
|
||||||
|
|||||||
51
app/interfaces/support.ts
Normal file
51
app/interfaces/support.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
export type TicketCategory =
|
||||||
|
| 'ORDER'
|
||||||
|
| 'PAYMENT'
|
||||||
|
| 'PAYOUT'
|
||||||
|
| 'TECHNICAL_ISSUE'
|
||||||
|
| 'ACCOUNT'
|
||||||
|
| 'OTHER';
|
||||||
|
|
||||||
|
export type TicketPriority = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||||
|
export type TicketStatus = 'OPEN' | 'IN_PROGRESS' | 'RESOLVED' | 'CLOSED';
|
||||||
|
|
||||||
|
export interface SupportMessage {
|
||||||
|
id: string;
|
||||||
|
ticketId: string;
|
||||||
|
senderId: string;
|
||||||
|
message: string;
|
||||||
|
attachments?: string[];
|
||||||
|
sender: {
|
||||||
|
name: string;
|
||||||
|
roleEnum: 'CUSTOMER' | 'DRIVER' | 'ADMIN';
|
||||||
|
};
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SupportTicket {
|
||||||
|
id: string;
|
||||||
|
ticketNumber: string;
|
||||||
|
category: TicketCategory;
|
||||||
|
priority: TicketPriority;
|
||||||
|
status: TicketStatus;
|
||||||
|
subject: string;
|
||||||
|
description: string;
|
||||||
|
orderId?: string;
|
||||||
|
customerId: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
messages?: SupportMessage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTicketPayload {
|
||||||
|
category: TicketCategory;
|
||||||
|
priority: TicketPriority;
|
||||||
|
subject: string;
|
||||||
|
description: string;
|
||||||
|
orderId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostMessagePayload {
|
||||||
|
message: string;
|
||||||
|
attachments?: string[];
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
HelpSupportScreen,
|
HelpSupportScreen,
|
||||||
WriteReviewScreen,
|
WriteReviewScreen,
|
||||||
OrderDetailsScreen,
|
OrderDetailsScreen,
|
||||||
|
SupportChatScreen,
|
||||||
} from '@features/screens';
|
} from '@features/screens';
|
||||||
import WalletScreen from '@features/screens/walletScreen/walletScreen';
|
import WalletScreen from '@features/screens/walletScreen/walletScreen';
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ export type AppStackParamList = {
|
|||||||
HelpSupportScreen: undefined;
|
HelpSupportScreen: undefined;
|
||||||
WriteReviewScreen: { productId: string } | undefined;
|
WriteReviewScreen: { productId: string } | undefined;
|
||||||
OrderDetailsScreen: { orderId: string } | undefined;
|
OrderDetailsScreen: { orderId: string } | undefined;
|
||||||
|
SupportChatScreen: { ticketId: string };
|
||||||
WalletScreen: undefined;
|
WalletScreen: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,6 +73,7 @@ export const AppStack: React.FC = () => {
|
|||||||
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
|
<Stack.Screen name="HelpSupportScreen" component={HelpSupportScreen} />
|
||||||
<Stack.Screen name="WriteReviewScreen" component={WriteReviewScreen} />
|
<Stack.Screen name="WriteReviewScreen" component={WriteReviewScreen} />
|
||||||
<Stack.Screen name="OrderDetailsScreen" component={OrderDetailsScreen} />
|
<Stack.Screen name="OrderDetailsScreen" component={OrderDetailsScreen} />
|
||||||
|
<Stack.Screen name="SupportChatScreen" component={SupportChatScreen} />
|
||||||
<Stack.Screen name="WalletScreen" component={WalletScreen} />
|
<Stack.Screen name="WalletScreen" component={WalletScreen} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
import { Text } from 'react-native';
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HomeScreen,
|
HomeScreen,
|
||||||
SearchScreen,
|
SearchScreen,
|
||||||
@ -19,33 +20,58 @@ export type MainTabParamList = {
|
|||||||
|
|
||||||
const Tab = createBottomTabNavigator<MainTabParamList>();
|
const Tab = createBottomTabNavigator<MainTabParamList>();
|
||||||
|
|
||||||
const tabIcons: Record<string, string> = {
|
|
||||||
HomeScreen: '🏠',
|
|
||||||
SearchScreen: '🔍',
|
|
||||||
MyOrdersScreen: '📦',
|
|
||||||
OffersScreen: '🏷️',
|
|
||||||
AccountScreen: '👤',
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTabIcon = (routeName: string, focused: boolean) => (
|
|
||||||
<Text style={{ fontSize: 22, opacity: focused ? 1 : 0.5 }}>
|
|
||||||
{tabIcons[routeName]}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const MainTabNavigator: React.FC = () => {
|
export const MainTabNavigator: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
screenOptions={({ route }) => ({
|
screenOptions={({ route }) => ({
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
tabBarIcon: ({ focused }) => renderTabIcon(route.name, focused),
|
|
||||||
|
tabBarIcon: ({ color, size, focused }) => {
|
||||||
|
let iconName: string;
|
||||||
|
|
||||||
|
switch (route.name) {
|
||||||
|
case 'HomeScreen':
|
||||||
|
iconName = focused ? 'home' : 'home-outline';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SearchScreen':
|
||||||
|
iconName = 'magnify';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'MyOrdersScreen':
|
||||||
|
iconName = focused ? 'package-variant' : 'package-variant-closed';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'OffersScreen':
|
||||||
|
iconName = focused ? 'tag' : 'tag-outline';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'AccountScreen':
|
||||||
|
iconName = focused ? 'account' : 'account-outline';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
iconName = 'circle';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={iconName}
|
||||||
|
size={size}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
tabBarActiveTintColor: '#05824C',
|
tabBarActiveTintColor: '#05824C',
|
||||||
tabBarInactiveTintColor: '#666666',
|
tabBarInactiveTintColor: '#666666',
|
||||||
|
|
||||||
tabBarStyle: {
|
tabBarStyle: {
|
||||||
|
height: 65,
|
||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
paddingTop: 8,
|
paddingTop: 8,
|
||||||
height: 60,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
tabBarLabelStyle: {
|
tabBarLabelStyle: {
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
@ -57,21 +83,25 @@ export const MainTabNavigator: React.FC = () => {
|
|||||||
component={HomeScreen}
|
component={HomeScreen}
|
||||||
options={{ tabBarLabel: 'Home' }}
|
options={{ tabBarLabel: 'Home' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="SearchScreen"
|
name="SearchScreen"
|
||||||
component={SearchScreen}
|
component={SearchScreen}
|
||||||
options={{ tabBarLabel: 'Search' }}
|
options={{ tabBarLabel: 'Search' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="MyOrdersScreen"
|
name="MyOrdersScreen"
|
||||||
component={MyOrdersScreen}
|
component={MyOrdersScreen}
|
||||||
options={{ tabBarLabel: 'Orders' }}
|
options={{ tabBarLabel: 'Orders' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="OffersScreen"
|
name="OffersScreen"
|
||||||
component={OffersScreen}
|
component={OffersScreen}
|
||||||
options={{ tabBarLabel: 'Offers' }}
|
options={{ tabBarLabel: 'Offers' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="AccountScreen"
|
name="AccountScreen"
|
||||||
component={AccountScreen}
|
component={AccountScreen}
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
"react-native-screens": "^4.25.2",
|
"react-native-screens": "^4.25.2",
|
||||||
"react-native-svg": "^15.15.5",
|
"react-native-svg": "^15.15.5",
|
||||||
"react-native-uuid": "^2.0.4",
|
"react-native-uuid": "^2.0.4",
|
||||||
|
"react-native-vector-icons": "^10.3.0",
|
||||||
"react-native-worklets": "^0.10.0",
|
"react-native-worklets": "^0.10.0",
|
||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
"reactotron-react-native": "^5.2.0",
|
"reactotron-react-native": "^5.2.0",
|
||||||
|
|||||||
37
yarn.lock
37
yarn.lock
@ -2686,6 +2686,15 @@ cliui@^6.0.0:
|
|||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
wrap-ansi "^6.2.0"
|
wrap-ansi "^6.2.0"
|
||||||
|
|
||||||
|
cliui@^7.0.2:
|
||||||
|
version "7.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||||
|
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
|
||||||
|
dependencies:
|
||||||
|
string-width "^4.2.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
wrap-ansi "^7.0.0"
|
||||||
|
|
||||||
cliui@^8.0.1:
|
cliui@^8.0.1:
|
||||||
version "8.0.1"
|
version "8.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
|
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
|
||||||
@ -5731,7 +5740,7 @@ prompts@^2.0.1, prompts@^2.4.2:
|
|||||||
kleur "^3.0.3"
|
kleur "^3.0.3"
|
||||||
sisteransi "^1.0.5"
|
sisteransi "^1.0.5"
|
||||||
|
|
||||||
prop-types@^15.8.1:
|
prop-types@^15.7.2, prop-types@^15.8.1:
|
||||||
version "15.8.1"
|
version "15.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
@ -5894,6 +5903,14 @@ react-native-uuid@^2.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/react-native-uuid/-/react-native-uuid-2.0.4.tgz#0c4f345523c5feac0282d3a51fe19887727df988"
|
resolved "https://registry.yarnpkg.com/react-native-uuid/-/react-native-uuid-2.0.4.tgz#0c4f345523c5feac0282d3a51fe19887727df988"
|
||||||
integrity sha512-LSJNeh559qC17fgVPBsWuTSW/OygFp2dwTcf94IQBLYft5FzIQS9pCsuT36OPvyvDOMb6yiGr6TafaJDnz9PPQ==
|
integrity sha512-LSJNeh559qC17fgVPBsWuTSW/OygFp2dwTcf94IQBLYft5FzIQS9pCsuT36OPvyvDOMb6yiGr6TafaJDnz9PPQ==
|
||||||
|
|
||||||
|
react-native-vector-icons@^10.3.0:
|
||||||
|
version "10.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz#de440f2627a2ed1079ce3b99d5b9d4f86894df28"
|
||||||
|
integrity sha512-IFQ0RE57819hOUdFvgK4FowM5aMXg7C7XKsuGLevqXkkIJatc3QopN0wYrb2IrzUgmdpfP+QVIbI3S6h7M0btw==
|
||||||
|
dependencies:
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
yargs "^16.1.1"
|
||||||
|
|
||||||
react-native-worklets@^0.10.0:
|
react-native-worklets@^0.10.0:
|
||||||
version "0.10.1"
|
version "0.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.1.tgz#7a3038a3b5ec66cab030a00e1568b6599696f5f2"
|
resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.10.1.tgz#7a3038a3b5ec66cab030a00e1568b6599696f5f2"
|
||||||
@ -7064,6 +7081,11 @@ yargs-parser@^18.1.2:
|
|||||||
camelcase "^5.0.0"
|
camelcase "^5.0.0"
|
||||||
decamelize "^1.2.0"
|
decamelize "^1.2.0"
|
||||||
|
|
||||||
|
yargs-parser@^20.2.2:
|
||||||
|
version "20.2.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
||||||
|
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
|
||||||
|
|
||||||
yargs-parser@^21.1.1:
|
yargs-parser@^21.1.1:
|
||||||
version "21.1.1"
|
version "21.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||||
@ -7086,6 +7108,19 @@ yargs@^15.1.0:
|
|||||||
y18n "^4.0.0"
|
y18n "^4.0.0"
|
||||||
yargs-parser "^18.1.2"
|
yargs-parser "^18.1.2"
|
||||||
|
|
||||||
|
yargs@^16.1.1:
|
||||||
|
version "16.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.2.tgz#c56731dca0d2788ae0866dd3c83907d6bab85f7d"
|
||||||
|
integrity sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==
|
||||||
|
dependencies:
|
||||||
|
cliui "^7.0.2"
|
||||||
|
escalade "^3.1.1"
|
||||||
|
get-caller-file "^2.0.5"
|
||||||
|
require-directory "^2.1.1"
|
||||||
|
string-width "^4.2.0"
|
||||||
|
y18n "^5.0.5"
|
||||||
|
yargs-parser "^20.2.2"
|
||||||
|
|
||||||
yargs@^17.3.1, yargs@^17.6.2:
|
yargs@^17.3.1, yargs@^17.6.2:
|
||||||
version "17.7.3"
|
version "17.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.3.tgz#779dffe6bcafec596a7172e983289a588647faaa"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.3.tgz#779dffe6bcafec596a7172e983289a588647faaa"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user